실무에서 트랜잭션때문에 UnexpectedRollbackException
발생하는 이슈가 있었는데
트랜잭션 매니저에 대해서 이해도가 떨어진다는 것을 느끼고 공부해 보았습니다.
먼저 트랜잭션을 commit을 하게 되면
TransactionStatus에 있는 트랜잭션에 대한 상태값을 가지고 수행을 하게 됩니다.
크게는 3가지를 수행하게 되는데요. 수행 동작은 아래와 같습니다.
IllegalTransactionStateException
발생unexpectedRollback
차이또한 TransactionStatus에는 rollbackOnly
라는 롤백 모드인지 판별하는 필드가 있는데
일단 여기서 중요하게 봐야 할 것은 Local, Global에 따른 롤백 상태를 분리해 둔 것입니다.
rollbackOnly
필드isRollbackOnly
메소드예를들어 모든 트랜잭션이 PROPAGATION_REQUIRED 라고 가정하고
A -> B -> C로 호출을 한다면
전체적으로는 하나의 트랜잭션이지만 A가 생성한 트랜잭션에 B, C 트랜잭션이 참여한다고 보면 됩니다.
그렇기 때문에
가 존재한다고 보면 될 것 같습니다.
실무에서 @Transactional
무지성으로 걸다가 다른 기능에 대해서 테스트를 하고 있었는데
UnexpectedRollbackException
이 발생하면서 롤백이 되는 현상을 발견하였습니다.
여기서 문제는
롤백이 실행되는 것은 당연히 맞는 상황인데
UnexpectedRollbackException
이 발생하면서 이후 처리를 아무것도 못하는 현상이 발생하였습니다.
그럼 왜 UnexpectedRollbackException
이 발생하였을까요??
해당 문제가 발생한 실무 코드랑 비슷한 구조로 코드를 작성해보았습니다.
아래와 같습니다.
public class A {
@Transactional
public void operate(Long id) {
Domain domain = findById(id);
B.test1(domain);
// Hello World 출력 안됨
System.out.println("Hello World!");
}
}
public class B {
@Transactional
public void test1(Long id) {
try {
Domain domain = findById(id);
C.test2(domain);
} catch (Exception e) {
log.error("error occured : ", e)
}
}
}
public class C {
@Transactional(rollbackFor = Exception.class)
public void test2(Domain domain) {
domain.plus();
throw new RuntimeException("throw error");
}
}
원하는 동작은 test2
에서 Exception이 발생해도 단순 에러 로그만 찍고 넘어가고
이후 Hello World 를 출력해보려고 합니다.
하지만 실제 코드를 실행해보면 Hello World는 출력이 안되고
test1
메소드가 종료되자마자
UnexpectedRollbackException
이 발생합니다.
그렇다면 대체 어디서 해당 Exception이 발생한걸까요??
원인은 바로 processCommit
메소드에 있었습니다.
try 부분만 살펴보겠습니다.
해당 코드를 보시면 hasSavepoint (자기가 최초 트랜잭션 X) or isNewTransaction (자기가 최초 트랜잭션) 아니면 그외 실패든
unexpectedRollback = status.isGlobalRollbackOnly();
상태를 저장시키고 해당 값이 true면 UnexpectedRollbackException
를 발생하는 것을 알 수 있습니다.
해당 발생 케이스가 try ~ catch를 이용한 해당 구조에서 발생하는 이유가 뭐냐면
rollbackToHeldSavepoint
실행 (최초 트랜잭션이 아니기 때문에)processCommit
을 수행을 하였고 이후 status.isGlobalRollbackOnly()
가 true로 설정이 되어있기 때문에 해당 UnexpectedRollbackException
발생결국은 트랜잭션에 대한 낮은 이해도 + 의미 없는 try ~ catch 에 남용으로 인한 콜라보레이션으로 에러가 발생한거였습니다.
(핑계를 대보자면 제가 짠 코드가 아닌 레거시 코드였다는점? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ)
예제에서는 의미 없는 try ~ catch를 제거하면 되기는 하지만
실무코드에서는 catch때 해주는 동작이 있었기 때문에 해당 동작을 전부 수행하고 나면 마지막에 한번더 Exception을 발생시켜서 processCommit
이 아닌 processRollback
를 수행하도록 해결하였습니다.
트랜잭션에 대해서는 어느정도 안다고 생각했지만
트랜잭션 매니저가 어떻게 작동하는지 제대로 몰라서 발생한 문제였네요
항상 안다고 생각하는 것들은 막상 이슈가 나오면 항상 모르는 것 같습니다.
@Transational
AOP 의 편리함때문에 트랜잭션 매니저를 직접 건드리는 일이 잘 없다 보니까 이러한 이해부족이 나타난 것 같습니다.
인터넷에 정보도 잘 없고 자세한 내용 알려주는 곳이 거의 없었는데
AbstractPlatformTransactionManager
랑
트랜잭션 매니저 구현체들 코드 까봐서 이해한게 8할 정도 인 듯 싶네요
요즘은 인터넷 봐서 해결하는 것보다 코드를 직접 까보고 이해하는게 더 쉽고 빠른 것 같다고 느끼는 중입니다.