Java/Spring2015. 11. 17. 08:01
반응형

 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


Posted by Reiphiel