Spring에서는 외부 설정을 읽기 위한 방법으로 아래와 같이 다양한 방법을 제공합니다.
- Environment
- @Value - 값 주입
- @ConfigurationProperties - 타입 안전한 설정 속성
이번 포스팅은 각 방법을 어떻게 사용하는지에 대해서 작성해 보았습니다.
application.properties
my.data.url=local.db.com
my.data.username=local_user
my.data.password=local_pw
my.data.etc.max-connection=1
my.data.etc.timeout=3500ms
my.data.etc.options=CACHE,ADMIN
위의 properties는 dash(-)를 사용하는 캐밥 표기법을 주로 사용합니다. (max-connection) 이렇게 사용된 표기법을 자바에서는 낙타 표기법(maxConnection)으로 변환하여 받아야 합니다.
Environment
MyData.class
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;
@Slf4j
public class MyData {
private final String url;
private final String username;
private final String password;
private final int maxConnection; // 낙타 표기법 (camel 표기법)
private final Duration timeout;
private final List<String> options;
public MyData(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);
}
}
config 클래스 (MyDataConfig.class)
import com.example.springboot.MyData;
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 MyDataConfig {
private final Environment env;
public MyDataConfig(Environment env) {
this.env = env;
}
@Bean
public MyData myData() {
String url = env.getProperty("my.data.url");
String username = env.getProperty("my.data.username");
String password = env.getProperty("my.data.password");
// Environment.getProperty(key, Type) --> Type으로 형 변환
int maxConnection = env.getProperty("my.data.etc.max-connection", Integer.class); // 문자 -> 숫자
Duration timeout = env.getProperty("my.data.etc.timeout", Duration.class); // 문자 -> 기간
List<String> options = env.getProperty("my.data.etc.options", List.class); // 문자 -> 리스트
return new MyData(url, username, password, maxConnection, timeout, options);
}
}
메인 함수를 실행하면, 아래 결과를 얻을 수 있습니다.
Environment를 사용할 경우
- application.properties에 필요한 외부 설정을 추가하고, Environment를 통해서 해당 값을 읽으면 향후 외부 설정 방식이 달라져도 애플리케이션 코드를 그대로 유지할 수 있다는 장점이 있습니다. (예를 들어, application.properties에서 커맨드 라인 옵션 인수 혹은 자바 시스템 속성으로 변경)
- Environment를 직접 주입받고, env.getProperty(key)를 통해서 값을 꺼내는 과정을 반복해야 한다는 단점이 있습니다. 이를 해결하기 위해 Spring은 @Value 방식을 제공합니다.
@Value
MyData.claass를 아래와 같이 @Value를 사용해 변경할 수 있습니다. (필드 주입)
- ${ }를 사용해서 외부 설정의 key 값을 주면 원하는 값을 주입받을 수 있습니다.
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import java.time.Duration;
import java.util.List;
@Slf4j
public class MyData {
@Value("${my.data.url}")
private String url;
@Value("${my.data.username}")
private String username;
@Value("${my.data.password}")
private String password;
@Value("${my.data.etc.max-connection}")
private int maxConnection;
@Value("${my.data.etc.timeout}")
private Duration timeout;
@Value("${my.data.etc.options}")
private List<String> 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);
}
}
MyDataConfig.class
import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class MyDataConfig {
@Bean
public MyData myData() {
return new MyData();
}
}
또한 아래와 같이 parameter에 설정 값을 주입받을 수도 있습니다.
MyData.class
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;
@Slf4j
public class MyData {
private String url;
private String username;
private String password;
private int maxConnection;
private Duration timeout;
private List<String> options;
public MyData(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);
}
}
MyDataConfig.class
import com.example.springboot.MyData;
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 MyDataConfig {
@Bean
public MyData myData(@Value("${my.data.url}") String url,
@Value("${my.data.username}") String username,
@Value("${my.data.password}") String password,
@Value("${my.data.etc.max-connection}") int maxConnection,
@Value("${my.data.etc.timeout}") Duration timeout,
@Value("${my.data.etc.options}") List<String> options) {
return new MyData(url, username,password,maxConnection,timeout,options);
}
}
기본값 설정
만약 key를 찾지 못할 경우 코드에서 기본값을 사용하고 싶다면 아래와 같이 : 뒤에 기본값을 적어주면 됩니다.
- @Value("${my.data.url:default.db.com}") : my.data.url 가 없을 경우, default.db.com을 사용합니다.
@Value를 사용할 경우
- Environment의 단점을 극복하였지만, @Value 또한 하나하나 외부 설정 정보의 키 값을 입력받아서 주입받아야 하는 부분이 번거롭습니다.
- 설정 파일을 보면 my.data 로 묶여 있는데 이런 부분을 객체로 변환해서 사용할 수 있다면 더 편리하고 좋을 것입니다. 이를 제공하는 기능이 @ConfigurationProperties입니다.
@ConfigurationProperties
type-safe(타입 안전)한 설정 속성이기 때문에, 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있고, 객체를 통해서 활용할 수 있는 부분들이 많습니다. 먼저 코드를 통해 @ConfigurationProperties를 사용하는 법을 확인해 보겠습니다.
MyDataProperties.class
해당 클래스에 application.properties의 속성값을 주입받습니다. 아래는 생성자를 생성하여 주입받았습니다. (생성자 대신, lombok의 @Setter를 사용해서도 주입이 가능합니다. 물론, 생성자를 직접 작성하는 대신 lombok의 @AllArgsConstructor를 사용할 수도 있습니다.)
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.List;
@Getter
@ConfigurationProperties("my.data") // 객체 묶음
public class MyDataProperties {
private String url;
private String username;
private String password;
private Etc etc;
public MyDataProperties(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;
}
}
}
- 외부 설정을 주입받을 객체 (MyDataProperties 클래스)를 생성합니다. 그리고 각 필드를 외부 설정의 key 값에 맞추어 준비합니다.
- @ConfigurationProperties가 있으면 외부 설정을 주입받는 객체라는 뜻입니다.
- 외부 설정 key의 묶음 시작점인 my.data를 지정합니다.
- @DefaultValue: 해당 값을 찾을 수 없는 경우 기본값을 사용합니다.
- @DefaultValue Etc etc : etc를 찾을 수 없을 경우, Etc 객체를 생성하고 내부 값은 비워둡니다. (null, 0)
- @DefaultValue("DEFAULT") List<String> options : options을 찾을 수 없을 경우 DEFAULT 값을 사용합니다.
MyDataConfig.class
import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(MyDataProperties.class) // 추가!
public class MyDataConfig {
private final MyDataProperties myDataProperties;
public MyDataConfig(MyDataProperties myDataProperties) {
this.myDataProperties = myDataProperties;
}
@Bean
public MyData myData() {
return new MyData(myDataProperties.getUrl(),
myDataProperties.getUsername(),
myDataProperties.getPassword(),
myDataProperties.getEtc().getMaxConnection(),
myDataProperties.getEtc().getTimeout(),
myDataProperties.getEtc().getOptions());
}
}
- @EnableConfigurationProperties를 이용해서 Spring에게 사용할 @ConfigurationProperties를 지정해주어야 합니다. 이렇게 지정함으로써, 해당 클래스는 Spring Bean으로 등록되고 필요한 곳에서 주입받아서 사용할 수 있습니다.
- private final MyDataProperties myDataProperties를 생성자를 통해 주입받아서 사용합니다.
MyData.class
MyData.class는 위에서 사용한 코드와 동일합니다.
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;
@Slf4j
public class MyData {
private String url;
private String username;
private String password;
private int maxConnection;
private Duration timeout;
private List<String> options;
public MyData(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);
}
}
실행 결과는 동일합니다. ConfigurationProperties는 type-safe 하다고 하였습니다. 만약, maxConnection을 문자열 (e.g. "abc")로 지정하면 실행하면 에러 메시지를 받을 수 있습니다. 즉, 타입이 다르면 오류가 발생하므로 외부 데이터의 타입에 대해서 믿고 사용할 수 있습니다.
@ConfigurationPropertiesScan
지금까지 @ConfigurationProperties를 등록할 때 @EnableConfigurationProperties에 해당 클래스를 직접 명시에서 사용하였습니다. @ConfigurationProperties 가 선언된 클래스를 하나하나 빈으로 지정할 경우 이렇게 직접 명시할 수도 있지만, @ConfigurationProperties를 특정 범위로 자동 등록하고 싶을 때는 @ConfigurationPropertiesScan을 사용하면 편리하게 Bean을 등록할 수 있습니다.
package com.example.springboot.config;
import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
// 주석 처리
// @EnableConfigurationProperties(MyDataProperties.class)
public class MyDataConfig {
private final MyDataProperties myDataProperties;
public MyDataConfig(MyDataProperties myDataProperties) {
this.myDataProperties = myDataProperties;
}
@Bean
public MyData myData() {
return new MyData(myDataProperties.getUrl(), myDataProperties.getUsername(),
myDataProperties.getPassword(),
myDataProperties.getEtc().getMaxConnection(),
myDataProperties.getEtc().getTimeout(),
myDataProperties.getEtc().getOptions());
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
// 해당 범위 안의 파일들 scan하여
// bean으로 등록
@ConfigurationPropertiesScan("com.example.springboot.config")
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
패키지 구조는 아래와 같습니다.
@ConfigurationProperties 검증
@ConfigurationProperties는 type-safe 하기 때문에, 숫자가 들어가야 하는 부분에 문자가 입력되는 문제와 같이 타입이 맞지 않는 데이터 입력의 문제는 예방할 수 있습니다. 하지만, 숫자의 범위라던가, 문자의 길이 같은 부분은 검증이 어렵습니다. 이를 개발자가 하나하나 검증 코드를 작성하는 방법 대신 Spring에서 자바 빈 검증기 (java bean validation) 기능을 제공합니다.
Java bean validation 사용하기
자바 빈 검증기를 사용하려면 build.gradle 파일에 아래의 dependency가 필요합니다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
검증기를 추가해서 MyDataProperties.class 코드를 수정해 보겠습니다.
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.data")
@Validated // 추가!
public class MyDataProperties {
@NotEmpty
private String url;
@NotEmpty
private String username;
@NotEmpty
private String password;
private Etc etc;
public MyDataProperties(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{
@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초를 허용합니다.
위에서 설정한 값 이외의 값이 외부 설정에 지정되어 있으면 애플리케이션 로딩 시점에 오류 메시지를 확인할 수 있습니다.
이렇게 ConfigurationProperties를 이용하면 외부 설정을 객체로 편리하게 변환해서 사용할 수 있고, 외부 설정의 계층을 객체로 묶어서 표현할 수 있으며, type-safe 및 검증기를 적용할 수 있다는 장점이 있습니다.
참고자료]
김영한, " 스프링 부트 - 핵심 원리와 활용", 인프런
'Spring Boot' 카테고리의 다른 글
서블릿(Servlet) 이란? (0) | 2024.01.01 |
---|---|
외부 파일(.properties, .yaml)로 설정하기 (1) | 2023.12.27 |
Spring Boot의 외부설정 (0) | 2023.12.25 |
Jar 파일의 빌드와 배포 (0) | 2023.12.16 |
나만의 Spring Boot로 Spring Boot 원리 파악하기 (0) | 2023.12.16 |