선언적 트랜잭션과 명령형 트랜잭션의 혼합 사용
문제
스프링부트에서 트랜잭션을 관리할 때, 대부분의 개발자들이 선언형 트랜잭션이라 불리는 @Transactional
어노테이션을 사용한다. 그 이유는 확실하다. @Transactional
은 AOP로 트랜잭션을 관리해주기 때문에, 개발자가 트랜잭션을 관리하는 코드를 작성하지 않아도 된다.
그런데 만약 선언형 트랜잭션과 명령형 트랜잭션을 같이 사용하게 되면 어떻게 될까? 무엇이 롤백되고, 무엇이 커밋될까? 아니면 트랜잭션의 경계가 꼬여버릴까? 한 번 알아보자.
준비 코드
Member.class
Member
는 JPA 엔티티이며, 이름과 나이를 멤버로 가진다.MemberService.class
- 메서드에
@Transactional
을 붙인 채로PlatformTransactionManager
를 주입받아 사용한다.
Test Code
- 우선 간단한 테스트를 위해
MemberService
에 대한 테스트를 만들었다. - 테스트에서 딱 하나 저장하기 때문에
findAll()
을 했을 때 반환되는List<Member>
가 1개가 맞는 지 확인하려고 한다.
테스트
이 상태로 테스트를 한 번 돌려보자.
문제 없이 Member
가 하나 저장되었다. 이번에는 rollback
을 해보자. 그러자 알 수 없는 예외가 발생했다.
Transaction silently rolled back because it has been marked as rollback-only
. 트랜잭션이 rollback-only
가 마크되어있어 말없이 롤백 됐다고 한다. 해당 예외는 트랜잭션이 전파 시 내부 트랜잭션의 롤백의 영향을 받아 외부 트랜잭션도 롤백될 때 나타난다. 여기서 알 수 있듯이, 내부에서 명령형 트랜잭션을 rollback
하면, rollback-only
가 Mark 된다.
여기서 하나 짚고 가자면, PlatformTransactionManager
의 getTransaction()
를 호출할 때 사용한 DefaultTransactionDefinition
은 기본적으로 REQUIRED
전파 옵션을 가진다.
트랜잭션 전파 옵션에는
REQUIRED
,REQUIRES_NEW
,NESTED
,SUPPORTS
,NOT_SUPPORTS
,MANDATORY
,NEVER
가 있다. 각각에 대한 설명은 https://www.baeldung.com/spring-transactional-propagation-isolation 를 참고하면 된다. 여기서는REQUIRED
와REQUIRES_NEW
만 다룬다.
REQUIRED
옵션은 기존에 트랜잭션이 존재하면 해당 트랜잭션에 참여하고, 없으면 새로 트랜잭션을 생성한다. 따라서, 위의 save()
메서드의 경우, @Transactional
이라는 외부 트랜잭션이 이미 있기 때문에, PlatformTransactionManager.getTransaction()
으로 트랜잭션을 가져올 때 기존 트랜잭션에 참여한다. REQUIRED
옵션은 내부 트랜잭션이 rollback
하면, 외부 트랜잭션도 rollback
이 되는 특성을 가지고 있다.
좀 전에 발생한 예외가 이러한 이유에서 발생하는 예외이다. 그렇다면 전파 옵션이 REQUIRES_NEW
라면 어떨까? 이번에는 예외는 발생하지 않고, 테스트의 Assertion이 실패했다고 한다.
롤백을 했기 때문에 Member
가 저장되지 않는 것이 당연하다. 이제 궁금한 것은 트랜잭션의 경계이다.
TransactionManager
에서 getTransaction()
을 하면 트랜잭션이 시작하는 것이라고 판단한다. 그렇다면 위와 같이 명령형 트랜잭션이 시작되기 전에 outerMember
를 저장하고, 명령형 트랜잭션에서 innerMember
를 저장한 후 명령형 트랜잭션만 롤백해보자. 테스트 코드는 다음과 같다.
outerMember
와 innerMember
를 각각 저장하고, innerMember
만 롤백되는 것을 예측한 테스트이다. 두구두구….
테스트는 성공했다. 예측대로 명령형 트랜잭션은 선언형 트랜잭션만 사용할 때와 마찬가지로 트랜잭션이 전파된다.
결론
선언형 트랜잭션이 선언된 메서드 내부에서 트랜잭션 매니저로 직접 트랜잭션을 가져와도, 선언형 트랜잭션만 사용하는 것 처럼 트랜잭션이 전파된다. 생각해보면 당연한 게, 선언형 트랜잭션은 AOP이기 때문에 어쨌든 명령형 트랜잭션으로 비즈니스 코드를 감싸준 것 뿐이다. @Transactional
은 마법이 아니라 AOP 프록시임을 다시 한 번 생각하게 되었다.
References
- https://www.baeldung.com/spring-transactional-propagation-isolation