Spring 프레임워크로 @Value 어노테이션(Annotation)을 이용하여 프로퍼티 값을 필드에 인젝션(Injection)하기 위해서 PropertyPlaceHolder관련 설정을 추가하고 어플리케이션을 재기동하자 해당 설정 파일(@Configuration 어노테이션을 이용한 Java based config)에서 @PostConstruct 어노테이션을 지정한 메소드가 실행되지 않는 현상이 발생했습니다.
@PostConstruct
먼저 @PostConstruct 어노테이션에 대해서 간단하게 알아보도록 하겠습니다. 이 어노테이션은 @PreDestroy과 쌍을 이루는 어노테이션으로 xml기반의 Bean 설정에서 각각 'init-method'와 'destroy-method' 해당합니다. @PostConstruct를 이용하면 Bean 객체를 생성한 이후에 초기화를 수행할 메소드를 지정할 수 있으며 @PreDestroy는 Bean 객체의 라이프 사이클이 종료되기 직전에 사용한 자원의 해제할 필요가 있을 경우등에 사용합니다.
※ @PostConstruct와 @PreDestroy는 Spring이 아닌 javax패키지로 Java의 표준 어노테이션이라는 점에 주의하시기 바랍니다.
@PostConstruct를 지정한 메소드가 호출되지 않는 원인
원인을 명확히 하기 위해서 전체 설정중 관련부분중 필수적인 부분만 발췌하여 코드를 작성해서 단위테스트를 실행해 보았습니다.
설정 클래스
//...package import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; //...import @Configuration @ComponentScan(basePackages = "test") public class TestConfig { private static final Logger LOGGER = LoggerFactory.getLogger(TestConfig.class); @Value("${testkey}") private String value; @Bean public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() { final Properties properties = new Properties(); properties.setProperty("testkey", "testvalue"); PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer(); configurer.setProperties(properties); return configurer; } @PostConstruct public void post() { LOGGER.debug(">> Post Construct - start"); LOGGER.debug(">> Post Construct - injected value : {}", this.value); LOGGER.debug(">> Post Construct - finish"); } }
단위 테스트 클래스
//...package //...import @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = TestConfig.class) public class TestClass { private static final Logger LOGGER = LoggerFactory.getLogger(TestClass.class); @Value("${testkey}") private String value; @Test public void test() { LOGGER.debug(">> Test - start"); LOGGER.debug(">> Test - injected value : {}", this.value); LOGGER.debug(">> Test - finish"); } @PostConstruct public void post() { LOGGER.debug(">> Test Post Construct - start"); LOGGER.debug(">> Test Post Construct - injected value : {}", this.value); LOGGER.debug(">> Test Post Construct - finish"); } }
위와 같이 설정한 후 테스트를 실행한 결과 @PostConstruct가 지정된 메소드가 실행되지 않아 로그가 출력되지 않는 현상을 확인할 수 있었습니다. 반면에 PropertyPlaceholder관련 설정 빼고 테스트를 실행했을 경우는 해당 메소드가 실행되어 로그가 출력되었습니다. 단위 테스트 클래스에서 @PostConstruct를 지정한 메소드는 두 케이스 모두 정상적으로 실행되었으므로 해당 설정은 Bean단위로 영향을 미친다는 것을 확인 할 수 있었습니다.
※ Java 기반의 설정 클래스도 하나의 빈으로 등록되어 처리되는 점에 유의하시기 바랍니다.
현상을 특정하고 원인을 찾아보던중 허무하게도 Spring의 JavaDoc에서 원인을 파악할 수 있었습니다. 해당 문서는 영문인 관계로 발번역이지만 간단하게 번역해 보았습니다.
Special consideration must be taken for @Bean methods that return Spring BeanFactoryPostProcessor (BFPP) types. Because BFPP objects must be instantiated very early in the container lifecycle, they can interfere with processing of annotations such as @Autowired, @Value, and @PostConstruct within @Configuration classes. To avoid these lifecycle issues, mark BFPP-returning @Bean methods as static.
첫번째 단락은 BeanFatoryPostProcess(BFPP) 타입을 반환하는 @Bean 메소드의 경우는 컨테이너의 라이프 사이클에서 매우 이른 시점에 초기화 되어야 하므로 @Configuration 클래스 내부의 @Autowired, @Value, @PostConstruct 와 같은 어노테이션 기반 프로세싱을 방해할 수 있다는 내용으로 회피하기 위에서는 @Bean 메소드를 static 메소드로 정의하여야 한다는 내용입니다.
By marking this method as static, it can be invoked without causing instantiation of its declaring @Configuration class, thus avoiding the above-mentioned lifecycle conflicts. Note however that static @Bean methods will not be enhanced for scoping and AOP semantics as mentioned above. This works out in BFPP cases, as they are not typically referenced by other @Bean methods. As a reminder, a WARN-level log message will be issued for any non-static @Bean methods having a return type assignable to BeanFactoryPostProcessor.
정적 메소드로 정의함으로서 @Configuration 클래스의 인스턴스를 생성하지 않고도 초기화가 가능하기 때문에 위에서 언급한 라이프 사이클 문제를 회피할 수 있지만 Inter-bean references를 사용할 수 없다는 문제가 있습니다. 하지만 BeanFactoryPostProcess 타입의 빈은 일반적으로 Inter-bean references를 사용하지 않기 때문에 특별히 문제되지 않는다고 기술되어 있습니다.
해결책
위의 인용구에서 언급된 것과 같이 BeanFactoryPostProcessor 타입의 Bean을 선언할 경우에는 해당 선언 메소드를 static 메소드로 선언하면 해당 문제를 회피할 수 있습니다.
위의 인용구 중 마지막 문장에 @Bean 메소드중 리턴타입이 BeanFactoryPostProcess이면서 정적메소드가 아닌경우 WARN레벨의 로그 메시지를 출력한다는 부분이 있어 확인해보았습니다.
WARN o.s.c.a.ConfigurationClassEnhancer - @Bean method TestConfig.propertyPlaceholderConfigurer is non-static and returns an object assignable to Spring's BeanFactoryPostProcessor interface. This will result in a failure to process annotations such as @Autowired, @Resource and @PostConstruct within the method's declaring @Configuration class. Add the 'static' modifier to this method to avoid these container lifecycle issues; see @Bean javadoc for complete details.
실제로 로그 메시지를 확인한 결과 위와 같이 WARN 레벨의 로그가 출력되고 있는 것을 확인할 수 있었습니다. 로그 메시지를 잘 확인하고 있었다면 문제가 발생한 시점에 대응책을 확인할 수 있었을 걸 하는 생각도 들었지만 사실 개발 단계에서는 DEBUG 레벨의 로그를 출력하도록 설정해 놓기 때문에 출력되는 로그가 방대하여 특별히 에러가 발생하기 전에는 인지하기 어려운 것도 현실인 듯 합니다. 따라서 주기적으로 로그를 확인하는 것 보다 설정 관련 부분은 사소한 부분도 단위 테스트를 이용하여 설정 미스나 다른 설정에 영향을 받아 상정하고 있던 것과 다른 동작을 하는 경우를 즉시 확인할 수 있도록 하는 것이 문제 예방의 지름길이 아닐까 생각해 보았습니다.
▷ 본문에서 인용한 JavaDoc은 아래의 링크를 통해서 확인하실 수 있습니다.
http://docs.spring.io/spring/docs/3.2.x/javadoc-api/org/springframework/context/annotation/Bean.html
'Java > Spring' 카테고리의 다른 글
하이버네이트 AttributeConverter에서 스프링 DI사용 (0) | 2019.03.13 |
---|---|
Spring Security 단위테스트 (2) | 2019.03.10 |
Spring Boot 재기동 없이 개발하기(Spring Loaded) (5) | 2015.02.09 |
스프링 트랜잭션 관리의 이해 - 개념편 (0) | 2014.11.05 |
Spring 로그 - Could not explicitly release JDBC savepoint (0) | 2014.11.05 |