오늘은 트랜잭션 전파에 대해서 알아보겠습니다.
이 글은 김영한님의 스프링 DB 2 / 스프링 공식 문서(data-access tx-propagation)를 정리한 내용입니다.
트랜잭션 전파
트랜잭션을 각각 사용하는 것이 아니라, 트래잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행하면 어떻게 될까?
기존 트랜잭션과 별도의 트랜잭션을 진행해야 할까? 아니면 기존 트랜잭션을 그대로 이어 받아서 트랜잭션을 수행해야 할까? 이런 경우 어떻게 동작할지 결정하는 것을 트랜잭션 전파라 한다.
아래 설명은 전파 REQUIRED 기준으로 설명합니다.
스프링 공식 문서/영한님의 강의 모두 트랜잭션을 논리적, 물리적 트랜잭션으로 나눈다.
위 그림에서 파란색 박스에 둘러쌓인 것이 물리적 트랜잭션
빨간색 박스로 둘러쌓인 것이 논리적 트랜잭션을 의미합니다.
전파 옵션이 REQUIRED면 각각의 트랜잭션 메소드에 논리적 트랜잭션을 만듭니다.
REQUIRED는 심플하다. 내부 트랜잭션(Method2)은 외부 트랜잭션(Method1)에 참여한다.
참여라는 의미를 이해하는 것이 매우 중요하다. 상단 그림 Method 2 exxcutes in the existing transaction 라는 문구가 있다. 해석하면 존재하고 있는 트랜잭션에 참여한다는 것입니다.
즉, 새 트랜잭션을 만들지 않는다. Method1에서 만든 트랜잭션 참여하고 이에 따라 트랜잭션 옵션도 따라갑니다. 예를 들어 Method2 에서 readOnly = true 라는 옵션을 두더라도 Method1 트랜잭션의 옵션이 readOnly = false 라면 이를 따라갑니다.
다른 관점에서 보면 트랜잭션의 범위가 넓어지는 것을 의미한다. 원래라면 Method1 까지만 트랜잭션의 범위였다면 이제는 Method1 ~ Method2 까지 트랜잭션의 범위가 넓어졌다. 정리하면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것입니다.
실제 코드를 실행시켜 살펴봅시다.
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // 처음 실행된 트랜잭션인지 확인
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", inner.isNewTransaction());
결과
내부 트랜잭션이 시작됐을 때는 새로운 트랜잭션이 생성되지 않을 것을 볼 수 있습니다.
이번에는 내부 트랜잭션에 커밋을 해봅시다.
log.info("내부 트랜잭션 커밋");
txManager.commit(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
내부 트랜잭션을 커밋해도 어떠한 로그가 남지 않는다. 실제 커밋이 되지 않은 것이다. 그리고 외부 트랜잭션이 커밋될 때 커밋되게 됩니다.
REQUIRED 옵션은 이제 어떤 의미인지 알게 되었습니다. 그러면 물리 트랜잭션이 등장한 이유는 무엇일까요?
예를 들어 내부 트랜잭션에서 예외가 발생했다고 해봅시다. 내부 트랜잭션을 커밋/롤백할지 선택해야 합니다. 그리고 외부 트랜잭션도 커밋/롤백해야 할지 결정해야 합니다. 이것처럼 복잡한 상황이 발생한다. 그래서 단순한 원칙을 만들기 위해
물리 트래잭션이 등장했다.
원칙
1. 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
2. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
즉, 내부 커밋/외부 롤백 혹은 내부 롤백/외부 커밋 된다면 물리트랜잭션은 롤백된다.
여기서 한 가지 고민해볼 거리가 있는데 내부 트랜잭션이다. 내부 트랜잭션은 외부 트랜잭션에 참여한다고 했다. 그래서 커밋을 해도 별 반응이 없었다. 롤백을 한다고 뭐가 달라질까? 만약 내부 트랜잭션에서 롤백을 한다면 큰일난다. 트랜잭션 전파가 끊기게 된다. 그렇다면 어떻게 스프링에서는 내부 트랜잭션 롤백 시, 외부 트랜잭션을 롤백할 수 있을까?
아래 코드와 로그를 통해 확인해봅시다.
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
다음과 같은 결과가 나옵니다.
rollback-only 가 마크되어 있으니 롤백한다. 그렇다면 예외는 왜 던지는 것일까?
영한님에 강의에서는 이렇게 설명한다. 예외를 던지지 않으면 개발자가 이게 롤백된건지 커밋된거지 인지하기 어렵다. 그래서 예외를 던진다.
공식문서에서도 비슷하게 말한다.
내부 트랜잭션에 롤백 마크를 남긴다. 그러나 외부 트랜잭션이 롤백을 결정하지 않는다면 그것은 예상치 못한 것(개발자가 예상하지 못한 것)이다. 그래서 UnexpectedRollbackExcetion 을 던진다. 아래는 이에 대한 부연설명이다.
결론적으로, 내부 트랜잭션이 롤백될 때 rollback mark를 남긴다. 그리고 외부 트랜잭션을 결정하기 직전에 내부 트랜잭션에 rollback mark가 찍혀있는지 확인하고 하나도 없다면 커밋한다. 이것이 이해되지 않는다면 물리 트랜잭션 롤백/커밋 조건을 다시 확인해보면 좋을 것 같다.
'항해99' 카테고리의 다른 글
항해99 81일차 TIL1 - 최종 프로젝트 더 개선하기 - 일정 생성 (2) (0) | 2023.03.17 |
---|---|
항해99 80일차 TIL1 - 나는 개발을 좋아할 수 있다. 좋아한다. 좋아하고 싶다. (1) | 2023.03.16 |
항해99 78일차 TIL1 - 최종 프로젝트 더 개선하기 - 일정 생성 (1) (0) | 2023.03.13 |
항해99 77일차 TIL1 - @Transactional(readOnly = true) (0) | 2023.03.11 |
항해99 76일차 TIL1 - 정적 팩토리 메서드 (0) | 2023.03.10 |