Spring Framework 4.1.4 Transaction Manager 이름 관련 버그

이번 주부터 Spring Framework 3.1 로 개발된 프로젝트를 마이그레이션하고 있다.
처음에는 3.2로 진행을 하다가 오늘 동료가 보내준 링크를 보니 3.2는 2015년 말이면 더이상 업데이트는 없고 spring 에서는 4.2를 개발중이라고…
그래서 4.1로 마이그레이션 하기로 했다.

문제

일단 가장 기본적인 것만 수정한 후 실행을 시켰는데 Transaction Manager 과 관련돤 오류가 발생했다.
프로젝트는 단일이 아닌 복수의 Transaction Manager로 구성되어 있고 기본 TransactionManager 로 지정된 것이 없다.

예를 들면 아래와 같이 2개의 TransactionManager 를 설정하였다.

<jpa:repositories base-package="com.foo" entity-manager-factory-ref="fooEm"
 transaction-manager-ref="fooTx"/>
<jpa:repositories base-package="com.foo" entity-manager-factory-ref="fooEm"
 transaction-manager-ref="fooTx"/>

각 TransactionManager를 사용하는 서비스가 있다. 서비스의 클래스에 @Transactional 을 설정하고, 필요한 경우 메소드에도 @Transactional 을 설정했다.

package com.foo;
@Entity
@Transactional(value = "fooTx")
public class FooServiceImpl implements FooService {
  @Autowired
  private FooRepository fooRepository;

  @Transactional
  public List<Foo> findAll() {
    return fooRepository.findAll();
  }
}
package com.bar;
@Entity
@Transactional(value = "barTx")
public class BarServiceImpl implements BarService {
  @Autowired
  private BarRepository barRepository;

  @Transactional
  public List<Bar> findAll() {
    return barRepository.findAll();
  }
}

그리고 프로그램을 실행하면 다음과 같은 오류가 뜨는 것을 볼 수 있다.

Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [org.springframework.transaction.PlatformTransactionManager] is defined: expected single matching bean but found 2: fooTx,barTx
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:365)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:331)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager(TransactionAspectSupport.java:370)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:271)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodIntercceptor.invoke(CrudMethodMetadataPostProcessor.java:122)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)

원인

이미 Spring 3.2.x 로 마이그레이션을 하여 실행에 문제가 없는 것을 확인했었는데 4.1에서는 안되는 것이 수상했다.
그래서 오류가 발생한 TransactionAspectSupport.determineTransactionManager() 를 디버그했다.

그리고 프로젝트 세대(Project  generation) 버전 업그레이드라 소스가 많이 바뀌었는데, 매우 이상한 것을 발견했다.

protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
  // Do not attempt to lookup tx manager if no tx attributes are set
  if (txAttr == null || this.beanFactory == null) {
    return getTransactionManager();
  }
  String qualifier = (txAttr.getQualifier() != null ?
      txAttr.getQualifier() : this.transactionManagerBeanName);
  if (StringUtils.hasText(qualifier)) {
    PlatformTransactionManager txManager = this.transactionManagerCache.get(qualifier);
    if (txManager == null) {
      txManager = BeanFactoryAnnotationUtils.qualifiedBeanOfType(
          this.beanFactory, PlatformTransactionManager.class, qualifier);
      this.transactionManagerCache.putIfAbsent(qualifier, txManager);
    }
    return txManager;
  }
  else {
    PlatformTransactionManager defaultTransactionManager = getTransactionManager();
    if (defaultTransactionManager == null) {
      defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
      this.transactionManagerCache.putIfAbsent(
          DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
    }
    return defaultTransactionManager;
  }
}

@Transactional 은 value 를 지정하지 않으면 빈공백이 기본 값이다. 그런데 위의 356 줄과 357줄을 보면 값 비교가 null 이다.
따라서 transactionManagerBean 명을 사용하지 못하고, defaultTransactionManager 도 지정하지 않았으니 예외가 발생되는 것이다.
Spring 3.2.x 소스는 정상적으로 값 비교를 하고 있어서 문제가 없었다.

해결

문제를 해결 하려면 아래와 같은 형태로 소스를 변경해야 한다.

String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
  // qualifier에 대한 TX 반환;
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
  // transactionManagerBeanName 에 대한 TX 반환
}

Spring JIRA에 버그 등록을 하기 전에 검색을 해보니 이미 해당 문제가 2015년 1월 1일 SPR-12577 로 보고되었고 이미 4.1.5 및 4.2RC 버전은 수정되었단다.
그래서 GitHub에서 4.1.x 브랜치의 해당 소스를 찾아 비교해 보니, null 대신 텍스트가 있는지로 비교를 하고 transactionManagerBean 도 사용할 수 있도록 수정된 것을 확인 할 수 있었다.

테스트

4.1.5 버전을 아래와 같이 변경하여 테스트 해보았는데 해당 문제가 발생하지 않았다.

  • Repository 를 http://repo.spring.io/snapshot로 변경
  • 버전을 4.1.5.BUILD-SNAPSHOT로 변경

그동안 Spring의 4.1 버전의 릴리즈 날짜를 보면 아래와 같다.

  • 4.1.0 : 2014-09-04
  • 4.1.1 : 2014-10-01
  • 4.1.2 : 2014-11-11
  • 4.1.3 : 2014-12-09
  • 4.1.4 : 2014-12-30

약 30일 정도가 릴리즈 주기라고 보면 4.1.5는 1월 말이나 2월 초가 될 것으로 예상해본다.

참고