Spring 의 몇몇 값들을 properties 를 이용해서 가져오는 방법은 상당히 유용하다. 추가 배포 없이 properties 만 동적으로 reload 시킬 수도 있고, 중요한 key 들을 github secret 을 이용해 관리할 수도 있기 때문이다.
Spring 에 상당히 멋진 기능이 포함되었는데 의외로 널리 알려지지 않은것 같은데다 한번 제대로 정리할 필요가 있다고 생각하여 이번에 사용법을 정리 하고자한다.
개요(필요한 것만 보셔도 됩니다)
소개
Spring boot 는 configuration 과 몇몇 속성을 *.properties 혹은 *.yaml 에서 쉽게 가져오는 방법을 제공하기 시작했다.
application.properties 나 application.yml 의 속성을 가져올 때 기존엔 다음과 같이 사용했을 것이다
@Value( "${jdbc.url}" )
private String jdbcUrl;
Java
복사
하지만, 이젠 이를 더 객체지향적으로 다룰 수 있으며, 덕분에 테스트에 사용하기도 편리해졌다.
Setup
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/>
</parent>
XML
복사
먼저 이와 같은 설정을 gradle 에 추가해야 한다. 이 기능은 2.2.0 부터 제공되었다.
Property를 정의해보자
Official document 에선 configuration properties 를 POJO를 통해 분리하는 것을 권장한다.
다음의 propertes 가 있다고 가정하자
mail:
hostname: host@mail.com
port: 9000
mail: mailer@mail.com
YAML
복사
예시를 위해 사용할 application.yml
이 속성을 다음과 같이 가져와서 사용할 수 있다.
@Configuration
@ConfigurationProperties(prefix = "mail")
public class ConfigProperties {
private String hostName;
private int port;
private String from;
// getter 와 setter 들 정의
}
Java
복사
•
여기에서 @Configuration 을 사용하는 이유는 Spring 에서 이 pojo 를 bean으로 만들기 위해서 이다.
•
@ConfigurationProperties 는 동일한 prefix 를 갖는 hierarchical properties 를 다루기에 최고이다.
◦
yaml 에서 사용하면 아주 달달하다.
Properties 는 camel? snake? 무엇을 사용해야 할까?
mail.hostName
mail.hostname
mail.host_name
mail.host-name
mail.HOST_NAME
YAML
복사
위의 어느 경우라도 private String hostName 과 binding 된다.
Debugging 하면서 발견한것은 무엇을 사용하든 Spring 내부에서 host-name 을 사용하는 듯 하다.
Spring Boot 2.2
스프링 2.2 버젼부터 @ConfigurationProperties 가 달려있는 class 는 @EnableConfigurationProperties 또는 @Component 대신 @ConfigurationPropertiesScan 을 통해 class path 를 찾을 수 있다.
그러므로 @Component 나 @Configruation 과 같은 annotation 을 붙이지 않아도 되고 @EnabledConfigurationProperties 또 사용할 필요가 없다.
주의!
원문에선 @Component @EnabledConfigruationProperties 등을 사용하지 말라고 나오지만, 이들을 사용하지 않고 @ConfigurationPropertiesScan 만 사용하면 compile error 가 발생된다.
원인은 후에 찾아서 달아두도록 하겠다.
@ConfigurationProperties(prefix = "mail")
@ConfigurationPropertiesScan
public class ConfigProperties {
private String hostName;
private int port;
private String from;
}
Java
복사
@Configuration 대신 @ConfigurationPropertiesScan 를 붙였다.
Class path scanner 는 @SpringBootApplication 으로 활성화 되어 @Component 가 없더라도 ConfigProperties 를 찾는다.
추가로 @ConfigurationPropertiesScan 의 path 를 변경하여 다른 location을 참조할 수 있다.
@SpringBootApplication
@ConfigurationPropertiesScan("com.baeldung.configurationproperties")
public class EnableConfigurationDemoApplication {
public static void main(String[] args) {
SpringApplication.run(EnableConfigurationDemoApplication.class, args);
}
}
Java
복사
@ConfigurationPropertiesScan("me.yevgnenll.configurationproperties") 로 다른 위치를 나타낸다.
위와 같이 명시하면 me.yevgnenll.proeprties 패키지를 참조한다.
중첩 properties
그런데 properties class 들도 객체지향적으로 사용하고 싶을 수 있으며, List, Map, 다른 class 등 더 명시적인 자료구조를 사용할 수도 있다.
다음의 새로운 class 가 있다고 가정하자
public class Credentials {
private String authMethod;
private String username;
private String password;
// getter, setter
}
Java
복사
ConfigProperties 에 다음과 같이 정의한다.
public class ConfigProperties {
private String host;
private int port;
private String from;
private List<String> defaultRecipients;
private Map<String, String> additionalHeaders;
private Credentials credentials;
// getter, setter
}
Java
복사
이 코드가 참조하는 properties 는 다음과 같다.
#Simple properties
mail:
hostname: mailer@mail.com
port: 9000
from: mailer@mail.com
#List properties
defaultRecipients[0]: admin@mail.com
defaultRecipients[1]: owner@mail.com
#Map Properties
additionalHeaders:
redelivery: true
secure: true
#Object properties
credentials:
username: john
password: password
authMethod: SHA1
YAML
복사
위 클래스와 binding 할 properties
@ConfigurationProperties 와 @Bean 사용하기
@ConfigurationProperties 와 @Bean 이 붙은 method 를 함께 사용할 수 있다.
이는 third-party 와 같이 우리의 통제를 벗어난 properties 를 binding 할 때 유용하다.
public class Item {
private String name;
private int size;
// getters, setters
}
Java
복사
@Configuration
public class ConfigProperties {
@Bean
@ConfigurationProperties(prefix = "item")
public Item item() {
return new Item();
}
}
Java
복사
item- prefix 인 property 는 item() 에 binding 되어 item instance 를 생성한다.
Property validation
@ConfigurationProperties 는 JSR-30 format 에 해당하는 validation 기능을 제공한다.
@NotBlank
private String hostName;
@Length(max = 4, min = 1)
private String authMethod;
@Min(1025)
@Max(65536)
private int port;
@Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
private String from;
Java
복사
이와 같이 validation 을 정의해두면 별도의 valid 함수를 정의할 필요가 없다.
또한 validation 을 만족하지 못한다면 IllegalStateException 을 throw 한다.
Property 변경하기
@ConfigurationProperties 는 여러가지 다양한 type 으로 변환되는 기능을 제공한다.
Duration
다음과 같이 두개의 Duration 필드가 있다.
@ConfigurationProperties(prefix = "conversion")
public class PropertyConversion {
private Duration timeInDefaultUnit;
private Duration timeInNano;
...
}
Java
복사
단위 | properties 표현 |
nanoseconds | ns |
microseconds | us |
milliseconds | ms |
secodns | s |
minutes | m |
hours | h |
days | d |
conversion:
timeInDefaultUnit: 10
timeInNano: 9ns
YAML
복사
timeInDefaultUnit 는 10miliseconds 로 정의되고, timeInNano 는 nano seconds 로 정의된다.
각 숫자와 단위는 표와 같다.
여기서 아무 단위를 입력하지 않는다면 default 로 milliseconds 가 적용된다.
또한, 다음과 같이 override 하여 Duration 을 정의할 수 있다.
@DurationUnit(ChronoUnit.DAYS)
private Duration timeInDays;
Java
복사
conversion:
timeInDays: 2
YAML
복사
DataSize
이건 몰랐는데 data size 역시 정의가 가능하다.
private DataSize sizeInDefaultUnit;
private DataSize sizeInGB;
@DataSizeUnit(DataUnit.TERABYTES)
private DataSize sizeInTB;
Java
복사
단위 | 표현 |
Bytes | B |
KilloBytes | KB |
MegaBytes | MB |
GigaBytes | GB |
TeraBytes | TB |
conversion:
sizeInDefaultUnit: 300
sizeInGB: 2GB
sizeInTB: 4
YAML
복사
여기도 default 단위가 존재하는데 bytes 가 된다.
역시 @DataSizeUnit 을 사용해 적절한 단위로 override 가 가능하다.
Custom Converter
이 항목이 내가 이 글을 작성한 원인이다.
public class Employee {
private String name;
private double salary;
}
Java
복사
conversion.employee: john,2000
YAML
복사
이와 같이 proeprties 를 정의하면 다음으로 conversion 된다.
private Employee employee;
Java
복사
그런데 여기서 Converter interface 를 정의하여 @ConfigurationPropertiesBinding 을 등록해줘야 의도한대로 값이 binding 된다.
@Component
@ConfigurationPropertiesBinding
public class EmployeeConverter implements Converter<String, Employee> {
@Override
public Employee convert(String from) {
String[] data = from.split(",");
return new Employee(data[0], Double.parseDouble(data[1]));
}
}
Java
복사
Immutable @ConfigruationProperties binding 하기
이 기능은 java 보다도 kotlin 에서 더 많이 사용했던 것 같다.
외부 API를 연동하기 위해 @ConfigruationProperties 를 사용할 경우, 혹은 어떠한 key, secret 을 사용할 땐 딱히 값이 변경되지 않는 경우가 상당하다.
@ConfigurationProperties(prefix = "mail.credentials")
@ConstructorBinding
public class ImmutableCredentials {
private final String authMethod;
private final String username;
private final String password;
public ImmutableCredentials(String authMethod, String username, String password) {
this.authMethod = authMethod;
this.username = username;
this.password = password;
}
public String getAuthMethod() {
return authMethod;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
Java
복사
여기서 주의해야 할 점은 위에서 볼 수 있듯이 constructor 를 정의하여 모든 parameter 를 명시해야 한다.
또한, 모든 field 는 final 을 명시하여 불변으로 정의했다.
따라서 setter 를 구현 할 필요가 없다.
그런데 이 immutable properties 를 사용하기 위해선 강조해야 하는 부분이 있는데
@EnableConfigurationProperties 혹은 @ConfigurationPropertiesScan 을 반드시 함께 사용해야 한다.
Java 14의 records
Java 14 에선 records 라 하는 class 가 추가되었다.
이에 대해선 따로 자세히 다룰 예정이며, 여기선 개념이나 용도 목적등을 따로 언급하지 않는다.
이 Records 클래스는 immutable 을 다루는데 아주 좋으며, DTO 로 사용하기에 알맞다고 한다.
@ConstructorBinding
@ConfigurationProperties(prefix = "mail.credentials")
public record ImmutableCredentials(
String authMethod, String username, String password) {
}
Java
복사
따로 필드가 없음
여기선 getter 도 setter 도 필요하지 않다.
Spring boot 2.6 부터 존재하며, single-constructor 이면 @ContructorBinding 을 누락할 수 있다.
하지만, multiple constructor 이면 @ContructorBinding 가 반드시 필요하다.
하지만, @ContructorBinding 를 사용한다는 의미는 property binding 임을 나타내는데 사용하므로 여전히 붙여둔다고 한다.
결론
이 글에서 @ConfigurationProperties 와 Nested, bean validation 을 위한 기능을 살펴보았다.
이 글의 원본은 https://www.baeldung.com/configuration-properties-in-spring-boot 이며 이를 번역하면서 약간 내 입맛대로 수정했다.
더 많은 개발자들이 @Value 의 감옥에서 벗어나 더 자유롭게 properties 를 구성하며 이를 통해 OOP를 강화하고, test code 가 더 유연해졌으면 좋겠다.
Properties class 를 정의하면서 가장 편한 것은 test code 를 작성하면 다루기가 너무 쉬워진다는 것이다.