본문 바로가기
프로그래밍 실습/Spring

marked as rollback-only Exception 해결기

by wylee 2023. 1. 13.

어느날 회사에서 로직을 짜던 도중

!! Transaction silently rolled back because it has been marked as rollback-only !! 라는 UnexpectedRollbackException을 마주쳤다.

 

 

사건의 발단은 이러했다.

@Transactional 어노테이션으로 감싼 메서드 안에 다른 서비스에 있는 @Transactional 어노테이션으로 감싼 메서드를 호출했다.

코드의 구조는 아래와 같다.

@Component
@Transactional
public class OuterClass {

    private final InnerClass innerClass;

    public OuterClass(InnerClass innerClass) {
        this.innerClass = innerClass;
    }

    public void outerMethod() {
        try {
            innerClass.innerMethod();
        }catch (Exception e){
//            e.printStackTrace();
        }
    }
}

@Component
@Transactional
public class InnerClass {

    public void innerMethod() {
        throw new RuntimeException("Inner Method");
    }

}

outerMethod에서 innerMethod를 호출했을때 innerMethod에서 Exception이 발생해도 outerMethod는 Exception을 catch할 것이고 outerMethod의 Tx는 정상적으로 commit 될 것이라 생각했다.

그러나 innerMethod에서 Exception이 발생하면 it has been marked as rollback-only라는 설명과 함께 UnexpectedRollbackException이 발생한다.

 

 

왜 이런일이 발생했을까?

 

 

해당 에러 로그에 대해 알아보던 도중 좋은 포스팅을 만났다.

https://techblog.woowahan.com/2606/

 

응? 이게 왜 롤백되는거지? | 우아한형제들 기술블로그

{{item.name}} 이 글은 얼마 전 에러로그 하나에 대한 호기심과 의문으로 시작해서 스프링의 트랜잭션 내에서 예외가 어떻게 처리되는지를 이해하기 위해 삽질을 해본 경험을 토대로 쓰여졌습니다.

techblog.woowahan.com

 

@Tx에서 Exception이 터지면 롤백 마크를 해서 해당 트랜잭션을 재사용하는게 불가능하다는 김영한 강사님의 말씀!

 

@Transaction의 기본 propagation 속성은 PROPAGATION_REQUIRED이고 이 속성은 어떤 트랜잭션 안에서 트랜잭션을 열려고 시도할 경우 기존 트랜잭션에 참여한다.

새로운 트랜잭션이 실패할 경우 기존 트랜잭션을 전역적으로 rollback-only로 마킹할 것인지  결정하는 속성(globalRollbackOnParticipationFailure)이 있다. 이 속성의 디폴트 값은 true이다.

 

 

결론적으로 innerMethod의 트랜잭션은 기존 outerMethod 트랜잭션에 참여한 것이고 Exception이 발생했을때 globalRollbackOnParticipationFailure 값에 따라 기존 트랜잭션(outerMethod의 트랜잭션)을 rollback-only로 마킹 해버린것이다. 그래서 기존 트랜잭션을 commit 하려고 할때 rollback-only가 마킹되어 있는 것을 확인하고 Transaction silently rolled back because it has been marked as rollback-only 라는 에러가 발생하는 것이다!

 

 

이 오류를 해결하기 위해선 어떻게 해야 할까?

@Transactional의 propagation 속성을 PROPAGATION_REQUIRES_NEW로 사용

=> 기존 트랜잭션에 참여하지 않고 완전히 새롭고 독립적인 트랜잭션이 시작됨 

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void propRequiresNew() {
    System.out.println("inner propRequiresNew start");
    throw new RuntimeException("Propagation.REQUIRES_NEW Inner Class");
}

위와 같이 REQUIRES_NEW 속성을 사용하면 새로운 트랜잭션이 실행되기 때문에 rollback-only 에러가 발생하지 않는다.

 

 

그러나 포스팅 아래에 "그런데, 트랜잭션 안에서 RuntimeException은 왜 잡으려고 했나요?" 라는 말을 보고 나도 머리가 띵해졌다.

트랜잭션 안에서의 RuntimeException은 잡으려 하면 안되는 것일까? 자연스럽게 Tx가 롤백되도록 놔두어야 하는 것일까? 그렇다면 트랜잭션이 없는 곳에서 Exception 처리를 해야 하는 것인가??

 

 

공부를 하면 할수록 어렵고,, 헷갈린다,,,

언제쯤 나도 멋진 개발자가 될 수 있을까! 

댓글