DB의 정보라던지 JWT의 내용이라던지 소스코드 외부적으로 값을 세팅해 줄 필요가 있을 경우 /resources 디렉토리 아래 .properties
혹은 .yaml
에서 값을 읽어올 수 있다. 스프링 부트는 기본적으로 DB를 사용할 일이 있으면 application.yaml
에서 DB 연결을 위한 값을 자동으로 설정한다. 이처럼 프레임워크의 흐름에 필요한 값을 넣기 위해 사용하는 경우도 있고, 개발자가 자체적으로 값을 소스코드 외부적으로 받아오도록 할 때 이러한 Properties들을 설정할 수 있는 방법은 여러가지가 있다.
왜 Properties가 필요한가?
우선 소스코드 외부적으로 왜 값을 넣어줄 필요가 있을지 생각을 해보자.
웹 서비스를 만들 땐 DB의 정보가 필요하다. 만약 팀원이 여러명 있다면, 각자의 컴퓨터에서 DB를 띄우고 그 정보를 이용해 각자의 컴퓨터에서 DB와 연결을 할 것이다. 같은 코드를 공유하더라도 DB의 정보가 소스코드 안에 들어있다면 팀원마다 소스코드를 변경하며 사용해야 한다. 소스코드의 변경을 피하기 위해 소스코드 외부에서 해당 정보를 받아오고, 환경마다 정보의 수정이 필요할 땐 Properties가 들어있는 외부 파일을 수정하면 된다. 환경마다 소스코드를 수정하던, properties파일을 수정하던 결국 수정해야하는 건데 왜 외부 파일을 수정해야할까?
실제로 웹 서비스를 서버에 배포를 하고 사용할 땐 소스코드 전체를 쓰는 것이 아닌 빌드 후 컴파일 된 코드만을 사용하고, 이를 압축한 jar파일을 통해 서비스를 실행한다. 즉, 소스코드를 변경할 수 없는 환경 또한 존재한다.
이를 해소하기 위해 외부에서 properties를 읽어 소스코드가 실행되거나 컴파일 될 때 값을 설정한다. 어떤 환경에서든 값을 외부에서 받아오니 소스코드의 변경 없이 환경에 의존하는 값을 다르게 사용할 수 있다. 소스코드의 변경을 최소화 하는 것은 중요하다!
.properties
vs .yaml
필요한 값을 .properties
를 이용해 주입하려면 아래와 같이 .properties
에 정의해야한다.
file.resources.path=src/main/resources/
file.resources.build-path=/resources/main/
jwt.secret-key=secret-key
jwt.expiration-minutes=15
file.resources나 jwt같이 중복되는 부분이 있더라도 다 작성해야하는 불편함이 있다.
이는 yaml을 이용해서 더욱 보기 쉽게 정의할 수 있다.
file:
resources:
path: "src/main/resources/"
build-path: "/resources/main/"
jwt:
secret-key: secret-key
expiration-minutes: 15
이 같이 yaml파일을 이용하면 중복을 줄여 더욱 보기 쉽게 나타낼 수 있어 일반적으로 yaml을 이용한다.
스프링 부트는 snakeyaml 라이브러리를 이용해서 application.yaml
의 값을 자동으로 읽을 수 있다. 부트가 나오기 전엔, 이 같은 yaml이 지원되지 않아 properties를 이용하기도 했고, 혹은 yaml을 읽을 수 있는 PropertySourceFactory
를 직접 구현해서 사용할 수 있었다.
public class YamlPropertiesFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
var factoryBean = new YamlPropertiesFactoryBean();
factoryBean.setResources(resource.getResource());
var properties = factoryBean.getObject();
return new PropertiesPropertySource(resource.getResource().getFilename(), properties);
}
}
이렇게 PropertySource를 읽을 PropertySourceFactory를 구현하고, 해당 Factory 객체를 읽을 수 있게끔 Configuration 빈에 설정을 해주어야 했다.
@Configuration
@PropertySource(value = "application.yaml", factory = YamlPropertiesFactory.class)
public class AppConfig {
}
@PropertySource
를 이용해 어떤 properties 파일을 어떤 방식으로 읽을 것인지 구현을 해주어야 했다. 기본으로는 .properties
파일 만을 지원하기 때문에 이처럼 다른 파일 형식을 사용할 경우 구현이 필요했고, 너무나도 오래된 방식이니 이렇게까지만 짚고 넘어가며 yaml을 당연하게 사용하려한다.
컨텍스트에서 직접 Properties를 받아오기
위와 같이 yaml파일 안에 사용할 Properties를 정의하고 애플리케이션을 실행할 때, 기본적으로 컨텍스트에서 직접 추출할 수도 있다.
@SpringBootApplication
public class PropertiesApplication {
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(PropertiesApplication.class, args);
String secretKey = applicationContext.getEnvironment()
.getProperty("jwt.secret-key");
String expirationMinutes = applicationContext.getEnvironment()
.getProperty("jwt.expiration-minutes");
System.out.println("Secret Key: " + secretKey);
System.out.println("Expiration Minutes: " + expirationMinutes);
}
}
콘솔:
Secret Key: secret-key
Expiration Minutes: 15
얼마나 투박한 방법인가! 애플리케이션이 굉장히 간단하거나 학습 목표라면 상관없겠지만 다른 방법을 고려하는 것이 바람직하다.
@Value
사용
별개의 AppConfig
라는 Configuration 빈을 만들고 스프링 프레임워크에서 제공하는 @Value
애너테이션을 사용해 Properties를 설정할 수 있다.
@Configuration
public class AppConfig {
@Value("${jwt.secret-key}")
private String secretKey;
@Value("${jwt.expiration-minutes}")
private String expirationMinutes;
@PostConstruct
private void print() {
System.out.println("Secret Key: " + secretKey);
System.out.println("Expiration Minutes: " + expirationMinutes);
}
}
값이 잘 넣어졌는지 확인하기 위해 빈이 생성되고 실행하는 @PostConstruct
애너테이션을 활용해 결과를 확인해봤다.
콘솔:
Secret Key: secret-key
Expiration Minutes: 15
의도대로 출력되는 것을 확인할 수 있었다.
Properties 불변으로 설정
Properties는 한 번 주입되면 값이 변경되면 안되기에 불변이라는 것을 명시하고 싶다. 현재의 AppConfig
에서 필드에 final
키워드를 붙이고 생성자를 통해 주입하면 될 것 같지만,
Could not autowire. No beans of 'String' type found.
빈을 찾을 수 없다는 문구가 나온다.
그래도 생성자를 이용해 불변 필드로 개선할 수 있었다.
@Configuration
public class AppConfig {
private final String secretKey;
private final String expirationMinutes;
public AppConfig(@Value("${jwt.secret-key}") String secretKey,
@Value("${jwt.expiration-minutes}") String expirationMinutes) {
this.secretKey = secretKey;
this.expirationMinutes = expirationMinutes;
}
@PostConstruct
private void print() {
System.out.println("Secret Key: " + secretKey);
System.out.println("Expiration Minutes: " + expirationMinutes);
}
}
콘솔:
Secret Key: secret-key
Expiration Minutes: 15
여기까지도 충분히 만족스러운 결과를 얻을 수 있었다.
아쉬운 점이 남았다. properties 파일이 아닌 yaml 파일이 더 선호되는 이유는 중복을 줄이고 계층으로 나타내 보기 쉽다는 것이 주된 이유이다. yaml파일을 이용해 properties 자체를 보기 쉽게 해놓았지만, 막상 사용되는 코드에선 properties 파일처럼 사용하고 있다. 거기다 문자열로 딱딱하게 주입하고 있으니, 아쉬운 마음을 지울 수가 없다. 이를 개선할 수 있는 다른 방법을 모색해봤다.
@ConfigurationProperties
사용
@ConfigurationProperties
를 사용해서 Properties 또한 문자열을 넣지 않고 객체처럼 사용할 수가 있었다.
package org.springframework.boot.context.properties;
...
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {
/**
* The prefix of the properties that are valid to bind to this object. Synonym for
* {@link #prefix()}. A valid prefix is defined by one or more words separated with
* dots (e.g. {@code "acme.system.feature"}).
* @return the prefix of the properties to bind
*/
@AliasFor("prefix")
String value() default "";
/**
* The prefix of the properties that are valid to bind to this object. Synonym for
* {@link #value()}. A valid prefix is defined by one or more words separated with
* dots (e.g. {@code "acme.system.feature"}).
* @return the prefix of the properties to bind
*/
@AliasFor("value")
String prefix() default "";
/**
* Flag to indicate that when binding to this object invalid fields should be ignored.
* Invalid means invalid according to the binder that is used, and usually this means
* fields of the wrong type (or that cannot be coerced into the correct type).
* @return the flag value (default false)
*/
boolean ignoreInvalidFields() default false;
/**
* Flag to indicate that when binding to this object unknown fields should be ignored.
* An unknown field could be a sign of a mistake in the Properties.
* @return the flag value (default true)
*/
boolean ignoreUnknownFields() default true;
}
prefix를 지정해 해당 prefix 계층의 값들만 자동으로 매핑시켜 객체를 생성할 수 있게 해주는 애너테이션이다. 하지만 주의해야할 점이 있다. 제공되는 package를 보면 알겠지만, Spring이 아닌 Spring Boot가 제공하는 애너테이션이다. Spring Boot가 아닌 애플리케이션이라면 사용하기가 힘들고, @ConfigurationProperties
로 Properties를 객체에 매핑시켜주기 위해선 스프링이 컴포넌트를 스캔할 때 필요한 @ComponentScan
처럼 스캔을 해주는 존재가 필요하다. 컴포넌트의 경우, 스프링 부트가 @SpringBootApplication
을 통해 기본적으로 소스 디렉토리 아래의 컴포넌트들을 스캔하지만, Properties는 그렇지 않다. 우리가 직접 스캔을 하도록 필요한 애너테이션을 붙여주어야 한다.
@ConfigurationPropertiesScan
컴포넌트 스캔처럼 @ConfigurationProperties
를 스캔할 수 있는 애너테이션이 @ConfigurationPropertiesScan
이다. 컴포넌트 스캔처럼 해당 애너테이션이 붙은 클래스의 디렉토리를 기준으로 스캔을 하니 @SpringBootApplication
이 붙어있는 애플리케이션 엔트리에 붙여주거나, Configuration을 모아놓은 디렉토리 아래의 클래스에 붙여주는 것이 좋다.
@Configuration
@ConfigurationPropertiesScan
public class AppConfig {
}
@ConfigurationProperties("jwt")
public class JwtConfig {
private String secretKey;
private String expirationMinutes;
@PostConstruct
private void print() {
System.out.println("Secret Key: " + secretKey);
System.out.println("Expiration Minutes: " + expirationMinutes);
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public void setExpirationMinutes(String expirationMinutes) {
this.expirationMinutes = expirationMinutes;
}
}
기존의 AppConfig
의 내용을 같은 디렉터리에 클래스를 생성해 옮기고 AppConfig
는 Properties를 스캔하도록 애너테이션을 붙여주었다.
이제 문자열로 딱딱하게 주입하지 않아도 카멜 케이스로 선언된 필드를 자동으로 찾아서 주입한다. 하지만 보다시피 setter 메서드를 통해 주입하기 때문에 setter 메서드가 필수이다. 하드코딩에서 벗어나게 되었고 yaml에 정의된 계층을 더욱 객체스럽게 사용할 수 있게 되었지만 불변은 깨지게 되었다.
오늘은 이만하고, 다음 포스트에서 @ConfigurationProperties
를 사용할 때의 불변성을 보장하기 위한 방법을 다루려한다!
https://jtechtalk.tistory.com/11
References
https://www.baeldung.com/configuration-properties-in-spring-boot
'Spring > Core(Boot)' 카테고리의 다른 글
객체지향스러운 Properties 관리 (1) | 2024.01.24 |
---|