Languege/Java & Spring / / 2025. 1. 24. 20:26

[Spring Transaction] TransactionManager 동작방식

🎈 개요

  • Mybatis와 JPA를 같이 사용중인 프로젝트가 있는데 TransactionManager가 분리되어 설정
  • JpaTransactionManager의 beanName이  'transactionManager'로 설정되어 있어 mainDB에서 사용하는 TransactionManager와 BeanName이 중복되어 'jpaTransactionManager'로 변경
  • 이후 JpaRepository.save() 메소드에서 transactionManager를 찾지 못하는 오류 발생

 

🔎 문제 원인

설정된 transactionManager가 반영이 안되고 있다.

 


No bean named 'transactionManager' available: No matching TransactionManager bean found for qualifier 'transactionManager' - neither qualifier match nor bean name match!

0 = {StackTraceElement@27738} "org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.qualifiedBeanOfType(BeanFactoryAnnotationUtils.java:136)"
1 = {StackTraceElement@27739} "org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.qualifiedBeanOfType(BeanFactoryAnnotationUtils.java:95)"
2 = {StackTraceElement@27740} "org.springframework.transaction.interceptor.TransactionAspectSupport.determineQualifiedTransactionManager(TransactionAspectSupport.java:515)"
3 = {StackTraceElement@27741} "org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager(TransactionAspectSupport.java:496)"
4 = {StackTraceElement@27742} "org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:342)"
5 = {StackTraceElement@27743} "org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)"
6 = {StackTraceElement@27744} "org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)"
7 = {StackTraceElement@27745} "org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)"
8 = {StackTraceElement@27746} "org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)"
9 = {StackTraceElement@27747} "org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)"
10 = {StackTraceElement@27748} "org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)"
11 = {StackTraceElement@27749} "org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)"
12 = {StackTraceElement@27750} "org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)"
13 = {StackTraceElement@27751} "org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)"
14 = {StackTraceElement@27752} "jdk.proxy3/jdk.proxy3.$Proxy296.save(Unknown Source)"

 

위 stackTrace에서 2,3,4를 분석하여 분명히 설정되어 있는 TransactionManager를 왜 찾지 못하는지 분석하였습니다.

 


 

1.TransactionAspectSupport.invokeWithinTransaction

해당 메소드 최상단에서 TransactionManager를 결정하는 메소드가 있고, 트랜잭션 매니저를 결정해서 이후 다양한 로직들을 처리하는데  내부 구현을 살펴보겠습니다.

  1. 현재 메서드와 클래스에 적용된 트랜잭션 속성(애노테이션 등)을 가져올 수 있는 TransactionAttributeSource를 가져옵니다.
  2.  @Transactional에 적용된 속성을 가져오는데, @Transactional(transactionManager = "jpaTransactionManager") 로 적용하게 되면 txAttr의 qualifier 라는 필드에 설정되어 위에 기재한 트랜잭션 매니저가 설정되어야 합니다.
     => 하지만, qualifier가 빈 문자열("")로 넘어오고 있었음
  3. determineTransactionManager(txAttr) 메서드를 통해, 해당 트랜잭션 속성에 맞는 실제 트랜잭션 매니저(예: DataSourceTransactionManager, JpaTransactionManager 등)를 선택합니다.

 

2. TransactionAspectSupport.determineTransactionManager

원래 정상적인 케이스라면 여기서 qualifier가 @Transactional(transactionManager = "jpaTransactionManager") 해당 어노테이션을 적용받아 "jpaTransactionManager"로 설정되어야 합니다.

하지만, 받지 못했기에 두 번째 else if문을 타게 됩니다. "transactionManagerBeanName" 를 보게되면 아래와 같이 클래스 내부에서 사용 가능하도록 선언되어 있고 기본값으로 문제의 "transactionManager"를 확인할 수 있습니다.

 

3. TransactionAspectSupport.determineQualifiedTransactionManager

위 메소드는 간단합니다.

캐싱된 트랜잭션 매니저가 있는지 qualifier(현재는 transactionManager)를 통해 확인하고, 없으면 beanFactory에서 가져와서 캐시에 저장하게 됩니다.

여기서 문제가 발생합니다. 현재 transactionManager라는 bean은 등록되어 있지않고 "jpaTransactionManager"와 "dataSourceTransactionManager" 두개의 bean name으로 등록되어 있어 문제가 발생하였습니다.

 

👍 문제 해결

해당 문제를 해결하기 위해 다양한 방법을 시도했었는데, 원인은 기존에 생성되어 있었던 Config Class에 있었습니다.

// JpaDBConfig.java
@Configuration
@EnableJpaRepositories(
   basePackages = "kr.co.example.jpa",
   // transactionManagerRef = "jpaTransactionManager" // <-- This is missing!!
)
public class JpaDBConfig {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(/* ... */) {
        // ...
    }

    @Bean(name = "jpaTransactionManager")
    public JpaTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

위 코드에서 basePackages에 적용 될 transactionManager의 default Bean name을 설정해주지 않아서 발생하는 문제였고, 해당 설정이 없으니 "transactionManager"라는 bean만 찾고 있었습니다.

해당 문제를 Spring-Framwork에 Issue로 등록하였고 저와 같은 실수로 문제를 겪으시는 분들께 도움이 되길 바라겠습니다.

문제를 해결하면서 많은 시행착오가 있었지만 TransactionManager에 대한 깊은 이해를 할 수 있게되어 즐거웠습니다.

추가로 TransactionManager가 JPA에서 동작하지 않을 때 SpringDataJpa를 implement 하는 interface에 @Transactional(transactionManager = "jpaTransactionManager") 를 붙이게 되면 위 설정 없이도 정상적으로 동작하게 되는데, TransactionManager를 찾는 과정에서 해당 interface에 어노테이션을 확인하기에 가능합니다. 일반적으로 클래스에서 위 인터페이스를 호출하기에 참고하시면 좋을 것 같습니다.

 

이슈 구경하러 가기(상세 예제 포함되어 있음)

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

 

Automatically detect or warn when JPA repositories lack `transactionManagerRef` · Issue #34185 · spring-projects/spring-framew

Hello Spring Team, My name is kimsan. I've recently encountered a common configuration pitfall when using multiple transaction managers (for example, mainDBTransactionManager for MyBatis/JDBC and j...

github.com