[Spring] @Transactional 롤백이 안돼요! or 롤백을 해버려요!🚨

누구세요·2024년 11월 13일

Spring은 마법같다.🪄 너무나도 사용하기 쉽다.
대신 모르면 잘못 사용해서 문제를 일으키기도 쉽다는 함정이 있다.

3줄 요약 간단 흐름🐳

  • 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에 발생합니다.
  • 쉽게말하면 commit or rollback이 호출되기 전까지 하나의 트랜젝션으로 묶인다.
  • Exception이 발생하면 rollback을 수행하고, 정상적으로 로직을 수행하면 commit이 이루어진다.

Rollback관련 주의사항

1. 기본 주의사항

  • @Transactionalpublic함수에서만 작동한다.
  • try-catch문을 사용하면 catch문에 rollback 동작을 위임하기 때문에 rollback이 발생하지 않는다.
  • @Transactional은 전파된다. 부모 메서드에 트랜젝션이 달려있다면 종속된다.

2. Checked Exception과 Unchecked Exception

  • Unchecked Exception
    • RuntimeException 계열의 예외는 기본적으로 호출 스택을 따라 최상위까지 전파된다.
    • REQUIRES_NEW로 생성된 트랜잭션에서 예외가 발생하면, 상위 트랜잭션으로 전파되어 상위 트랜잭션까지 롤백된다.
  • Checked Exception
    • 롤백을 수행하지 않는다.
    • IOException, NullPointException과 같은 예외를 말한다.
    • Checked exception의 경우 try-catch 블록으로 감싸서 처리해야 하지만, 처리하지 않고 그대로 전파하면 상위 트랜잭션에도 영향을 미칠 수 있다.
    • 롤백을 수행하려면 @Transactional(rollbackFor = FileNotFoundException.class)와같이 rollbackFor을 지정해줘야한다.

3. REQUIRES_NEW 옵션은 완벽하게 독립적인 트랜젝션이 아니다.

  • 스프링 트랜잭션의 독립이란 물리적인것이 아닌 논리적 독립이다.
  • 실제로 REQUIRES_NEW로 시작한 트랜젝션도 현재 트랜젝션과 동일한 스레드에서 진행된다.
  • 따라서 REQUIRES_NEW에서 발생한 예외는 이를 호출한 트랜젝션에 전파가 된다.
  • REQUIRES_NEW옵션을 사용하더라도 같은 class내의 함수를 호출할 경우 하나의 트랜젝션으로 간주한다.

아래는 REQUIRES_NEW를 이용하여 다룬 예시 코드이다.

(공통 조건)

  • 부모는 기본옵션, 자식은 REQUIRES_NEW옵션을 주었다.
  • UneckedException을 발생시킨다.
  • 부모와 자식 메서드는 다른 class이다.

부모쪽 코드 (ManagerService class)

@Transactional
public void parentMethod() {
   managerLogService.save("parent");
   managerLogService.childMethod();
}

자식 코드 (ManagerLogService class)

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() throws IOException {
   save("child");
}

public void save(String msg) {
   ManagerLog log = ManagerLog.builder()
           .result(msg)
           .build();
   managerLogRepository.save(log);
}

CASE 1

  1. 부모와 자식 메서드 모두 try-catch문이 없다.
  2. 자식 쪽에서 Exception이 발생한다.

부모 코드

@Transactional
public void parentMethod() {
    managerLogService.save("parent");
    managerLogService.childMethod();
}

자식 코드

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() {
    save("child");
    throw new InvalidRequestException("error");
}

자식 메서드에서 예외가 발생하여 롤백된다.
부모 메서드에도 예외가 전파되어 롤백된다.

CASE 2

  1. 부모와 자식 메서드 모두 try-catch문이 없다.
  2. 부모 쪽에서 Exception이 발생한다.

부모 코드

@Transactional
public void parentMethod() {
    managerLogService.save("parent");
    managerLogService.childMethod();
    throw new InvalidRequestException("error");
}

자식 코드

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() {
    save("child");
}

자식 메서드는 정상처리되어 커밋된다.
부모 메서드는 예외가 발생하여 롤백된다.

CASE 3

  1. 자식 메서드 쪽에 try-catch가 있다.
  2. 자식 쪽에서 Exception이 발생한다.

부모 코드

@Transactional
public void parentMethod() {
    managerLogService.save("parent");
    managerLogService.childMethod();
}

자식 코드

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() {
    try{
        save("child");
        throw new InvalidRequestException("error");
    } catch (InvalidRequestException ex) {
        System.out.println(ex.getMessage());
    }
}

자식 메서드에 catch문이 있으므로 정상 처리되어 커밋된다.
부모 메서드도 커밋된다.

CASE 4

  1. 부모 메서드 쪽에 try-catch가 있다.
  2. 자식 쪽에서 Exception이 발생한다.

부모 코드

@Transactional
public void parentMethod() {
    try {
        managerLogService.save("parent");
        managerLogService.childMethod();
    }catch (InvalidRequestException ex) {
        System.out.println(ex.getMessage());
    }
}

자식 코드

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() {
    save("child");
    throw new InvalidRequestException("error");
}

자식 메서드는 예외가 발생하여 롤백된다.
부모 메서드는 catch로 잡아 정상커밋된다.

CASE 5

  1. 부모 메서드 쪽에 try-catch가 있다.
  2. 부모 쪽에서 Exception이 발생한다.

부모 코드

@Transactional
public void parentMethod() {
    try {
        managerLogService.save("parent");
        managerLogService.childMethod();
        throw new InvalidRequestException("error");
    }catch (InvalidRequestException ex) {
        System.out.println(ex.getMessage());
    }
}

자식 코드

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() {
    save("child");
}

자식 메서드는 정상처리되어 커밋된다.
부모 메서드에서 예외가 발생했지만 catch문이 있으므로 정상 처리되어 커밋된다.

참고

REQUIRES_NEW 옵션과 Try-Catch

0개의 댓글