Spring Boot 핵심 기능 4 - 외부설정(2)
이전 포스팅에서 다루었던 커맨드 라인 옵션 인수, 자바 시스템 속성, OS 환경변수는 모두 외부 설정을 key=value 형식으로 사용할 수 있는 방법입니다. 그런데 어디에 있는 외부 설정값을 읽어야 하는지에 따라 각각 읽는 방법이 다르다는 단점이 있습니다. 스프링은 이 문제를 Environment와 PropertySource라는 추상화를 통해서 해결합니다.
스프링의 외부 설정 통합
스프링은 로딩 시점에 필요한 PropertySource들을 생성하고, Environment에서 사용할 수 있게 연결합니다. 모든 외부 설정은 environment.getProperty(key)를 통해서 조회하면 됩니다. application.properties, application.yml도 PropertySource에 추가되기에 Environment를 통해서 접근할 수 있습니다.
package hello;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class EnvironmentCheck {
private final Environment env;
public EnvironmentCheck(Environment env) {
this.env = env;
}
@PostConstruct
public void init() {
String url = env.getProperty("url");
String username = env.getProperty("username");
String password = env.getProperty("password");
log.info("env url={}", url);
log.info("env username url={}", username);
log.info("env password url={}", password);
}
}
- 커맨드 라인 옵션 인수 실행: --url=devdb --username=dev_user --password=dev_pw
- 자바 시스템 속성 실행: -Durl=devdb -Dusername=dev_user -Dpassword=dev_pw
스프링은 Environment는 물론이고 Environment를 활용해서 더 편리하게 외부 설정을 읽는 방법들을 제공합니다.
- Environment
- @Value: 값 주입
- @ConfigurationProperties: 타입 안전한 설정 속성
다음의 예제 코드를 기반으로 외부 설정을 읽어서 활용하는 다양한 방법들을 보겠습니다.
MyDataSource.class
package hello.datasource;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;
@Slf4j
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);
}
}
application.properties
my.datasource.url=local.db.com
my.datasource.username=username
my.datasource.password=password
my.datasource.etc.max-connection=1
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN
Environment
package hello.config;
import hello.datasource.MyDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import java.time.Duration;
import java.util.List;
@Slf4j
@Configuration
public class MyDataSourceEnvConfig {
private final Environment env;
public MyDataSourceEnvConfig(Environment env) {
this.env = env;
}
@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.getProperty(key, Type)를 호출할 때 타입 정보를 주면 해당 타입으로 변환해줍니다.
- env.getProperty("my.datasource.etc.max-connection", Integer.class): 문자 → 숫자로 변환
- env.getProperty("my.datasource.etc.timeout", Duration.class): 문자 → Duration(기간) 변환
- env.getProperty("my.datasource.etc.options", List.class): 문자 → List 변환
@Value
@Value를 사용하면 외부 설정값을 편리하게 주입받을 수 있습니다. @Value도 내부에서는 Environment를 사용합니다.
package hello.config;
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 aMyDataSourceValueConfig {
@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에 ${}를 사용해서 외부 설정의 키 값을 주면 원하는 값을 주입 받을 수 있습니다.
- @Value는 필드에 사용(myDataSource1)할 수도 있고, 파라미터에 사용(myDataSource2)할 수도 있습니다.
- 기본값을 사용하려면 다음과 같이 : 뒤에 기본값을 적어주면 됩니다.
예) @Value("${my.datasource.etc.max-connection:1}"): key가 없는 경우 1을 사용합니다.
@ConfigurationProperties
스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공합니다. 이것을 타입 안전한 설정 속성(Type-safe Configuration Properties)이라 합니다.
- 외부 설정을 객체로 편리하게 변환해서 사용할 수 있습니다.
- 외부 설정의 계층을 객체로 편리하게 표현할 수 있습니다.
- 외부 설정을 타입 안전하게 사용할 수 있습니다.
- 검증기를 적용할 수 있습니다.
package hello.datasource;
import lombok.Data;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
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;
public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
- @ConfigurationProperties이 있으면 외부 설정을 주입 받는 객체라는 뜻입니다. 여기에 외부 설정 KEY의 묶음 시작점인 my.datasource를 적어줍니다.
- @DefaultValue: 해당 값을 찾을 수 없는 경우 기본값을 사용합니다.
Note. @ConstructorBinding
스프링 3.0 이전에는 생성자 바인딩 시에 @ConstructorBinding 애노테이션을 필수로 사용해야 했습니다.
스프링 3.0 부터는 생성자가 하나일 때는 생략할 수 있으며, 둘 이상인 경우 사용할 생성자에 @ConstructorBinding 애노테이션을 적용하면 됩니다.
package hello.config;
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());
}
}
- @EnableConfigurationProperties(MyDataSourcePropertiesV2.class): 스프링에게 사용할 @ConfigurationProperties를 지정해주어야 합니다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있습니다.
- private final MyDataSourcePropertiesV2 properties: 설정 속성을 생성자를 통해 주입 받아서 사용합니다.
@ConfigurationProperties 검증
자바 빈 검증기를 사용하려면 spring-boot-starter-validation이 필요합니다. 다음과 같이 의존성을 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
검증기를 추가해서 ConfigurationProperties을 만듭니다.
package hello.datasource;
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.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
import java.time.Duration;
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;
public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
YAML
스프링은 설정 데이터를 사용할 때 application.properties 뿐만 아니라 application.yml 이라는 형식도 지원합니다. YAML(YAML Ain't Markup Language)은 사람이 읽기 좋은 데이터 구조를 목표로 하며, 확장자는 yaml, yml 입니다. 주로 yml을 사용합니다.
application.yml
my:
datasource:
url: local.db.com
username: local_user
password: local_pw
etc:
max-connection: 1
timeout: 60s
options: LOCAL, CACHE
- YAML의 가장 큰 특징은 사람이 읽기 좋게 계층 구조를 이룬다는 점입니다.
- YAML은 space(공백)로 계층 구조를 만듭니다. space는 1칸을 사용해도 되는데, 보통 2칸을 사용합니다.
- 구분 기호로 : 를 사용합니다.
주의. application.properties, application.yml을 같이 사용하면 application.properties가 우선권을 가집니다. 이것을 둘이 함께 사용하는 것은 일관성이 없으므로 권장하지 않으며, 실무에서는 설정 정보가 많아서 보기 편한 yml을 선호합니다.
다음은 yml에 프로필을 적용한 예입니다.
my:
datasource:
url: local.db.com
username: local_user
password: local_pw
etc:
max-connection: 1
timeout: 60s
options: LOCAL, CACHE
---
spring:
config:
activate:
on-profile: dev
my:
datasource:
url: dev.db.com
username: dev_user
password: dev_pw
etc:
max-connection: 10
timeout: 60s
options: DEV, CACHE
---
spring:
config:
activate:
on-profile: prod
my:
datasource:
url: prod.db.com
username: prod_user
password: prod_pw
etc:
max-connection: 50
timeout: 10s
options: PROD, CACHE
@Profile
@Profile은 특정 조건에 따라서 해당 빈을 등록할지 말지 선택합니다. 코드를 보면 @Conditional(ProfileCondition.class)를 확인할 수 있습니다. 스프링은 @Conditional 기능을 활용해서 개발자가 더 편리하게 사용할 수 있는 @Profile 기능을 제공하는 것입니다. @Profile을 사용하면 각 환경 별로 외부 설정 값을 분리하는 것을 넘어 등록되는 스프링 빈도 분리할 수 있습니다.
package org.springframework.context.annotation;
...
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
다음은 default 프로필(기본값)이 활성화 되어 있으면 LocalPayClient를, prod 프로필이 활성화 되어 있으면 ProdPayClient를 빈으로 등록하는 예제 코드입니다.
package hello.pay;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@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();
}
}
[참고정보]