트랜잭션 매니저 Commit은 어떻게 작동할까

Panda·2023년 12월 8일
1

Spring

목록 보기
40/42

실무에서 트랜잭션때문에 UnexpectedRollbackException 발생하는 이슈가 있었는데
트랜잭션 매니저에 대해서 이해도가 떨어진다는 것을 느끼고 공부해 보았습니다.

트랜잭션 매니저 Commit

먼저 트랜잭션을 commit을 하게 되면
TransactionStatus에 있는 트랜잭션에 대한 상태값을 가지고 수행을 하게 됩니다.

크게는 3가지를 수행하게 되는데요. 수행 동작은 아래와 같습니다.

  • isCompleted (이미 완료된 상태)
    • IllegalTransactionStateException 발생
  • Local or Global 롤백 상태에 따른 롤백 처리
    • unexpectedRollback 차이
  • commit 처리

또한 TransactionStatus에는 rollbackOnly 라는 롤백 모드인지 판별하는 필드가 있는데
일단 여기서 중요하게 봐야 할 것은 Local, Global에 따른 롤백 상태를 분리해 둔 것입니다.

  • Local 롤백 상태 : TransactionStatus 에 존재하는 rollbackOnly 필드
  • Global 롤백 상태 : 트랜잭션 매니저에 존재하는 isRollbackOnly 메소드

예를들어 모든 트랜잭션이 PROPAGATION_REQUIRED 라고 가정하고
A -> B -> C로 호출을 한다면
전체적으로는 하나의 트랜잭션이지만 A가 생성한 트랜잭션에 B, C 트랜잭션이 참여한다고 보면 됩니다.

그렇기 때문에

  • A, B, C 에 각각 해당하는 롤백 상태 (TransactionStatus 롤백 상태)
  • 전체 트랜잭션에 해당하는 롤백 상태 (트랜잭션 매니저 롤백 상태)

가 존재한다고 보면 될 것 같습니다.

UnexpectedRollbackException 발생

실무에서 @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를 이용한 해당 구조에서 발생하는 이유가 뭐냐면

  1. test2에서 발생한 Exception으로 인해 전체 트랜잭션 롤백마크가 설정이 되고
    test2 트랜잭션에서 바로 전체 롤백이 수행이 아닌 rollbackToHeldSavepoint 실행 (최초 트랜잭션이 아니기 때문에)
  2. 이후 test1에서 catch가 되었지만 추가적인 Exception을 발생 시킨게 아니라서
    test1 트랜잭션에서는 processCommit을 수행을 하였고 이후 status.isGlobalRollbackOnly() 가 true로 설정이 되어있기 때문에 해당 UnexpectedRollbackException 발생

결국은 트랜잭션에 대한 낮은 이해도 + 의미 없는 try ~ catch 에 남용으로 인한 콜라보레이션으로 에러가 발생한거였습니다.
(핑계를 대보자면 제가 짠 코드가 아닌 레거시 코드였다는점? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ)

해결

예제에서는 의미 없는 try ~ catch를 제거하면 되기는 하지만
실무코드에서는 catch때 해주는 동작이 있었기 때문에 해당 동작을 전부 수행하고 나면 마지막에 한번더 Exception을 발생시켜서 processCommit 이 아닌 processRollback 를 수행하도록 해결하였습니다.

느낀 점

트랜잭션에 대해서는 어느정도 안다고 생각했지만
트랜잭션 매니저가 어떻게 작동하는지 제대로 몰라서 발생한 문제였네요
항상 안다고 생각하는 것들은 막상 이슈가 나오면 항상 모르는 것 같습니다.

@Transational AOP 의 편리함때문에 트랜잭션 매니저를 직접 건드리는 일이 잘 없다 보니까 이러한 이해부족이 나타난 것 같습니다.

인터넷에 정보도 잘 없고 자세한 내용 알려주는 곳이 거의 없었는데
AbstractPlatformTransactionManager
트랜잭션 매니저 구현체들 코드 까봐서 이해한게 8할 정도 인 듯 싶네요
요즘은 인터넷 봐서 해결하는 것보다 코드를 직접 까보고 이해하는게 더 쉽고 빠른 것 같다고 느끼는 중입니다.

참고

profile
실력있는 개발자가 되보자!

0개의 댓글