springboot-external-read 프로젝을 새로 만들었습니다.
다양한 외부 설정 읽기
스프링은 Environment
는 물론이고 Environment
를 활용해서 더 편리하게 외부 설정을 읽는 방법들을 제공한다.
스프링이 지원하는 다양한 외부 설정 조회 방법
Environment
@Value
- 값 주입@ConfigurationProperties
- 타입 안전한 설정 속성예제
MyDataSource
package hello.datasource;
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);
}
}
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
properties
캐밥 표기법
properties
는 자바의 낙타 표기법(maxConnection
)이 아니라 소문자와 - (dash)를 사용하는 캐밥 표기법(max-connection
)을 주로 사용한다.
MyDataSourceEnvConfig
package hello.config;
import hello.datasource.MyDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import java.time.Duration;
import java.util.List;
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); // List 변환 [A,B]
return new MyDataSource(url, username, password, maxConnection, timeout, options);
}
}
MyDataSource
에 값을 설정하고 스프링 빈으로 등록Environment
를 사용하면 외부 설정의 종류와 관계없이 코드 안에서 일관성 있게 외부 설정을 조회할 수 있다.Environment.getProperty(key, Type)
를 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다. ExternalReadApplication
package hello;
import hello.config.MyDataSourceEnvConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@Import(MyDataSourceEnvConfig.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalReadApplication.class, args);
}
}
@SpringBootApplication(scanBasePackages = "hello.datasource")
@Import
로 설정 정보를 계속 바꿀 것이기 때문에 설정 정보를 바꾸면서 사용하기 위해 hello.config
의 위치를 피해서 컴포넌트 스캔 위치를 설정했다.실행 결과
단점
Environment
를 직접 주입받고, env.getProperty(key)
를 통해서 값을 꺼내는 과정을 반복해야 한다는 점이다.
@Value
를 사용하면 외부 설정값을 편리하게 주입받을 수 있다.
(@Value
도 내부에서는 Environment
를 사용한다.)
MyDataSourceValueConfig
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 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:10}") 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:10}")
: key
가 없는 경우 10을 사용한다.
@Import 변경 후 실행
my.datasource.etc.max-connection 주석처리
단점
@Value
를 사용하는 방식도 좋지만, @Value 로 하나하나 외부 설정 정보의 키 값을 입력받고, 주입 받아와야 하는 부분이 번거롭다.스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다. 이것을 타입 안전한 설정 속성(Type-safe Configuration Properties)이라 한다.
MyDataSourcePropertiesV1
package hello.config;
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 = new Etc();
@Data
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
}
}
@ConfigurationProperties
이 있으면 외부 설정을 주입 받는 객체라는 뜻이다. 여기에 외부 설정 KEY
의 묶음 시작점인 my.datasource
를 적어준다.설정 속성을 실제 어떻게 사용하는지 확인해보자.
MyDataSourceConfigV1
package hello.config;
import hello.datasource.MyDataSource;
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(MyDataSourcePropertiesV1.class)
@ConfigurationProperties
를 지정해주어야 한다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있다.private final MyDataSourcePropertiesV1 properties
설정 속성을 생성자를 통해 주입 받아서@Import 바꾸고 실행
타입 안전
maxConnection=kwonyongho
로 입력하고 실행
숫자를 입력하는 곳에 문자를 입력하는 문제를 방지해준다. 그래서 타입 안전한 설정 속성이라고 한다.
application.properties
에 필요한 외부 설정을 추가하고, @ConfigurationProperties
를 통해서 MyDataSourcePropertiesV1
에 외부 설정의 값들을 설정했다. 그리고 해당 값들을 읽어서 MyDataSource
를 만들었다.
문제
MyDataSourcePropertiesV1
은 스프링 빈으로 등록된다. 그런데 Setter
를 가지고 있기 때문에 누군가 실수로 값을 변경하는 문제가 발생할 수 있다. 여기에 있는 값들은 외부 설정값을 사용해서 초기에만 설정되고, 이후에는 변경하면 안된다. Setter
를 제거하고 대신에 생성자를 사용하면 중간에 데이터를 변경하는 실수를 근본적으로 방지할 수 있다.@ConfigurationProperties
는 Getter, Setter
를 사용하는 자바빈 프로퍼티 방식이 아니라 생성자를 통해서 객체를 만드는 기능도 지원한다.
MyDataSourcePropertiesV2
package hello.config;
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.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;
}
}
}
@Getter
롬복이 자동으로 getter
를 만들어준다.@DefaultValue
: 해당 값을 찾을 수 없는 경우 기본값을 사용한다.@DefaultValue Etc etc
etc
를 찾을 수 없을 경우 Etc
객체를 생성하고 내부에 들어가는 값은 비워둔다. (null, 0 )
@DefaultValue("DEFAULT") List<String> options
options
를 찾을 수 없을 경우 DEFAULT
라는 이름의 값을 사용한다.MyDataSourceConfigV2
package hello.config;
import hello.datasource.MyDataSource;
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());
}
}
MyDataSourcePropertiesV2
를 적용하고 빈을 등록@Import 변경 후 실행
@ConfigurationProperties
은 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원한다.spring-boot-starter-validation
이 필요하다.MyDataSourcePropertiesV3
package hello.config;
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.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, 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초를 허용한다.@Import 변경 후 실행
maxConnection=0 설정, timeout=100 설정
ConfigurationProperties 장점
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"
space
(공백)로 계층 구조를 만든다. space
는 1칸을 사용해도 되는데, 보통 2칸을 사용한다.:
를 사용한다. 만약 값이 있다면 이렇게 key:
value :
이후에 공백을 하나 넣고 값을 넣어주면 된다.properties -> yml 바꾸기(application.properteis -> application_backup.properties로 변경)
application.properties , application.yml
을 같이 사용하면application.properties
가
우선권을 가진다.- 이것을 둘이 함께 사용하는 것은 일관성이 없으므로 권장하지 않는다.
- 참고로 실무에서는 설정 정보가 많아서 보기 편한 yml 을 선호한다.
application.yml
my:
datasource:
url: local.db.com
username: local_user
password: local_pw
etc:
max-connection: 1
timeout: 60s
options: LOCAL, CACHE
정상작동 하는 것을 확인 할 수 있다.
프로필과 외부 설정을 사용해서 각 환경마다 설정값을 다르게 적용하는 것은 이해했다.
그런데 설정값이 다른 정도가 아니라 각 환경마다 서로 다른 빈을 등록해야 한다면 어떻게 해야할까?
application.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:
maxConnection: 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:
maxConnection: 50
timeout: 10s
options: PROD, CACHE
결제 기능 추가
간단히 LocalPayClient
와 ProdPayClinet
를 구현하였고 log로 남길 수 있게 하였다.
PayConfig
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();
}
}
@Profile
애노테이션을 사용하면 해당 프로필이 활성화된 경우에만 빈을 등록한다.default
프로필(기본값)이 활성화 되어 있으면 LocalPayClient
를 빈으로 등록한다.prod
프로필이 활성화 되어 있으면 ProdPayClient
를 빈으로 등록한다OrderRunner
package hello.pay;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class OrderRunner implements ApplicationRunner {
private final OrderService orderService;
@Override
public void run(ApplicationArguments args) throws Exception {
orderService.order(1000);
}
}
ApplicationRunner
인터페이스를 사용하면 스프링은 빈 초기화가 모두 끝나고 애플리케이션 로딩이
완료되는 시점에 run(args) 메서드를 호출해준다.
프로필 없이 실행
prod 프로필 실행
--spring.profiles.active=prod
프로필 활성화 적용
@Profile
@Profile
은 특정 조건에 따라서 해당 빈을 등록할지 말지 선택한다. 어디서 많이 본 것 같지 않은가? 바로 @Conditional
이다. 코드를 보면 @Conditional(ProfileCondition.class)
를 확인할 수 있다.