Java/Spring2019. 5. 9. 02:00
반응형

Spring 트랜잭션 관리방법

Spring(스프링)에서 트랜잭션(Transaction)을 관리하는 방법은 크게 서로 대비되는 2가지 방법으로 나눌 수 있습니다.

프로그램에 의한(Programmatic) 트랜잭션 관리

첫번째로 알아볼 방법은 프로그램 코드에 의한 트랜잭션 관리입니다.

@Autowired
private PlatformTransactionManager transactionManager;

public void operateSome() {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
    } catch (RuntimeException e) {
        transactionManager.rollback(status);
        throw e;
    } finally {
        if (status.isRollbackOnly()) {
            transactionManager.rollback(status);
        } else {
            transactionManager.commit(status);
        }
    }
}

위의 코드와 같이 트랜잭션 매니저를 통해서 직접 트랜잭션 개시, 커밋, 롤백등을 수행하는 방법으로 소스코드에 직접기술하기 때문에 가독성을 떨어트리고 실수할 가능성도 높아지므로 많이 사용하는 방식은 아닙니다만, 내부구현을 외부에 노출하고 싶지 않은 경우에 사용할 수 있습니다.

선언적(Declarative) 트랜잭션 관리

다음은 선언적 트랜잭션 관리 입니다. 선언적 트랜잭션 관리라는 말이 어려울 수 있지만 간단하게 설명하면 트랜잭션에 관한 코드를 비지니스 코드로 부터 분리해서 비침투적인 방법으로 기술하여 관리하는 방법을 의미합니다.

어노테이션(Annotation)으로 트랜잭션 선언

@Transactional
public class SomeService() {
    @Transactional
    public void operateSome() {
    }
}

@Transactional 어노테이션을 사용할 경우에는 프래그램을 사용할 때와같이 트랜잭션 매니저를 직접 지정하지 못하기 때문에 transactionManager 라는 속성을 통해서 사용할 트랜잭션 매니저를 지정할 수 있습니다.

AOP설정으로 트랜잭션 선언

<aop:config>
    <aop:pointcut id="serviceOperation"
          expression="execution(* service..*Service.*(..))"/>
    <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
</aop:config>

<tx:advice id="txAdvice">
    <tx:attributes>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>
@Bean
public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) {
    return new TransactionInterceptor(transactionManager, transactionAttributeSource());
}

@Bean
public NameMatchTransactionAttributeSource transactionAttributeSource() {
    NameMatchTransactionAttributeSource tas = new NameMatchTransactionAttributeSource();

    Map<String, AttributeSource> matches = new HashMap<>();
    matches.put("get*", new RuleBasedTransactionAttribute());
    return tas;
}

@Bean
public AspectJExpressionPointcutAdvisor transactionAdvisor(TransactionInterceptor advice) {
    AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
    advisor.setAdvice(advice);
    advisor.setExpression("execution(* service..*Service.*(..))");
    return advisor;
}

스프링에서 제공하는 설정방식인 어노테이션이나 혹은 AOP설정 이용하여 트랜잭션 관리를 수행할 수 있습니다. @Transactional 어노테이션 이외의 방식은 거의 사용되지 않으므로 할 수 있다정도만 알고 있으면 되겠습니다.

Spring 트랜잭션 속성

속성 설명
propagation 트랜잭션 개시할지 등 전파행위에 관한 속성.
isolation 트랜잭션 격리레벨에 관한 속성으로 기본값은 Default레벨이며 실제 사용하는 데이터베이스(JDBC) 등의 기본값을 따릅니다.
readOnly 트랜잭션을 읽기전용으로 지정하는 속성. 최적화 관점에서 지원되는 프로터티이므로 현재 트랜잭션 상태에따라 다르게 동작할 수 있습니다.
timeout 트랜잭션의 타임아웃(초단위)을 지정하는 속성으로 지정하지 않을 경우 사용하는 트랜잭션 시스템의 타임아웃을 따릅니다.
rollbackFor Checked 예외 발생시에 롤백을 수행할 예외를 지정하는 속성.
rollbackForClassName rollbackFor와 동일하지만 문자열로 클래스명을 지정하는 속성.
noRollbackFor Spring의 트랜잭션은 기본적으로 Runtime예외만 롤백처리를 수행하지만 Runtime예외중 특정 예외는 롤백을 수행하지 않아야 할 경우 사용하는 속성.
noRollbackForClassName noRollbackFor와 동일하지만 문자열로 클래스명을 지정하는 속성.

설정예

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public class SomeService {

    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.DEFAULT, timeout = 10)
    public void operateSome() {
    }
    //...
}

