-
Spring Boot 핵심 기능 4 - 외부설정(2)BackEnd/Spring Boot 2023. 5. 4. 07:00반응형
이전 포스팅에서 다루었던 커맨드 라인 옵션 인수, 자바 시스템 속성, 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(); } }
[참고정보]
반응형'BackEnd > Spring Boot' 카테고리의 다른 글
Spring Boot 핵심 기능 5 - 모니터링 & 관리 (Micrometer) (0) 2023.05.12 Spring Boot 핵심 기능 5 - 모니터링 & 관리 (Actuator) (0) 2023.05.04 Spring Boot 핵심 기능 4 - 외부설정(1) (0) 2023.05.03 Spring Boot 핵심 기능 3 - 자동 구성(Auto Configuration) (0) 2023.04.20 Spring Boot 핵심 기능 2 - 라이브러리 관리 (0) 2023.04.19