Goal
이번 포스팅에서는 Spring, Spring boot 기반 애플리케이션에서 `PropertySourcesPlaceholderConfigurer`를 기반으로 Property 파일을 설정하는 것의 문제점을 살펴보고, 해당 문제점을 해결할 수 있는 다른 방법에 대해 설명한다.
Problem
기존에 필자가 관리하던 프로젝트에서는 `*.properties` 파일을 `PropertySourcesPlaceholderConfigurer` bean을 생성함으로써 설정하고 있었다. 이렇게만 설정해주어도 @Value로 Property 값을 바인딩하거나, Env에서 값을 사용하는 경우에 큰 이슈가 발생하지 않았다.
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(ConfigurableEnvironment env,
List<ResourcePropertySource> customPropertySources) {
var propertySources = environment.getPropertySources();
customPropertySources.forEach(resource -> propertySources.addFirst(resource)); //Env에 Setting
var placeholderConfigurer = new PropertySourcesPlaceholderConfigurer();
placeholderConfigurer.setPropertySources(env.getPropertySources()); //placeholderConfigurer에 Setting
return placeholderConfigurer;
}
하지만 Spring boot 애플리케이션에서 위의 설정 방법으로 `@ConditionalOnProperty` 어노테이션을 사용하는 경우 property 값을 제대로 인식하지 않는 이슈가 발생하였다. `@ConditionalOnProperty` 어노테이션에 대한 평가가 `PropertySourcesPlaceholderConfigurer` BeanFactoryPostProcessor가 실행되기 이전에 발생하기 때문에, 설정한 `*.properties` 파일에 있는 property 값들이 로드가 되지 않은 채로 평가가 되어 오동작하게 된 것이다.
Solution
그럼 어떻게 `@ConditionalOnProperty`가 평가되기 이전에 Property 설정을 할 수 있을까?
`@ConditionalOnProperty`보다 이전에 context에 property를 업로드할 수 있는 방법은 2가지가 있다.
첫번째, `EnvironmentPostProcessor`를 구현한다. (Spring boot 애플리케이션에서만 사용 가능)
해당 interface를 아래와 같이 구현하고, `/resources/META-INF/spring.factories` 파일에 구현된 클래스의 경로를 입력해주면 `EnvironmentPostProcessor`가 `SpringApplication`의 prepareEnvironment 단계에서 로드된다.
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor {
//customPropertySources 구하는 코드 생략
var propertySources = environment.getPropertySources();
customPropertySources.forEach(resource -> propertySources.addFirst(resource));
}
#/resources/META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor={classpath}
두번째, `ApplicationContextInitializer`를 구현한다. (Spring/Spring boot 애플리케이션 모두 사용 가능)
`EnvironmentPostProcessor`는 Spring boot에서만 지원하기 때문에 Spring 애플리케이션의 경우에는 아래와 같이 `ApplicationContextInitializer`를 구현하면 된다. `/resources/META-INF/spring.factories` 파일에 구현된 클래스의 경로를 입력해주면 `ApplicationContextInitializer`가 `SpringApplication`의 prepareContext 단계에서 로드된다.
public class CustomContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
var propertySources = configurableApplicationContext.getEnvironment().getPropertySources();
getCustomPropertySources().forEach(resource -> propertySources.addFirst(resource));
configurableApplicationContext.addBeanFactoryPostProcessor(getCustomPlaceholderConfigurer(propertySources));
}
private PropertySourcesPlaceholderConfigurer getCustomPlaceholderConfigurer(PropertySources propertySources) {
PropertySourcesPlaceholderConfigurer placeholderConfigurer = new PropertySourcesPlaceholderConfigurer();
placeholderConfigurer.setPropertySources(propertySources);
return placeholderConfigurer;
}
private List<ResourcePropertySource> getCustomPropertySources() {
//코드 생략
}
}
#/resources/META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer={classpath}
Spring boot가 아닌 경우에는 `ApplicationContextInitializer`를 application context에 직접 등록해주면 된다.
두 방법 모두 아래와 같이 context가 refresh 되기 전에 실행이 되기 때문에 두 interface를 구현해주면 context에 미리 property를 세팅할 수 있다.
public class SpringApplication {
//코드 생략
public ConfigurableApplicationContext run(String... args) {
//코드 생략
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments); // --> EnvironmentPostProcessor 실행
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner); // --> ApplicationContextInitializer 실행
this.refreshContext(context); // --> @ConditionalOnProperty 실행
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
//코드 생략
}
}
이번 포스팅에서는 `PropertySourcesPlaceholderConfigurer`로 Property 파일을 세팅했을 때 발생했던 이슈와, 어떻게 해당 이슈를 피할 수 있었는지에 대해 간단히 정리해보았다. 방법을 찾아보면서 Property를 세팅하기 위해 사용하는 `PropertySourcesPlaceholderConfigurer`를 `@OnConditionalProperty` 어노테이션과 함께 사용했을 때 흔히들 같은 이슈를 겪는다는 것을 알 수 있었다. 비슷한 이슈로 어려움을 겪는 분들께 조금이라도 도움이 되기를..
'3. 기술 공부 > Java (Spring, Spring Boot)' 카테고리의 다른 글
[Java Servlet] 3. Servlet 3.0, 3.1 그리고 Spring MVC (2) | 2021.07.16 |
---|---|
[Java Servlet] 2. Servlet과 Servlet Container (0) | 2021.07.16 |
[Java Servlet] 1. Sync? Async?, Blocking? Non-Blocking? (0) | 2021.07.16 |
[Effective Java 3E] 5. 제네릭 (Generic) (0) | 2021.03.03 |