위와같이 클래스 단위 혹은 메소드 단위로 지정할 수 있습니다. 메소드에 기술한 설정이 우선 적용되며 지정하지 않은 경우 클래스에 기술한 설정이 적용됩니다.

전파행위(Propagation Behavior)

전파행위(거동)는 트랜잭션을 개시할지 혹은 기존 트랜잭션을 이용할지 등 트랜잭션 경계(Transaction Boundary)를 설정할 때 이용하는 속성으로 가장 중요한 속성이라고 할 수 있습니다.

설정 가능한 전파행위 목록

속성 설명
MANDATORY 트랜잭션이 존재할 경우 해당 트랜잭션을 이용하며 존재하지 않을 경우 예외발생.
NESTED 트랜잭션이 존재할 경우 중첩된 트랜잭션을 개시하고 존재하지 않을 경우는 REQUIRED와 동일하게 동작.
NEVER 트랜잭션이 존재할 경우 예외발생.
NOT_SUPPORTED 트랜잭션이 존재할 경우 중단(Suspend)해서 트랜잭션을 이용하지 않음.
REQUIRED 트랜잭션이 존재하는 경우 해당 트랜잭션을 그대로 하며 개시된 트랜잭션이 없는 경우 트랜잭션 개시.
REQUIRES_NEW 항상 신규트랜잭션을 개시함. 트랜잭션이 존재하는 경우 해당 트랜잭션을 중단하고 새로운 트랜잭션 개시.
SUPPORTS 트랜잭션이 존재할 경우 해당 트랜잭션을 이용하고 존재하지 않을 경우는 트랜잭션을 이용하지 않음.

각각의 전파행위에 대해 좀더 자세히 알아보도록 하겠습니다.

MANDATORY

트랜잭션이 개시된 것을 강제해야할 경우 사용하는 속성으로 데이터베이스의 락을 취득한다던지 시퀀셜한 번호를 생성하거나 하는등 단독으로 사용할 이유가 없는 경우에 해당 상황을 배제하기 위해 사용합니다.

NESTED

중첩트랜잭션은 이미 트랜잭션이 시작되어있는 상태에서 트랜잭션 내부에 새로운 트랜잭션 경계를 설정하고자 할 때 사용하는 속성으로 JDBC의 Savepoint 기능을 이용합니다. Savepoint 기능이 JDBC 3.0이후부터 지원되는 영향인지 전파행위의 다른 속성들은 동일한 이름으로 EJB에 존재하지만 중첩 트랜잭션은 존재하지 않습니다.

중첩트랜잭션을 사용할만한 상황을 예를 들어보면 주문 트랜잭션 내부에서 포인트 적립을 처리하는 부분만 별도로 경계를 설정하고 해당 경계 내부의 처리에 실패하더라도 주문자체는 정상 처리시키고자 할 때 사용할 수 있을 듯 합니다. 하지만 최근에 많이 사용하는 JPA를 사용하는 경우, 변경감지를 통해서 업데이트문을 최대한 지연해서 발행하는 방식을 사용하기 때문에 중첩된 트랜잭션 경계를 설정할 수 없어 지원하지 않습니다.

JPA(Hibernate 구현체)에서 중첩 트랜잭션을 사용할려고 하면 아래와 같은 예외를 만나게 됩니다.

org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities

JDBC의 Savepoint관련 내용은 이전글( JDBC Transaction Savepoints)을 참조하세요.

NEVER

트랜잭션이 존재하는 경우에 예외를 발생시켜 트랜잭션을 사용하지 않는 것을 강제하는 속성입니다.

NOT_SUPPORTED

트랜잭션이 존재할 경우 해당 트랜잭션을 중단하고 트랜잭션이 없는 상태로 처리를 수행합니다.

REQUIRED

기본값으로 사용되는 속성으로 트랜잭션이 존재하지 않을 경우 개시하고 있으면 그대로 사용하는 속성입니다. 어노테이션기반의 설정일 경우에는 어노테이션에 기본값으로 명시되어있으며 그 이외의 경우에는 별도로 지정하지 않은경우 org.springframework.transaction.support.DefaultTransactionDefinition의 기본값이 사용됩니다.

REQUIRES_NEW

트랜잭션이 존재할 경우 해당 트랜잭션을 중단하고 새로운 트랜잭션 경계를 설정하는 속성으로 트랜잭션이 존재하지 않는 경우에는 REQUIRED와 동일한 동작을 합니다. 모든 트랜잭션 매니저가 실제 트랜잭션 중단을 지원하는 것은 아니기 때문에 일반적으로 기존 트랜잭션을 방치한 상태로 새로운 트랜잭션을 생성합니다.

