스프링이 지원하는 다양한 외부 설정 조회 방법
Environment
@Value - 값 주입
@ConfigurationProperties - 타입 안전한 설정 속성
이 세가지는 밑으로 갈 수록 더욱 편리하다!

MyDataSource
@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);
}
}
MyDataSourceEnvConfig
@Slf4j
@Configuration
public class MyDataSourceEnvConfig {
private final Environment env;
public MyDataSourceEnvConfig(Environment env) {
this.env = env;
} //여기에서 bean으로 주입됨!
@Bean
public MyDataSource myDataSource() {
String url = env.getProperty("my.datasource.url");
String username = env.getProperty("my.datasource.username");
String password = env.getProperty("my.datasource.password");
int maxConnection = env.getProperty("my.datasource.etc.max-connection", Integer.class);
Duration timeout = env.getProperty("my.datasource.etc.timeout",Duration.class);
List<String> options = env.getProperty("my.datasource.etc.options",List.class);
return new MyDataSource(url, username, password, maxConnection, timeout,options);
}
}
MyDataSource를 스프링 빈으로 등록하는 자바 설정이다.
Environment를 사용하면 외부 설정의 종류와 관계없이 코드 안에서 일관성 있게 외부 설정을 조회할 수 있다.
Environment.getProperty(key,Type)을 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다.(스프링 내부 변환기가 작동한다.)
- env.getProperty("my.database.etc.max-connection", Integer.class): 문자 -> 숫자로 변환
- env.getProperty("my.datasource.etc.timeout", Duration.class) : 문자 -> Duration (기간) 변환
- env.getProperty("my.datasource.etc.options", List.class) : 문자 List 변환 ( A,B [A,B] )
ExternalReadApplication
@Import(MyDataSourceEnvConfig.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalReadApplication.class, args);
}
}
@Import(MyDataSourceEnvConfig.class)를 추가했다.@SpringBootApplication(scanBasePackages = "hello.database")@Value를 사용하면 외부 설정값을 편리하게 주입받을 수 있다.
참고로 @Value도 내부에서는 Environment를 사용한다.
@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);
}
}
@Value 에 {my.datasource.etc.max-connection:1}") : key 가 없는 경우 1 을 사용한다.
ExternalReadApplication - 수정
//@Import(MyDataSourceEnvConfig.class)
@Import(MyDataSourceValueConfig.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalReadApplication.class, args);
}
}

