[Spring] R2dbcEntityTemplate에서 @Transactional이 동작하지 않는 이슈
이슈 상황
@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 {
// ...
}
}
'개발 이야기 > Spring' 카테고리의 다른 글
[Spring] Spring Event 정리 (0) | 2022.11.04 |
---|---|
[Spring] Flyway 가이드 (Kotlin + Spring + R2DBC) (2) | 2022.11.03 |
[Spring] @ModelAttribute 사용할 때 주의할 점 (0) | 2021.10.10 |
[Spring] 서블릿과 JSP의 차이 (9) | 2021.06.20 |
[Spring] 서블릿과 서블릿 컨테이너란? (0) | 2021.06.19 |
댓글