728x90

@Test
public void inner_rollback(){
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner); //rollback-only 표시
log.info("외부 트랜잭션 커밋");
Assertions.assertThatThrownBy(() -> txManager.commit(outer))
.isInstanceOf(UnexpectedRollbackException.class);
}
- 실행 결과를 보면 마지막에 외부 트랜잭션을 커밋할 때 UnexpectedRollbackException.class 이 발생하는 것을 확인할 수 있다. 이 부분은 바로 뒤에 설명한다.
실행 결과 - inner_rollback()
외부 트랜잭션 시작
Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@2101886654 wrapping conn0: url=jdbc:h2:mem:0811fb91-c5e0-4890-963d-b768d3bf16d9 user=SA] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@2101886654 wrapping conn0: url=jdbc:h2:mem:0811fb91-c5e0-4890-963d-b768d3bf16d9 user=SA] to manual commit
내부 트랜잭션 시작
Participating in existing transaction
내부 트랜잭션 롤백
Participating transaction failed - marking existing transaction as rollback-only
Setting JDBC transaction [HikariProxyConnection@2101886654 wrapping conn0: url=jdbc:h2:mem:0811fb91-c5e0-4890-963d-b768d3bf16d9 user=SA] rollback-only
외부 트랜잭션 커밋
Global transaction is marked as rollback-only but transactional code requested commit
Initiating transaction rollback
Rolling back JDBC transaction on Connection [HikariProxyConnection@2101886654 wrapping conn0: url=jdbc:h2:mem:0811fb91-c5e0-4890-963d-b768d3bf16d9 user=SA]
Releasing JDBC Connection [HikariProxyConnection@2101886654 wrapping conn0: url=jdbc:h2:mem:0811fb91-c5e0-4890-963d-b768d3bf16d9 user=SA] after transaction
- 외부 트랜잭션 시작
- 물리 트랜잭션을 시작한다.
- 내부 트랜잭션 시작
- Participating in existing transaction
- 기존 트랜잭션에 참여한다.
- 내부 트랜잭션 롤백
- Participating transaction failed - marking existing transaction as rollbackonly
- 내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지 않는다. 대신에 기존 트랜잭션을 롤백 전용으로 표시한다.
- 외부 트랜잭션 커밋
- 외부 트랜잭션을 커밋한다.
- Global transaction is marked as rollback-only
- 커밋을 호출했지만, 전체 트랜잭션이 롤백 전용으로 표시되어 있다. 따라서 물리 트랜잭션을 롤백한다
응답 흐름

응답 흐름 - 내부 트랜잭션
- 로직 2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백한다. (로직 2에 문제가 있어서 롤백한다고 가정한다.)
- 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 이 경우 신규 트랜잭션이 아니기 때문에 실제 롤백을 호출하지 않는다. 이 부분이 중요한데, 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다. 아직 트랜잭션이 끝난 것이 아니기 때문에 실제 롤백을 호출하면 안 된다. 물리 트랜잭션은 외부 트랜잭션을 종료할 때까지 이어져야 한다.
- 내부 트랜잭션은 물리 트랜잭션을 롤백하지 않는 대신에 트랜잭션 동기화 매니저에 rollbackOnly=true 라는 표시를 해둔다.
응답 흐름 - 외부 트랜잭션
- 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다.
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 외부 트랜잭션은 신규 트랜잭션이다. 따라서 DB 커넥션에 실제 커밋을 호출해야 한다. 이때 먼저 트랜잭션 동기화 매니저에 롤백 전용( rollbackOnly=true ) 표시가 있는지 확인한다. 롤백 전용 표시가 있으면 물리 트랜잭션을 커밋하는 것이 아니라 롤백한다.
- 실제 데이터베이스에 롤백이 반영되고, 물리 트랜잭션도 끝난다.
- 트랜잭션 매니저에 커밋을 호출한 개발자 입장에서는 분명 커밋을 기대했는데 롤백 전용 표시로 인해 실제로는 롤백이 되어버렸다.
- 이것은 조용히 넘어갈 수 있는 문제가 아니다. 시스템 입장에서는 커밋을 호출했지만 롤백이 되었다는 것은 분명하게 알려주어야 한다.
- 예를 들어서 고객은 주문이 성공했다고 생각했는데, 실제로는 롤백이 되어서 주문이 생성되지 않은 것이다.
- 스프링은 이 경우 UnexpectedRollbackException 런타임 예외를 던진다. 그래서 커밋을 시도했지만, 기대하지 않은 롤백이 발생했다는 것을 명확하게 알려준다.
정리
- 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다.
- 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시한다.
- 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인한다. 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고, UnexpectedRollbackException 예외를 던진다.
728x90
'스프링 DB 2편(데이터 접근 활용 기술)' 카테고리의 다른 글
| Ch10. 스프링 트랜잭션 전파(기본) - 다양한 전파 옵션 (0) | 2022.07.08 |
|---|---|
| Ch10. 스프링 트랜잭션 전파(기본) - REQUIRES_NEW (0) | 2022.07.07 |
| Ch10. 스프링 트랜잭션 전파(기본) - 외부 롤백 (0) | 2022.07.07 |
| Ch10. 스프링 트랜잭션 전파(기본) - 전파 기본 & 예제 (0) | 2022.07.07 |
| Ch10. 스프링 트랜잭션 전파(기본) - 트랜잭션 두 번 사용 (0) | 2022.07.07 |