정리
application.properties 에 필요한 외부 설정을 추가하고, @Value 를 통해서 해당 값들을 읽어서,MyDataSource 를 만들었다.
단점
@Value 를 사용하는 방식도 좋지만, @Value 로 하나하나 외부 설정 정보의 키 값을 입력받고, 주입 받아와야 하는 부분이 번거롭다. 그리고 설정 데이터를 보면 하나하나 분리되어 있는 것이 아니라 정보의 묶음으로 되어 있다. 여기서는 my.datasource 부분으로 묶여있다. 이런 부분을 객체로 변환해서 사용할 수 있다면 더 편리하고 더 좋을 것이다.
Type-safe Configuration Properties
스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다. 이것을 타입 안전한 설정 속성이라 한다.
객체를 사용하면 타입을 사용할 수 있다. 따라서 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있고, 객체를 통해서 활용할 수 있는 부분들이 많아진다. 쉽게 이야기해서 외부 설정을 자바 코드로 관리할 수 있는 것이다. 그리고 설정 정보 그 자체도 타입을 가지게 된다.
MyDataSourcePropertiesV1
@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {
private String url;
private String username;
private String password;
private Etc etc = new Etc();
@Data
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
}
}
@ConfigurationProperties가 있으면 외부 설정을 주입받는 객체라는 뜻이다. 여기에 외부 설정의 KEY묶음 시작점인 my.datasource를 적어준다.@Data에 의해 자동 생성된다.)빈 프로퍼티 방식(Property Injection)
빈 프로퍼티 주입은 자바에서 객체의 의존성을 외부에서 주입할 때 사용되는 기법이다. 이 방식에서는 특정 필드(프로퍼티)에 대해 Setter 메서드를 통해 의존성을 주입하는 것이 일반적이다.
예를 들어, 어떤 클래스가 외부 객체나 리소스에 의존할 때 그 의존성을 생성자가 아닌 Setter 메서드를 통해 설정해주는 것이 이 방식의 핵심이다.
코드에서 Setter 메서드를 정의해놓으면 Spring 컨테이너가 객체 생성 후 해당 메서드를 통해 의존성을 주입하게 된다.
설정 속성을 어떻게 사용하냐면 ~
MyDataSourceConfigV1
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());
}
}
ExternalReadApplication - 수정
//@Import(MyDataSourceValueConfig.class)
@Import(MyDataSourceConfigV1.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {...}

정리
application.properties 에 필요한 외부 설정을 추가하고, @ConfigurationProperties 를 통해서 MyDataSourcePropertiesV1 에 외부 설정의 값들을 설정했다. 그리고 해당 값들을 읽어서 MyDataSource 를 만들었다.
표기법 변환
maxConnection 은 표기법이 서로 다르다. 스프링은 캐밥 표기법을 자바 낙타 표기법으로 중간에서 자동으로 변환해준다.
@ConfigurationPropertiesScan
@ConfigurationProperties 를 하나하나 직접 등록할 때는@ConfigurationPropertiesScan 예시
@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyApplication {}
빈을 직접 등록하는 것과 컴포넌트 스캔을 사용하는 차이와 비슷하다.
문제
MyDataSourcePropertiesV1 은 스프링 빈으로 등록된다. 그런데 Setter 를 가지고 있기 때문에 누군가 실수로 값을 변경하는 문제가 발생할 수 있다. 여기에 있는 값들은 외부 설정값을 사용해서 초기에만 설정되고, 이후에는 변경 하면 안된다. 이럴 때 Setter 를 제거하고 대신에 생성자를 사용하면 중간에 데이터를 변경하는 실수를 근본적으로 방지할 수 있다.
이런 문제가 없을 것 같지만, 한번 발생하면 정말 잡기 어려운 버그가 만들어진다.
대부분의 개발자가 MyDataSourcePropertiesV1 의 값은 변경하면 안된다고 인지하고 있지만, 어떤 개발자가 자신의 문제를 해결하기 위해 setter 를 통해서 값을 변경하게 되면, 애플리케이션 전체에 심각한 버그를 유발할 수 있다. 좋은 프로그램은 제약이 있는 프로그램이다.
@ConfigurationProperties 는 Getter, Setter를 사용하는 자바빈 프로퍼티 방식이 아니라 생성자를 통해서 객체를 만드는 기능도 지원한다. 다음 코드를 통해서 확인해보자.
MyDataSourcePropertiesV2
@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {
private String url;
private String username;
private String password;
private Etc etc;
//이게 생성자! Setter 대신!
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;
public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
참고 @ConstructorBinding
스프링부트 3.0이전에는 생성자 바인딩 시에 @ConstructorBinding 애노테이션을 필수로 사용해야 했다.
스프링부트 3.0 부터는 생성자가 하나일 때는 생략할 수 있다. 생성자가 둘 이상인 경우에는 사용할 생성자에 @ConstructorBinding어노테이션을 적용하면 된다.
Setter 없이 생성자 주입을 이용해 만든 MyDataSourcePropertiesV2 를 사용해보자.
@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());
}
}

생성자 주입 방식은 Setter 없이 생성자를 만들어놓으면 되는 방식이었다.
문제
타입과 객체를 통해서 숫자에 문자가 들어오는 것 같은 기본적인 타입 문제들은 해결이 되었다. 그런데 타입은 맞는데 숫자의 범위가 기대하는 것과 다르면 어떻게 될까? 예를 들어서 max-connection 의 값을 0 으로 설정하면 커넥션이 하나도 만들어지지 않는 심각한 문제가 발생한다고 가정해보자.
max-connection 은 최소 1 이상으로 설정하지 않으면 애플리케이션 로딩 시점에 예외를 발생시켜서 빠르게 문제를 인지할 수 있도록 하고 싶다.
예를 들어서 최대 커넥션 숫자는 최소 1 최대 999 라는 범위를 가져야 한다면 어떻게 검증할 수 있을까? 이메일을 외부 설정에 입력했는데, 만약 이메일 형식에 맞지 않는다면 어떻게 검증할 수 있을까?
개발자가 직접 하나하나 검증 코드를 작성해도 되지만, 자바에는 자바 빈 검증기(java bean validation)이라는 훌륭한 표준 검증기가 제공된다.
@ConfigurationProperties 은 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원한다.
자바 빈 검증기를 사용하려면 spring-boot-starter-validation 이 필요하다. build.gradle 에 다음 코드를 추가하자.
implementation 'org.springframework.boot:spring-boot-starter-validation' //추가
검증기를 추가해서 ConfigurationProperties를 만들어보자.
MyDataSourcePropertiesV3
@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;
public Etc(int maxConnection, Duration timeout, List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
@NotEmpty url , username , password 는 항상 값이 있어야 한다. 필수 값이 된다.
@Min(1) @Max(999) maxConnection : 최소 1 , 최대 999 의 값을 허용한다.
@DurationMin(seconds = 1) @DurationMax(seconds = 60) : 최소 1, 최대 60초를 허용한다.
jakarta.validation.constraints.Max
패키지 이름에 jakarta.validation 으로 시작하는 것은 자바 표준 검증기에서 지원하는 기능이다.
org.hibernate.validator.constraints.time.DurationMax
패키지 이름에 org.hibernate.validator 로 시작하는 것은 자바 표준 검증기에서 아직 표준화 된 기능은 아니고, 하이버네이트 검증기라는 표준 검증기의 구현체에서 직접 제공하는 기능이다. 대부분 하이버네이트 검증기를 사용하므로 이 부분이 크게 문제가 되지는 않는다.
MyDataSourceConfigV3
@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());
}
}


