[Spring legacy] @Value("${~}")가 왜 프로퍼티 파일를 못 읽을까?

식빵·2021년 12월 30일
2

Spring Lab

목록 보기
6/35
post-thumbnail

설명의 Spring Legacy라는 표현은 Spring boot 환경이 아닌 프로젝트를 의미합니다.


시작하기 앞서

나는 지금 Spring Legacy 프로젝트에서 작업 중이며

  • Root Application Context
  • Dispatcher Servlet Application Context

각각 xml 설정 파일이 나뉘어져 있다.
이게 핵심적인 원인을 제공하기 때문에 미리 말하는 것이다.



문제 발생

작업 도중 @Controller에서 API를 호출하는 부분이 있는데,
이때 *.properties 파일에서 API Key 값을 읽어와야 했다.

스프링이 제공하는 @Value를 사용해서 아래처럼 읽어오려고 했다.

@Controller
@RequestMapping("/somewhere/you/dont/know")
public class BlaBlaController {
	
    @Value("${api.key}")
    private String apiKey;
    
    // 생략...
}

그런데 뭔가 이상하다.
디버거로 찍어서 apiKey 값을 확인하면 아래와 같다.

apiKey = "${api.key}"; // resolve 가 안되고 "${~}" 문자열 그대로 준다;;


혹시나 해서 ROOT application context에서 사용하는 xml 설정 파일에 propertyConfigurer 설정을 봤다.

<bean id="propertyConfigurer"
      class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
          <value>classpath:/work/${spring.profiles.active}.properties</value>
        </list>
    </property>
</bean>




문제 파악

후우... 일단 공식 문서와 API 정보를 찾아다녔다.

일단 현재 상태는 "${~}" 가 resolve 가 되지는 않지만 private String 필드에 정상적으로 입력되는 상태이다.

그렇다면 나눠서 생각해보자.

  • @Value 를 필드의 값에 매핑해주는 존재는 누구?
  • @Value("${api.key}") 에 있는 "${~}"를 resolve 해주는 존재는 누구?

AutowiredAnnotationBeanPostProcessor

@Value 를 필드의 값에 매핑해주는 존재이다.
이 클래스는 BeanPostProcessor(앞으로는 BPP라고 부르겠다) 인터페이스를 구현하는 bean으로 bean@Autowired 또는 @Value 애노테이션이 있다면 처리를 해준다.

그래서 지금은 비록 "${~}" 에 대한 resolve 는 해결을 못했지만 String 필드에
그 문자열 "${~}" 자체가 잘 들어가는 것을 확인할 수 있다.

일단 이 녀석 문제는 아니라는 것을 알 수 있다.


PropertyPlaceholderConfigurer

@Value("${api.key}") 에 있는 "${~}"를 resolve 해주는 존재이다.

이 클래스는 BeanFactoryPostProcessor(앞으로 BFPP 라고 부르겠다) 인터페이스를 구현한 bean이다.
이 bean은 Application Context 가 Bean Definition 에 대한 정보를 모두 얻었을 때, Bean Factory 에 대한 참조를 얻어서 조작이 가능하도록 해준다.

이러한 BFPP 는 하나의 인터페이스 메소드를 제공한다.

void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException

설명: Modify the application context's internal bean factory after its standard initialization. All bean definitions will have been loaded, but no beans will have been instantiated yet. This allows for overriding or adding properties even to eager-initializing beans.

설명을 대충 보면 알겠지만 Application Context 가 내부적으로 갖는 bean factory의 초기화가 완료되고, 해당 bean factory 에 대한 참조를 받을 수 있다.
그리고 그 bean factory로 로딩된 bean definition의 정보를 조작할 수 있다.


참고
아래 그림은 PropertyPlaceholderConfigurer 클래스의 부모 클래스인 PropertyResourceConfigurer에 있는 postProcessBeanFactory 메소드이다.

bean factory 로부터 프로퍼티에 대한 정보를 읽어서 뭔 작업을 해준다는 것을 직감할 수 있다.


원인 파악

분명 PropertyPlaceholderConfigurer 가 범인이라는 느낌이 강하다.
root application context 의 xml 파일 설정에 분명 선언을 했는데 왜 이럴까?

핵심적인 이유는 구글링을 하면서 알아냈다.
spring-framwork issues글의 말을 빌리면 아래와 같다.

Also, BeanFactoryPostProcessors are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanFactoryPostProcessor in one container, it will only be applied to the bean definitions in that container. Bean definitions in one container will not be post-processed by BeanFactoryPostProcessors in another container, even if both containers are part of the same hierarchy.

