-
Spring 트랜잭션 롤백 관리: rollback-only 이슈 분석과 해결📖 개발 공부 2024. 11. 10. 14:39
"Transaction silently rolled back because it has been marked as rollback-only"
요 에러는 어떤 상황에 발생할까?문제 상황
회사에서 주기적으로 돌고 있는 배치잡에서
Transaction silently rolled back because it has been marked as rollback-only
이런 로그가 발생하면서, 데이터 처리가 누락된 현상들을 종종 발견했다.
확인해보니 데이터 변경 로직이 포함된 유스케이스 메서드에 @Transactional 어노테이션이 적용되어 있으며, 이 메서드에서 @Transactional 이 적용된 FindHelper 클래스의 메서드를 호출하고 있다.
이 FindHelper 메서드에서 특정 조건에 부합하지 않으면, IllegalStateException 예외를 던지도록 되어있다.
다음 코드와 같이 유스케이스에서는 이 경우 runCatching을 통해서 null을 반환하도록 하였다. 그리고 data가 null이면 skip 메서드를 호출한다.
val data = runCatching { findHelper.find(id).data }.getOrNull() ?.takeIf { it.deletedAt == null } if (data == null) { skip() }
skip() 메서드 내부에서는 데이터를 변경하는 로직이 포함되어있다.
설명만 보면 코드는 트랜잭션 관리에 문제가 없어 보이지만, 실제로는 rollback-only 트랜잭션 상태를 고려하지 않은 흐름 제어가 문제의 핵심이다.
트랜잭션이 이미 rollback-only로 설정된 상태에서, 유스케이스에서 runCatching을 통해 IllegalStateException을 잡고 null을 반환하도록 설계된 것이다.
그렇기 때문에 skip() 메서드가 호출되어 데이터 변경 로직이 실행되더라도, 트랜잭션이 rollback-only 상태이기 때문에 변경된 내용이 커밋되지 않고 Transaction silently rolled back because it has been marked as rollback-only와 같은 예상치 못한 롤백이 발생했다.
→ 결국, skip() 메서드 내부의 데이터 변경이 반영되지 않는 문제로 이어진 것이다.
문제 해결
- 사실 FindHelper는 단순 조회 로직에 그치기 때문에, 트랜잭션이 필요없다. 그렇기 때문에 FindHelper에 달려있는 @Transactional 어노테이션을 제거했다.
기존에 하위 메서드에 어노테이션이 달려있을 때에는 해당 메서드 내에서 트랜잭션 상태를 변경할 수 있었는데,
어노테이션을 제거함으로써 유스케이스에서 해당 예외를 catch해 핸들링하면, rollback 마킹 없이 정상적으로 처리할 수 있게 된다. - 만약 내부에도 트랜잭션이 필요한 상태라면, Transaction propagation 속성을 REQUIRED_NEW 를 설정할 수 있다. REQUIRES_NEW 설정 시 현재 트랜잭션을 별도의 트랜잭션으로 실행하여 외부 트랜잭션에 독립적인 동작을 수행할 수 있다. 이는 내부 트랜잭션에서 발생한 예외가 외부 트랜잭션에 영향을 미치지 않게 할 수 있다.
트랜잭션 롤백 메커니즘 분석
Spring 트랜잭션 관리의 기본 동작에서, @Transactional 메서드 내에서 발생한 체크되지 않은 예외(Unchecked Exception, 여기서는 IllegalStateException)는 트랜잭션을 자동으로 rollback-only 상태로 설정한다.
위의 상황을 재현하는 코드를 작성해보았다.
@Service class TransactionPerformer( private val rollbackTestService: RollbackTestService, private val testRepository: TestRepository, ) { @Transactional fun execute() { try { rollbackTestService.performTransaction() } catch (e: Exception) { println("TransactionPerformer -- \\n" + e.message) } testRepository.save(TestEntity(name = "parent transaction")) } } @Service class **RollbackTestService**( private val testRepository: TestRepository ) { @Transactional fun performTransaction() { // 데이터 저장 testRepository.save(TestEntity(name = "First Entity")) // Checked Exception은 롤백을 마킹하지 않는다. // throw Exception("Simulated Exception") // RuntimeException은 롤백을 마킹한다. throw IllegalStateException("Simulated IllegalStateException") } }
throw IllegalStateException("Simulated IllegalStateException") 로직을 넣었을 때, 로그 level을 조정하여서 자세한 로그를 살펴보았다.
# 트랜잭션(com.may.transactionrollback.TransactionPerformer.execute) 생성 o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.may.transactionrollback.TransactionPerformer.execute]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT --- # IllegalStateException 예외 발생 직후 o.s.t.i.TransactionInterceptor : Completing transaction for [com.may.transactionrollback.RollbackTestService.performTransaction] after exception: java.lang.IllegalStateException: Simulated IllegalStateException o.s.orm.jpa.JpaTransactionManager : Participating transaction failed - **marking existing transaction as rollback-only** --- # 커밋 시점 o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(801808302<open>)] o.h.e.t.internal.TransactionImpl : committing cResourceLocalTransactionCoordinatorImpl : On commit, transaction was marked for roll-back only, rolling back
이렇게 IllegalStateException 예외가 발생한 이후 rollback-only=true가 설정되었다.
→ marking existing transaction as rollback-only이후, 커밋하는 시점에 rollback-only 마킹을 확인하고 롤백을 진행하는 것을 확인할 수 있다.
위의 설명을 다이어그램으로 표현해보았다.
IllegalStateException이 아닌, Exception 예외를 던지는 경우엔 Checked Exception이기 때문에 롤백을 마킹하지 않고, 커밋이 성공한다. 만약 Exception 또한 롤백으로 간주하고 싶다면 rollbackFor 속성을 추가하면 된다. (@Transactional(rollbackFor = [Exception::class]))
이번 이슈와 분석을 통해, rollback-only 트랜잭션 상태에 대한 공부를 할 수 있었다. 앞으로는 트랜잭션 상태와 예외 처리 방식을 더 세심하게 관리하여 유사한 문제에 대해 유연하고 빠른 대처를 할 수 있을 듯! 💪
🔗 참고
반응형'📖 개발 공부' 카테고리의 다른 글
Hikari CP 설정 알아보기 (0) 2024.11.24 The SAGA Pattern (0) 2024.09.29 Toss | 서버 증설 없이 처리하는 대규모 트래픽 (0) 2024.07.27 system-design-101. CAP 이론 (CAP theorem) (2) 2024.07.14 system-design-101. How to improve API performance? (0) 2024.06.22 - 사실 FindHelper는 단순 조회 로직에 그치기 때문에, 트랜잭션이 필요없다. 그렇기 때문에 FindHelper에 달려있는 @Transactional 어노테이션을 제거했다.