개발 이야기/Spring

[Spring] R2dbcEntityTemplate에서 @Transactional이 동작하지 않는 이슈

제이온 (Jayon) 2022. 7. 18.

이슈 상황

@Configuration
@EnableR2dbcRepositories(entityOperationsRef = "mySQLEntityTemplate")
class MySQLConfiguration {

    @Autowired
    private lateinit var env: Environment

    @Bean
    @Qualifier("mySQLConnectionFactory")
    fun mySQLConnectionFactory(): ConnectionFactory {
        // ...
    }
}

@CoroutineLogging
@Transactional // 동작하지 않음. (롤백 x)
suspend fun createChannel(command: CreateChannel.Command): CreateChannel.Result {
    // ...   
}

 

R2DBC 관련 설정 클래스에서 ConnectionFactory 객체를 빈으로 등록하고, 코드에는 없지만 R2dbcEntityTemplate을 사용하는 CRUD 코드를 여러 개 실행한 뒤 예외가 발생하더라도 롤백이 발생하지 않았습니다.

 

원인

결론부터 이야기하면, R2DBC 클래스에서 TransactionManager를 빈으로 등록하지 않았기 때문입니다.

 

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public TransactionManagerCustomizers platformTransactionManagerCustomizers(
            ObjectProvider<PlatformTransactionManagerCustomizer<?>> customizers) {
        return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
    public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
        return TransactionalOperator.create(transactionManager);
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnSingleCandidate(PlatformTransactionManager.class)
    public static class TransactionTemplateConfiguration {

        @Bean
        @ConditionalOnMissingBean(TransactionOperations.class)
        public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
            return new TransactionTemplate(transactionManager);
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnBean(TransactionManager.class)
    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    public static class EnableTransactionManagementConfiguration {

        @Configuration(proxyBeanMethods = false)
        @EnableTransactionManagement(proxyTargetClass = false)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
        public static class JdkDynamicAutoProxyConfiguration {

        }

        @Configuration(proxyBeanMethods = false)
        @EnableTransactionManagement(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
                matchIfMissing = true)
        public static class CglibAutoProxyConfiguration {

        }

    }

}

 

위 클래스는 트랜잭션 세팅과 관련된 내장 JDK 코드입니다. 여기서 핵심이 되는 부분은 바로 EnableTransactionManagementConfiguration 클래스입니다.

 

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

    @Configuration(proxyBeanMethods = false)
    @EnableTransactionManagement(proxyTargetClass = false)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
    public static class JdkDynamicAutoProxyConfiguration {

    }

    @Configuration(proxyBeanMethods = false)
    @EnableTransactionManagement(proxyTargetClass = true)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
            matchIfMissing = true)
    public static class CglibAutoProxyConfiguration {

    }

}

 

해당 클래스는 @EnableTransactionManagement을 사용하여 AOP 방식으로 선언적 트랜잭션 어노테이션(@Transactional)을 사용할 수 있도록 만들어 줍니다. 이때 @EnableTransactionManagement의 proxyTargetClass가 false이면 JDK 다이나믹 프록시 방식으로, true면 CGLIB 방식으로 세팅됩니다. 스프링 부트에서는 기본적으로 CGLIB 방식으로 AOP가 동작합니다.

 

여기서 중요한 점은 @ConditionalOnBean에 의하여, EnableTransactionManagementConfiguration 클래스는 TransactionManager가 Bean으로 등록이 되어 있어야 같이 Bean으로 등록된다는 사실입니다. 즉, TransactionManager가 있어야만 @Transactional이 동작한다는 의미죠.

 

JPA를 사용해 온 개발자라면, 저는 그동안 TransactionManager를 Bean으로 등록한 적이 없는데요? 라고 의문을 가질 수 있습니다. 그것은 바로, Spring Data JPA는 자동으로 JpaTransactionManager를 Bean으로 등록해주기 때문에 정상적으로 @Transactional이 동작했던 것입니다.

 

해결 방법

@Configuration
@EnableR2dbcRepositories(entityOperationsRef = "mySQLEntityTemplate")
class MySQLConfiguration {

    @Autowired
    private lateinit var env: Environment

    @Bean
    @Qualifier("mySQLConnectionFactory")
    fun mySQLConnectionFactory(): ConnectionFactory {
        // ...
    }

   @Bean
   fun reactiveTransactionManager(@Qualifier("mySQLConnectionFactory") connectionFactory: ConnectionFactory): ReactiveTransactionManager {
       return R2dbcTransactionManager(connectionFactory)
   }
}

 

적절한 트랜잭션 매니저를 빈으로 등록하면 됩니다. 우리는 R2DBC를 사용하고 있으므로 R2dbcTransactionManager 객체를 빈으로 등록하면 끝입니다.

 

디버깅해 보면 정상적으로 트랜잭션을 생성하는 것을 알 수 있습니다.

 

 

기타 주의 사항

TransactionManager가 여러 개일 경우 @Transactional에서 어떠한 TransactionManager 객체를 선택해야 할 지 알 수 없다는 예외가 발생합니다. 이때는 아래와 같이 @Transactional의 value 값을 TransactionManager bean의 Qualifier를 설정해 주면 됩니다.

 

@Configuration
@EnableR2dbcRepositories(entityOperationsRef = "mySQLEntityTemplate")
class MySQLConfiguration {

    @Autowired
    private lateinit var env: Environment

    @Bean
    @Qualifier("mySQLConnectionFactory")
    fun mySQLConnectionFactory(): ConnectionFactory {
        // ...
    }

    @Bean
    fun mysqlTransactionManager(@Qualifier("mySQLConnectionFactory") connectionFactory: ConnectionFactory): ReactiveTransactionManager {
        return R2dbcTransactionManager(connectionFactory)
    }
}

@Configuration
@EnableR2dbcRepositories(entityOperationsRef = "postgresQLEntityTemplate")
class PostgresQLConfiguration {

    @Autowired
    private lateinit var env: Environment

    @Bean
    @Qualifier("postgresQLConnectionFactory")
    fun postgresQLConnectionFactory(): ConnectionFactory {
        // ...
    }

    @Bean
    fun postgresQLTransactionManager(@Qualifier("postgresQLConnectionFactory") connectionFactory: ConnectionFactory): ReactiveTransactionManager {
        return R2dbcTransactionManager(connectionFactory)
    }
}

@Service
class SharedTestChannelApplicationServiceLogic(
    // ...
) {
    @CoroutineLogging
    @Transactional("mysqlTransactionManager")
    suspend fun setup(command: SharedTestChannelApplicationService.Setup.Command): SharedTestChannelApplicationService.Setup.Result {
        // ...
    }

    @CoroutineLogging
    @Transactional("postgresQLTransactionManager")
    suspend fun setup2(command: SharedTestChannelApplicationService.Setup.Command): SharedTestChannelApplicationService.Setup.Result {
        // ...
    }
}

댓글

추천 글