스프링은 설정 데이터를 사용할 때 application.properties 뿐만 아니라 application.yml이라는 형식도 지원한다.
YAML(YAML Ain't Markup Language)은 사람이 읽기 좋은 데이터 구조를 목표로 한다. 확장자는 yaml , yml 이다. 주로 yml 을 사용한다.
application.properties 예시
environments.dev.url=https://dev.example.com
environments.dev.name=Developer Setup
environments.prod.url=https://another.example.com
environments.prod.name=My Cool App
application.yml 예시
environments:
dev:
url: "https://dev.example.com"
name: "Developer Setup"
prod:
url: "https://another.example.com"
name: "My Cool App"
YAML의 가장 큰 특징은 사람이 읽기 좋게 계층 구조를 이룬다는 점이다.
YAML은 space(공백)로 계층 구조를 만든다. space는 1칸을 사용해도 되는데, 보통 2칸을 사용한다.
구분기호로 : 를 사용한다. 만약 값이 있다면 이렇게 key: value: 이후에 공백을 하나 넣고 값을 넣어주면 된다.
스프링은 YAML의 계층 구조를 properties 처럼 평평하게 만들어서 읽어들인다. 쉽게 이야기해서 위의 application.yml 예시는 application.properties 예시처럼 만들어진다.

주의
application.properties, application.yml을 같이 사용하면 application.properties가 우선권을 가진다.
하지만, 이 둘을 함께 사용하는 것은 일관성이 없으므로 권장하지 않는다.
실무에서는 설정정보가 많기 때문에 보기 편한 yml을 선호한다.
YML에도 프로필을 적용할 수 있다.
application.yml
my:
datasource:
url: local.db.com
username: local_user
password: local_pw
etc:
maxConnection: 2
timeout: 60s
options: LOCAL, CACHE
spring:
config:
activate:
on-profile: dev
my:
datasource:
url: dev.db.com
username: dev_user
password: dev_pw
etc:
maxConnection: 10
timeout: 60s
spring:
config:
activate:
on-profile: prod
my:
datasource:
url: prod.db.com
username: prod_user
password: prod_pw
etc:
maxConnection: 50
timeout: 10s
options: PROD, CACHE
- yml은 ---dash(-) 3개를 사용해서 논리 파일을 구분한다.
- spring.config.active.on-profile을 사용해서 프로필을 적용할 수 있다.
- 나머지는 application.propeerties와 동일하다.
## @Profile
프로필과 외부 설정을 사용해서 각 환경마다 설정값을 다르게 적용하는 것은 이해했다.
그런데 설정값이 다른 정도가 아니라 각 환경마다 서로 다른 빈을 등록해야 한다면 어떻게 해야할까?
예를 들어서 결제 기능을 붙여야 하는데, 로컬 개발 환경에서는 실제 결제가 발생하면 문제가 되니 가짜 결제 기능이 있는 스프링 빈을 등록하고, 운영 환경에서는 실제 결제 기능을 제공하는 스프링 빈을 등록한다고 가정해보자.


PayConfig
```java
@Slf4j
@Configuration
public class PayConfig {
@Bean
@Profile("default")
public LocalPayClient localPayClient() {
log.info("LocalPayClient 빈 등록");
return new LocalPayClient();
}
@Bean
@Profile("prod")
public ProdPayClient prodPayClient() {
log.info("ProdPayClient 빈 등록");
return new ProdPayClient();
}
}



