참고용 이전 발행글
2025.05.04 - [Backend/Spring(활용)] - Spring Boot 외부 설정(5) - 설정 관리 전략: 외부 파일, 내부 분리, 내부 통합 비교와 우선순위
참고용 소스코드
https://github.com/namic123/Spring-Dev-Notes/tree/master/spring-external-read
1. @Value를 통한 외부 설정 주입
스프링 프레임워크는 외부 설정값을 자바 코드에 주입하는 다양한 방법을 제공하는데, 그중 가장 직관적이고 간단한 방식이 @Value 애노테이션이다. 이 애노테이션은 application.properties, application.yml, 환경 변수 등에서 값을 읽어올 수 있으며, Environment 추상화를 내부적으로 활용하여 작동한다.
우선, 여러 테스트를 위해 아래 properties 설정과 클래스를 생성한다.
application.properties
my.datasource.url=local.db.com
my.datasource.username=local_user
my.datasource.password=local_pw
my.datasource.etc.max-connection=1
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN
POJO 클래스 (MyDataSource)
- 외부 설정값을 담는 DTO 형태의 객체
- @PostConstruct로 값 출력
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;
@Slf4j
@Data
public class MyDataSource {
private String url;
private String username;
private String password;
private int maxConnection;
private Duration timeout;
private List<String> options;
public MyDataSource(String url, String username, String password, int
maxConnection, Duration timeout, List<String> options) {
this.url = url;
this.username = username;
this.password = password;
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
@PostConstruct
public void init() {
log.info("url={}", url);
log.info("username={}", username);
log.info("password={}", password);
log.info("maxConnection={}", maxConnection);
log.info("timeout={}", timeout);
log.info("options={}", options);
}
}
아래는 @Value를 활용해 외부 설정값을 읽어와서 MyDataSource 빈을 생성하는 예제이다.
MyDataSourceValueConfig 구성 클래스
import hello.datasource.MyDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.List;
@Slf4j
@Configuration
public class MyDataSourceValueConfig {
@Value("${my.datasource.url}")
private String url;
@Value("${my.datasource.username}")
private String username;
@Value("${my.datasource.password}")
private String password;
@Value("${my.datasource.etc.max-connection}")
private int maxConnection;
@Value("${my.datasource.etc.timeout}")
private Duration timeout;
@Value("${my.datasource.etc.options}")
private List<String> options;
@Bean
public MyDataSource myDataSource1() {
return new MyDataSource(url, username, password, maxConnection, timeout,
options);
}
@Bean
public MyDataSource myDataSource2(
@Value("${my.datasource.url}") String url,
@Value("${my.datasource.username}") String username,
@Value("${my.datasource.password}") String password,
@Value("${my.datasource.etc.max-connection}") int maxConnection,
@Value("${my.datasource.etc.timeout}") Duration timeout,
@Value("${my.datasource.etc.options}") List<String> options) {
return new MyDataSource(url, username, password, maxConnection, timeout,
options);
}
}
이 클래스에서는 두 가지 방식으로 외부 설정값을 주입받는다.
- 필드 주입 방식 (myDataSource1)
설정값을 클래스의 필드에 직접 주입받아, 빈 생성 시 해당 필드를 사용한다. - 메서드 파라미터 주입 방식 (myDataSource2)
빈을 생성하는 메서드의 파라미터에 직접 @Value를 붙여 주입받는다.
이처럼 @Value는 상황에 따라 필드, 생성자, 메서드 파라미터 등 다양한 위치에서 사용이 가능하다.
SpringBootApplication class에 설정 클래스 등록하기
@Import(MyDataSourceValueConfig.class) // 설정 클래스 등록
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalReadApplication.class, args);
}
}
실행 결과는 아래와 같으며, 같은 설정으로(myDataSource1, 2) 두 개의 빈이 생성되므로 로그는 두 번 출력된다.
2. @Value 방식의 한계와 기본값 처리
@Value 방식은 단일 설정을 빠르게 주입할 수 있다는 장점이 있으나, 다음과 같은 한계가 존재한다.
- 설정 키를 문자열로 직접 입력해야 하므로 오타에 취약
- 논리적으로 연관된 설정 그룹을 관리하기 어려움
- 설정 항목이 많아질수록 코드가 장황해지고 복잡해짐
기본값을 설정하는 문법은 다음과 같다
@Value("${my.datasource.etc.max-connection:10}")
private int maxConnection;
이처럼 : 이후 값을 지정하면 해당 키가 없을 경우 기본값을 사용하게 된다.
3. @ConfigurationProperties의 개념과 구조
복잡한 설정을 구조화하여 객체 단위로 다루고자 할 때는 @ConfigurationProperties를 사용하는 것이 바람직하다. 이 방식은 설정 키의 접두사(prefix)를 기준으로 계층 구조를 자바 객체로 매핑해주며, 다음과 같이 클래스를 정의할 수 있다.
설정 속성 객체 생성: MyDataSourcePropertiesV1
먼저 외부 설정의 키 구조에 맞추어 자바 클래스를 정의한다. 예컨대 my.datasource로 시작하는 설정 묶음을 표현하기 위해 다음과 같이 클래스를 구성할 수 있다.
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {
private String url;
private String username;
private String password;
private Etc etc;
@Data
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
}
}
이 클래스는 설정 키의 prefix가 my.datasource인 항목들을 매핑할 수 있도록 설계되었으며, 내부 클래스 Etc를 통해 하위 구조도 명확히 표현하였다. 스프링 부트는 이러한 클래스에 대해 자동으로 application.properties 값을 매핑해주는 기능을 제공한다.
설정 클래스의 사용: MyDataSourceConfigV1
import hello.datasource.MyDataSource;
import hello.datasource.MyDataSourcePropertiesV1;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
public class MyDataSourceConfigV1 {
private final MyDataSourcePropertiesV1 properties;
public MyDataSourceConfigV1(MyDataSourcePropertiesV1 properties) {
this.properties = properties;
}
@Bean
public MyDataSource dataSource() {
return new MyDataSource(
properties.getUrl(),
properties.getUsername(),
properties.getPassword(),
properties.getEtc().getMaxConnection(),
properties.getEtc().getTimeout(),
properties.getEtc().getOptions());
}
}
위 클래스는 @EnableConfigurationProperties를 통해 외부 설정 객체를 스프링 컨테이너에 빈으로 등록하고, 이를 주입받아 실제 사용할 객체 MyDataSource에 값을 전달한다. 설정값은 불변 객체를 생성하는 데 사용되며, 이후에는 변경되지 않도록 관리된다
설정 오류 방지: 타입 안전성 확보
@ConfigurationProperties의 가장 큰 장점은 타입 안전성이다. 예를 들어 maxConnection은 정수형이어야 하는데, 실수로 문자열 "abc"를 입력하게 되면 스프링은 애플리케이션 로딩 시점에 다음과 같은 오류를 발생시킨다.
이러한 방식은 개발자가 타입 오류를 조기에 발견할 수 있도록 돕고, 애플리케이션 실행 후 발생할 수 있는 예외를 사전에 방지해준다.
자동 등록을 위한 대안: @ConfigurationPropertiesScan
여러 설정 클래스를 사용할 경우, 매번 @EnableConfigurationProperties로 등록하는 대신, @ConfigurationPropertiesScan을 사용하여 지정된 패키지 내 모든 설정 객체를 자동으로 인식시킬 수 있다.
@Import(MyDataSourceConfigV1.class) // 설정 클래스 등록
@ConfigurationPropertiesScan // 설정 클래스 스캔
// @ConfigurationPropertiesScan("com.example.config") // 패키지 지정도 가능
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalReadApplication.class, args);
}
}
4. 생성자 기반 바인딩 방식
위에서 외부 설정 값을 주입하기 위한 방법으로 @ConfigurationProperties를 사용해왔으며, 일반적으로는 JavaBean 스타일의 Getter/Setter 기반 방식을 따랐다. 하지만 이 방식은 객체 생성 후에도 값이 수정될 수 있다는 단점이 있으며, 이로 인해 실수로 설정값이 변경되는 위험이 존재한다. 이러한 문제를 방지하기 위한 더욱 안전한 방법으로 생성자 기반 바인딩 방식을 사용할 수 있다.
설정 객체 구성 – 생성자 바인딩 방식
MyDataSourcePropertiesV2 클래스는 다음과 같이 생성자 기반으로 외부 설정을 받아들이는 구조로 정의된다.
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {
private String url;
private String username;
private String password;
private Etc etc;
public MyDataSourcePropertiesV2(String url, String username, String password, @DefaultValue Etc etc) {
this.url = url;
this.username = username;
this.password = password;
this.etc = etc;
}
@Getter
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
이 구조는 불변성(immutability) 을 보장하며, 애플리케이션 실행 이후에는 설정 값이 변경되지 않도록 설계되어 있다. 이는 설정 값의 안정성을 극대화하며, 예측 가능한 동작을 유도하는 데 효과적이다.
- @DefaultValue 어노테이션은 해당 설정 키가 존재하지 않을 경우 기본값을 지정하는 역할을 하며,
- 스프링 부트 3.0 이상에서는 생성자가 하나만 존재한다면 @ConstructorBinding 어노테이션 없이도 동작한다.
설정 객체 사용 – 설정 클래스 등록
import hello.datasource.MyDataSource;
import hello.datasource.MyDataSourcePropertiesV2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV2.class)
public class MyDataSourceConfigV2 {
private final MyDataSourcePropertiesV2 properties;
public MyDataSourceConfigV2(MyDataSourcePropertiesV2 properties) {
this.properties = properties;
}
@Bean
public MyDataSource dataSource() {
return new MyDataSource(
properties.getUrl(),
properties.getUsername(),
properties.getPassword(),
properties.getEtc().getMaxConnection(),
properties.getEtc().getTimeout(),
properties.getEtc().getOptions());
}
}
장점
생성자 기반의 @ConfigurationProperties 방식은 다음과 같은 장점을 갖는다.
- 설정 객체를 불변(immutable)하게 유지하여 중간에 값 변경을 차단할 수 있다.
- 타입뿐만 아니라 객체의 구조에 맞는 안전한 설정 주입을 지원한다.
- 잘못된 타입 입력 시 애플리케이션 초기 로딩에서 오류를 발생시켜 조기에 문제를 인지할 수 있다.
- 기본값 설정을 통해 필수 설정 누락 시에도 유연하게 대응할 수 있다.
5. 설정 값 검증과 Bean Validation 적용
타입 안정성은 확보되었지만, 설정값의 유효성은 별도로 검증해주어야 한다. 예를 들어, 최대 커넥션 수는 1 이상이어야 하고, 타임아웃은 1초 이상이어야 한다. 이를 위해 @Validated 애노테이션과 함께 Bean Validation 어노테이션을 사용한다.
의존성 추가
스프링 부트에서 자바 빈 검증기를 사용하기 위해서는 spring-boot-starter-validation 의존성을 build.gradle 파일에 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
설정 클래스 등록
import hello.datasource.MyDataSource;
import hello.datasource.MyDataSourcePropertiesV3;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV3.class)
public class MyDataSourceConfigV3 {
private final MyDataSourcePropertiesV3 properties;
public MyDataSourceConfigV3(MyDataSourcePropertiesV3 properties) {
this.properties = properties;
}
@Bean
public MyDataSource dataSource() {
return new MyDataSource(
properties.getUrl(),
properties.getUsername(),
properties.getPassword(),
properties.getEtc().getMaxConnection(),
properties.getEtc().getTimeout(),
properties.getEtc().getOptions());
}
}
@Import(MyDataSourceConfigV3.class)
@ConfigurationPropertiesScan
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalReadApplication.class, args);
}
}
설정 속성 클래스에서의 검증 적용
검증이 필요한 설정 클래스에는 @Validated 애노테이션을 적용하고, 필드에 검증용 제약 조건 애노테이션을 선언한다.
예제 클래스인 MyDataSourcePropertiesV3는 다음과 같은 검증 규칙을 포함하고 있다.
- @NotEmpty : 문자열이 비어 있지 않아야 함 (필수 입력)
- @Min(1) / @Max(999) : 숫자 값의 최소 및 최대 범위 제한
- @DurationMin, @DurationMax : 시간 값의 최소/최대 제한
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import org.hibernate.validator.constraints.time.DurationMax;
import org.hibernate.validator.constraints.time.DurationMin;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Getter
@ConfigurationProperties("my.datasource")
@Validated
public class MyDataSourcePropertiesV3 {
@NotEmpty
private String url;
@NotEmpty
private String username;
@NotEmpty
private String password;
private Etc etc;
public MyDataSourcePropertiesV3(String url, String username, String password, Etc etc) {
this.url = url;
this.username = username;
this.password = password;
this.etc = etc;
}
@Getter
public static class Etc {
@Min(1)
@Max(999)
private int maxConnection;
@DurationMin(seconds = 1)
@DurationMax(seconds = 60)
private Duration timeout;
private List<String> options = new ArrayList<>();
public Etc(int maxConnection, Duration timeout, List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
이처럼 설정값이 정해진 범위를 벗어날 경우, 애플리케이션은 실행 시점에서 예외를 발생시켜 문제를 조기에 감지할 수 있도록 돕는다.
또한, @DurationMin과 @DurationMax는 스프링이 기본적으로 사용하는 Hibernate Validator에서 제공하는 기능으로, 표준 자바 검증기에서 지원하지 않는 타입에 대한 유효성 검사를 수행할 수 있다.
아래는 설정값이 정해진 범위를 벗어난 경우의 예시이다.
7. 마무리 정리
스프링에서 외부 설정값을 주입하는 방식은 상황에 따라 다양한 선택지를 제공하며, 각 방식의 특징은 다음과 같다.
방식 | 장점 | 단점 |
@Value | 간단하고 빠르게 사용 가능 | 키 직접 입력, 구조화 어려움 |
@ConfigurationProperties | 계층적 구조 표현 가능, 가독성 우수 | 등록 과정 필요 |
생성자 바인딩 | 불변성, 안정성 확보 | 스프링 부트 2.2 이상 필요 |
Bean Validation | 설정 유효성 검증 가능 | 의존성 추가 필요 |
실무에서는 설정 항목이 많고 복잡할수록 @ConfigurationProperties와 생성자 기반 바인딩, 그리고 Bean Validation을 함께 사용하는 것이 가장 안전하고 유지보수에 유리한 전략이라 할 수 있다.
'Backend > Spring(활용)' 카테고리의 다른 글
Spring Boot Actuator - 프로덕션 환경 준비 : 모니터링 시각화를 위한 첫 단계 (2) | 2025.05.14 |
---|---|
SpringBoot 외부 설정(7) - YAML 프로필 구성과 @Profile 기반 환경별 빈 등록 전략 (0) | 2025.05.13 |
Spring Boot 외부 설정(5) - 설정 관리 전략: 외부 파일, 내부 분리, 내부 통합 비교와 우선순위 (2) | 2025.05.04 |
SpringBoot 외부설정(4) - 다양한 외부 설정 방법, 하나의 접근 방식 (Environment, PropertySource) (1) | 2025.05.02 |
SpringBoot 외부 설정(3) - 커맨드 라인 옵션 인수 활용 방법 (2) | 2025.05.01 |