다시 말해서 물리적으로 데이터베이스 커넥션을 새로 얻는다는 의미입니다. 요청이 많은 특정 서비스에 사용할 경우 데이터베이스 커넥션을 얻기위해 대기하는 리소스 데드락(Resource Deadlock - 특정리소스를 점유한 스레드들이 동일한 리소스를 얻으려고 대기하는 상태) 을 유발할 가능성이 있으므로 주의해서 사용해야할 필요가 있습니다. 위에서 설명한 NOT_SUPPORTED의 경우도 트랜잭션이 중단된 상태에서 다시 REQUIRED를 만나게 되면 동일한 현상이 발생할 가능성이 있습니다.

SUPPORTS

트랜잭션이 존재하면 해당 트랜잭션을 사용하고 존재하지 않을 경우 트랜잭션 경계를 설정하지 않는 속성입니다. 내부적으로 트랜잭션관련된 처리는 없지만 실패할 경우 트랜잭션 롤백을 해야할 경우에 사용할 수 있습니다.

격리레벨(Isolation Level)

JDBC에서 제공하는 격리레벨을 설정하는 속성 입니다. 격리레벨에 대한 설명은 JDBC나 데이터베이스에 관련된 내용이고 대부분의 경우에 기본 격리레벨로 충분하므로 어떤 값이 있는지만 간단하게 알아겠습니다.

속성 설명
DEFAULT 사용하는 저장소의 기본 격리레벨을 이용하는 속성으로 데이터베이스마다 다를 수 있습니다.
READ_UNCOMMITTED 다른 트랜잭션에의해 커밋되지 않은 변경사항을 읽을 수 있는 격리레벨로 Dirty read, Nonrepeatable read, Panthom read가 발생합니다.
READ_COMMITTED 다른 트랜잭션에 의해 커밋된 내용이 읽을때마다 반영되는 격리레벨입니다. Nonrepeatable read, Panthom read가 발생합니다. Oracle(오라클), PostgreSQL등의 기본 격리레벨입니다.
REPEATABLE_READ 동일 트랜잭션 경계안에서 반복해서 읽을 경우 다른 트랜잭션에 의해 커밋된 내용이 반영되지 않고 동일한 내용이 읽히는 격리레벨입니다. 다만 Panthom read는 발생합니다.
SERIALIZABLE 해당하는 테이블을 모두 잠그는 가장 높은 수준의 격리레벨로 다른 레벨에서 발생할 수 있는 모든 문제가 차단됩니다. 하지만 성능적인 측면에서 문제가 있어서 거의 사용되지 않습니다.
  • Dirty read는 커밋되지 않는 변경사항이 읽히는 문제로 해당하는 내용이 롤백될 경우 읽은 내용이 유효하지 않을 가능성이 있는 문제입니다.

  • Nonrepeatable read는 트랜잭션 경계 내부에서 반복적으로 읽기를 수행할때 다른 데이터를 읽을 수 있는 가능성이 있는 문제입니다.

  • Panthom read는 다른 트랜잭션에 의해서 추가된 행(Row)이 읽히는 문제입니다.

주의사항

Spring에서 제공하는 트랜잭션을 사용하면서 가장 중요하게 인식해야할 사항은 트랜잭션이 적용된 메소드를 경계로 트랜잭션의 상태가 관리된다는 점입니다. 예외를 트랜잭션이 적용된 메소드 외부로 던지면 트랜잭션에 롤백해야한다고 표시되며 해당 트랜잭션을 커밋할려고 시도할 경우 아래와 같은 예외와 만나게 됩니다.

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only

혹은

Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly

따라서 예외가 트랜잭션이 적용된 메소드 경계를 지나 던져졌을 경우 catch를 이용하여 잡더라도 특정 처리후에 다시 던지거나 다른 예외를 던저야 한다는 점에 주의해야합니다.

또 한가지 주의할 점은 Load Time Weaving(LTW)나 Compile Time Weaving등을 별도로 사용하지 않는 경우 AOP가 Proxy기반으로 동작한다는 점입니다. 이는 특정 Spring 빈 내부에서 this를 통해서 다른 메소드를 호출할 경우 트랜잭션에 관한 설정이 적용되지 않는다는 점입니다.

따라서 빈 내부에서 트랜잭션이 선언된 내부의 메소드를 호출할 경우에도 ApplicationContext를 통해서 빈의 레퍼런스를 얻는 방법을 통해서 Proxy를 경유해서 호출해야 트랜잭션에 관한 선언이 적용됩니다.

마치며

Spring에서 제공하는 트랜잭션관리 기능을 사용하는 방법과 주의사항에 대해서 알아보았습니다. 평소에 의식하지 않고 사용하는 기능이므로 정리하는 차원에서 작성했습니다. 이기회에 다같이 정리해 보는건 어떨까요?

참고자료

Transaction Management

Spring AOP : Replace XML with annotations for transaction management?