참고. BeanPostProcessors are scoped per-container...

하나의 BeanFactoryPostProcessors 는 자신이 속한 Application Context에만 영향을 준다는 것이 핵심이다.

나는 root application context에만 PropertyPlaceholderConfigurer 를 설정해주고, servlet application contextPropertyPlaceholderConfigurer를 설정하지 않아서 이런 일이 발생한 것이다.




문제 해결

Dispatcher Servlet Application Context의 xml 설정 파일에도 아래처럼 작성하면 된다.

<bean id="propertyConfigurer"
      class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            <value>classpath:/work/${spring.profiles.active}.properties</value>
        </list>
    </property>
</bean>

이러고 나서 다시 테스트하면 정상적으로 프로퍼티 value 값을 읽어온다.
참고로 간단하게 아래처럼 namespace를 사용해서 위 설정을 대체할 수 있다.

<context:property-placeholder
	location="classpath:/work/${spring.profiles.active}.properties"/>

context:property-placeholder 설명


참고로 context:property-placeholderPropertySourcesPlaceholderConfigurer
사용하고 내 xml 설정에서는 PropertyPlaceholderConfigurer를 하고 있다.

일단 미리 말해두지만 둘 다 사용해도 지금의 상황에서는 동일하게 작동한다!

둘의 차이가 궁금하다면 아래 보충 를 읽어보자.

참고로 공식 문서에서는 PropertySourcesPlaceholderConfigurerPropertyPlaceholderConfigurer를 대체하기 위해 만들어졌다고 하니, 특별한 경우가 아니라면 PropertySourcesPlaceholderConfigurer를 사용하자.😊



보충

1. org.springframework.context.support.PropertySourcesPlaceholderConfigurer

Specialization of PlaceholderConfigurerSupport that resolves ${...} placeholders within bean definition property values and @Value annotations against the current Spring Environment and its set of PropertySources.

This class is designed as a general replacement for PropertyPlaceholderConfigurer introduced in Spring 3.1. It is used by default to support the property-placeholder element in working against the spring-context-3.1 or higher XSD, whereas spring-context versions <= 3.0 default to PropertyPlaceholderConfigurer to ensure backward compatibility. See the spring-context XSD documentation for complete details.

Any local properties (e.g. those added via setProperties, setLocations et al.) are added as a PropertySource. Search precedence of local properties is based on the value of the localOverride property, which is by default false meaning that local properties are to be searched last, after all environment property sources.

See org.springframework.core.env.ConfigurableEnvironment and related javadocs for details on manipulating environment property sources.



2. org.springframework.beans.factory.config.PropertyPlaceholderConfigurer

PlaceholderConfigurerSupport subclass that resolves ${...} placeholders against local properties and/or system properties and environment variables.

As of Spring 3.1, PropertySourcesPlaceholderConfigurer should be used preferentially over this implementation; it is more flexible through taking advantage of the org.springframework.core.env.Environment and org.springframework.core.env.PropertySource mechanisms also made available in Spring 3.1.

PropertyPlaceholderConfigurer is still appropriate for use when:

  • the spring-context module is not available (i.e., one is using Spring's BeanFactory API as opposed to ApplicationContext).
  • existing configuration makes use of the "systemPropertiesMode" and/or "systemPropertiesModeName" properties. Users are encouraged to move away from using these settings, and rather configure property source search order through the container's Environment; however, exact preservation of functionality may be maintained by continuing to use PropertyPlaceholderConfigurer.




참고글

https://github.com/spring-projects/spring-framework/issues/13634

Also, BeanFactoryPostProcessors are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanFactoryPostProcessor in one container, it will only be applied to the bean definitions in that container. Bean definitions in one container will not be post-processed by BeanFactoryPostProcessors in another container, even if both containers are part of the same hierarchy.

https://stackoverflow.com/questions/24733058/spring-propertyplaceholderconfigurer-doesnt-inject-values

A final word on the placeholder support. This class is a BeanFactoryPostProcessor and it will process bean definitions and replace the placeholders in them. However it does this only for beans in the same application context!. If you defined this bean in the parent and expect it to replace placeholders in the child context then that isn't going to happen.

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

4개의 댓글

comment-user-thumbnail
2023년 11월 29일

이 글을 통해 저도 같은 문제를 해결했습니다! 감사합니다~

1개의 답글
comment-user-thumbnail
2024년 5월 19일

정말 감사합니다.

1개의 답글