현재 내가 개발을 하고 있는 프로젝트에서 팀원 분이 다음과 같은 이슈가 발생한다고 하셨다.
외부(최상단) 트랜잭션에서 a,b,c라는 로직을 수행하고 d라는 알림톡 발송 로직을 수행하고 있었는데, d라는 알림톡 발송 로직이 실패하더라도 외부 트랜잭션에서 호출하는 a,b,c 로직에 대해서는 정상적으로 커밋이 되길 바라셨는데 롤백이 되었던 이슈였다.
따라서 나는 알림톡 발송 로직을 수행하는 d의 트랜잭션 전파 옵션을 REQUIRES_NEW로 바꾸어서 테스트를 해보았는데도 a,b,c 로직은 여전히 롤백되었다..
외부 트랜잭션 -> d(알림톡 발송로직 및 기타 로직) -> (실제 알림톡 발송 로직) 이렇게 비즈니스 로직이 구성되어있었는데, 이것에 대해 각각 A,B,C라고 언급하도록 하겠다.
작성된 코드를 보니, C에서 예외가 발생하면 try-catch로 잡고 있었고, 그것을 커스텀예외로 변환해서 B로 던지고 있었다.
그리고 B는 try-catch로 해당 예외를 잡고, 로그만 남기고 A로는 던지지 않고 있었다. 따라서 나는 A에는 예외가 던져지지 않고, B에서 예외가 모두 잡혀 A에 대한 로직은 정상적으로 커밋이 되어야한다고 생각했다. 하지만 이게 웬걸.. 계속 커밋이 되지 않고 UnExpectedRollbackException이 계속 발생하는 것이었다. 나는 원인이 뭐지 싶어서 구글링을 계속 해보았는데 다음 링크와 같은 글을 찾을 수 있었다.
REQUIRES_NEW인데 rollback되는 이유가 궁금합니다. - 인프런 | 커뮤니티 질문&답변
누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.
www.inflearn.com
김영한님이 답변하신 내용은 다음과 같다.
A(REQUIRED)
B(REQUIRES_NEW) -> C(REQUIRED)
결과적으로 A와, B 2개의 물리 트랜잭션이 존재하는 것이지요. C의 경우에는 B에 포함된 논리적인 트랜잭션입니다.
이 경우 C에서 롤백 마킹을 하게 됩니다. 그러면 B에서 커밋을 하는 순간에 UnexpectedRollbackException이 발생하게 됩니다.
그런데 이 예외가 AOP에서 발생하기 때문에 [A] -> [B의 AOP] -> [B]와 같이 됩니다.
결과적으로 [B의 AOP]에서 UnexpectedRollbackException을 던집니다.
A가 이 예외를 명확하게 잡아서 예외를 제거하면 A를 정상 커밋할 수 있습니다.
A가 이 예외를 잡지 않고 그냥 둔다면 A도 예외가 발생했기 때문에 트랜잭션이 롤백됩니다.
음.. 그런데 사실 내가 보고 있던 코드는 A에서 예외를 잡고 있기 때문에 A가 정상 커밋이 되어야했는데 여전히 되고 있지 않았다. 정말 여기서부터는 머리가 하얘졌고, 강의도 다시 돌려보고 구글링도 계속 해봤는데 이해가 정말 가지 않았다.
그러다가, 한 줄기 빛과 같은 글을 보았는데, 실제로 한 클래스 안에서 @Transactional 어노테이션을 단 다른 메서드를 호출해도 계속 같은 트랜잭션을 사용을 한다는 것이었다.
Spring은 메서드 또는 클래스에 @Transactional이 달린 클래스들에 대한 proxy를 생성하고, 트랜잭션 적용을 위해 프록시를 통해 호출하는 메서드만 인터셉트한다. 동일한 bean에 있는 메서드라면 프록시를 통해 호출하는 것이 아니다. 즉, 내부 메서드를 호출한다는 것은 this.메서드()를 호출한 것과 같은 의미이기 때문에 프록시가 적용되지 않아 같은 트랜잭션을 사용하게 된다.
해결 방법으로는 다음과 같다.
Service(클래스)를 분리해서 새롭게 Service를 스프링 빈으로 등록하고 해당 Service내에 B를 두고, A에서 B를 호출하도록 하는 것이었다. 물론 여기서도 REQUIRES_NEW는 적용해야한다.
실제로 이렇게 하면, A에서 catch (Exception e) 을 타고, C에서 발생한 예외로 인한 B의 nested exception is org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only가 로그로 남는 걸 알 수있다. 그리고 이 예외를 catch 했기 때문에 A는 정상적으로 커밋, B,C만 롤백되는 것을 알 수 있다.
참고로 다른 해결 방법도 있는데, 그 방법은 이 링크를 참고하시길 바랍니다!
📕 참고
https://yeonyeon.tistory.com/283
[Spring] @Transactional이 동작하지 않는다?😨
부제: 동일한 bean에서는 @Transactional 적용이 되지 않는다. 핵심 내용 바로가기 😎 문제 상황 줍줍은 자꾸만 사라지는 슬랙 메시지를 백업해주는 서비스입니다. 최근 줍줍에서는 신규 이용자들의
yeonyeon.tistory.com
@Transactional propagation이 동작하지 않는 문제
모든 코드는 github 에 있습니다. 들어가기 앞서... 다음과 같은 서비스 메서드가 있을 때, @RequiredArgsConstructor @Service public class HumanHandler { private final HumanRepository humanRepository; //passOneyear을 3번 호출
ocblog.tistory.com
'개발기록(feat.삽질)' 카테고리의 다른 글
| log4jdbc로 JPA 쿼리 로그 Formatter 적용하기 (0) | 2025.06.26 |
|---|---|
| Pageable 동작 방식 (2) | 2025.01.03 |
| JPA Bulk Insert (1) | 2024.12.13 |
| [Querydsl] java.time.LocalDateTime is not compatible with java.lang.String 에러 (0) | 2024.10.23 |
| Jackson사용 시 primitive boolean주의점 (0) | 2024.08.19 |