Posted by Reiphiel
Java/Spring2019. 3. 13. 23:02
반응형

 스프링(Spring)에 JPA구현체로 하이버네이트(Hibernate)를 사용할 경우 AttributeConverter에서 스프링의 DI를 사용할 수 없는 것이 오랫동안 아쉬웠습니다. JPA의 스펙상에는 2.2에서 CDI지원이 추가 되었으나 그동안은 하이버네이트에서 지원하지 않았기 때문에 약간의 꼼수를 통해 스프링 빈을 사용해왔습니다. 사실 CDI 지원은 JPA 2.1부터 추가 된것으로 보이나 2.1에서는 EntityListener에 대한 지원만이 정의 되었으며 2.2부터 AttributeConverter가 추가되었습니다.



CDI(Contexts and Dependency Injection) 는 JavaEE의 의존성 주입(DI)을 의미함(JSR 299)

JPA 2.2는 2017년8월4일에 릴리스(JSR338)


 

 하이버네이트 5.3.0(HHH-12135)에서 드디어 JPA 2.1 및 JPA2.2의 CDI를 지원하기 시작함으로서 아쉬운 상황이 해소되었습니다. CDI를 지원하기 위해 BeanContainer라는 인터페이스가 추가되었으며 JavaEE관련 해당 인터페이스 구현이 제공되고 있습니다. 스프링도 5.1(SPR-16305) 부터 SpringBeanContainer라는 해당 인터페이스의 구현을 제공하기 시작합니다. 따라서 해당버전을 사용할 경우 간단한 설정만으로 AttributeConverter에 DI를 적용할 수 있습니다. 스프링 부트의 경우 2.1.0부터 자동설정되는 것으로 보입니다.(정확한 버전은 확인하지 못했습니다)


 따라서 스프링 부트를 사용하는 경우는 지원하는 버전의 경우 별다른 설정 없이도 적용되므로 AttributeConverter를 스프링 빈으로 설정하면 컨버터에 DI가 적용되므로 편리하게 사용할 수 있을 것으로 보입니다.


 스프링 부트를 사용하지 않는 경우는 아래와 같이 별도의 설정이 필요합니다. 


    //...
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf 
            = new LocalContainerEntityManagerFactoryBean();
        //....
        emf.getJpaPropertyMap()
            .put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));
        //.....
        return emf;
    }
    //...

※xml 베이스의 설정도 가능하며 자세한 내용은 이 링크를 참고하시기 바랍니다.




  또한, 하이버네이트는 5.3.0 이상을 사용할 수 있지만 5.1이전의 스프링을 사용할 수 밖에 없는 상황이라고 하면 SpringBeanContainer의 구현을 참고하여 해당 기능을 직접 구현하여 사용할 수 있을 듯 합니다. 하지만 하이버네이트도 이전 버전만 사용가능한 상황의 경우는 약간의 꼼수를 동원해야할 듯 합니다. 다른 글에서도 많이 소개된 패턴이므로 간단하게 소개하도록 하겠습니다.


 

아래와 같이 ApplicationContext를 레퍼런스를 static 필드로 가질수 있는 Holder클래스를 정의.

public class ApplicationContextHolder {
    private static ApplicationContext applicationContext;
    
    public void setApplicationContext(ApplicationContext applicationContext) {
        ApplicationContextHolder.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }
}

 

스프링 초기화시에 ApplicationContext의 레퍼런스를 설정.

public class SomeConfiguration {
    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        ApplicationContextHolder.setApplicationContext(this.applicationContext);
    }
}

 

ApplicationContextHolder를 이용하여 AttributeConverter내에서 static으로 접근하여 스프링 빈을 얻어 사용.

public class CustomConverter implements AttributeConverter {
    @Override
    public String convertToDatabaseColumn(String attribute) {
        if ((attribute != null) && !attribute.isEmpty()) {
            return ApplicationContextHolder.getBean(Encoder.class).encode(attribute);
        }
        return attribute;
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        if ((dbData != null) && !dbData.isEmpty()) {
            return ApplicationContextHolder.getBean(Encoder.class).decode(dbData);
        }
        return dbData;
    }
}

※위에서 소개한 방법은 그다지 스프링다운 방식은 아니므로 추천하지는 않습니다만, 구버전을 사용하여 어쩔수 없는 경우에 한하여 사용하도록 합시다.


 한동안 지원되지 않아 불편했던 AttributeConverter에서 스프링 DI가 지원됨으로서 좀더 스프링스러운 방법으로 하이버네이트를 통합(integration)하여 사용할 수 있을듯 합니다. 여러분들도 가급적 새로 지원되는 기능을 이용하는 것을 추천 드립니다.



Posted by Reiphiel
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