my.datasource.url=local.db.com
my.datasource.username=username
my.datasource.password=password
my.datasource.etc.max-connection=10
my.datasource.etc.options=CACHE,ADMIN
my.datasource.etc.timeout=3500ms
application.properties에 들어갈 외부설정은 위와 같다. 이를 읽을 방법으로, 우리는 스프링의Environment 인터페이스를 활용했었다. 스프링이 지원하는 다른 방법들이 존재하는데 애노테이션 방식인@ConfigurationProperties로 하여금 더 편리한 이용이 가능하다.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class MyDataSourceEnvConfig {
private final Environment 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);
List<String> options = env.getProperty("my.datasource.etc.options", List.class);
Duration timeout = env.getProperty("my.datasource.etc.timeout", Duration.class);
MyDataSource myDataSource = new MyDataSource(url, username, password, maxConnection, timeout, options);
return myDataSource;
}
}
MyDataSource는 환경설정의 값들을 보관할 객체이다. 기존에 배웠던 Environment를 통해 외부설정을 곧바로 읽어들여와 MyDataSource를 빈 등록하는 시점에 값들을 채워넣는다.
기존 프로세스는 Environment로 하여금 설정을 읽어와서 MyDataSource에 넣었다. 이 방식에서 Envrioment로 읽기를 ConfigurationProperties애노테이션으로 하여금 자동화할 수 있다.
@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<>();
}
}
위와 같이 @ConfigurationProperties("my.datasource")를 주면 그 아래 설정 값들을 모두 필드로 읽어오게 된다. @Data로 하여금 setter와 getter를 확보한 클래스이므로 프로퍼티 주입방식으로 설정정보를 읽어오게 된다.
이 클래스를 빈으로 등록하면 빈으로 등록될 때 자동적으로 설정값을 읽어와 빈으로 등록된다. 이 빈 객체(MyDataSourcePropertiesV1)에서 값을 뽑아 MyDataSource에 담아주면 된다.
위에서는 Setter 사용으로 하여금 설정 값을 읽어오는 방식이지만 누군가가 setter를 사용할 수 있다는 단점이 존재한다. 설정 값을 애플리케이션 로직 내에서 바꾸는 시나리오는 100% 존재할 수 없기에 Setter를 열어놓는 것은 좋지 않다. 생성자 방식은 이에 대한 해답이 된다.
@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {
private String url;
private String username;
private String password;
private Etc etc;
@Getter
public static class Etc {
private int maxConnection;
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;
}
}
public MyDataSourcePropertiesV2(String url, String username, String password, Etc etc) {
this.url = url;
this.username = username;
this.password = password;
this.etc = etc;
}
}
클래스에 생성자를 만들고 내부 클래스인 Etc에도 생성자를 만들어 주입하는 길을 열어준다면 스프링은 설정 값을 이에 주입해준다.
만약 누군가가 max-connection값에 10000을 대입했고 이는 시스템에서 매우 과한 숫자로 받아들여진다고 가정해보자. 그렇기에 우리는 최소1, 최대999라는 숫자만 max-connection 값으로 들어와도 괜찮도록 제한을 두기 위해 자바 빈 검증기(java bean validation)를 사용한다.
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation' //추가
@Getter
@ConfigurationProperties("my.datasource")
@Validated
public class MyDataSourcePropertiesV3 {
@NotEmpty
private String url;
@NotEmpty
private String username;
@NotEmpty
private String password;
private Etc etc;
위와 같이 @Validated애노테이션을 주고 @NotEmpty같은 애노테이션을 필드에서 이용할 수 있다.
보통 application.properties보다 application.yml을 많이 사용한다. 실무에서는 설정 값이 복잡하기 때문에 .yml이 주로 이용된다. 차이를 직접 관찰해보자.
my:
datasource:
url: local.db.com
username: username
password: password
etc:
max-connection: 10
options: CACHE,ADMIN
timeout: 60s
---
spring:
config:
activate:
on-profile: dev
my:
datasource:
url: dev.db.com
username: dev_user
password: dev_pw
etc:
max-connection: 100
options: DEV,ADMIN
timeout: 60s
위와 같이 계층으로 구분하기 때문에 개발자가 시각적으로 설정에 대한 구조를 더 쉽게 파악할 수 있다.
설정을 읽는데에 있어 계층만 잘 맞추어준다면 properties와 바꿔치기만 해주면 되기 때문에 다른 조작은 필요없다.
다만 properties와 같이 사용하진 않도록하자.(우선순위는 properties에 있지만 두개의 형식을 같이 사용해서 헷갈릴 여지를 줄 이유가 없다.)
환경마다 설정을 다르게 잡는 것을 더해 환경마다 등록될 빈을 결정하는 것도 @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();
}
}
개발단계에서 결제 기능을 확인하기 위해 가짜 결제 메서드를 만들어 사용할 경우 위와 같이 빈등록을 한다면 프로필 외부설정에 아무 값도 주지 않을 경우 LocalPayClient가 빈 등록될 것이고 prod(배포)의 경우 ProdPayClient가 등록될 것이다.
이를 통해 개발용 jar, 배포용 jar로 분리되지 않고 하나의 jar에서 설정 값을 통해 만들어질 초기화 프로세스가 달라지도록 설계할 수 있다.