Spring / / 2024. 6. 30. 22:29

Transaction Propagation

서버를 운영하다 보면 하나의 로직에서 Exception과 같이 정상적으로 수행되지 못한 경우 rollback 처리를 할 때가 빈번합니다.

왜냐하면 A -> B, C -> D 가 정상 로직인데 A -> B 만 성공하고 C -> D는 성공하지 못했을 때, A -> B 또한 다시 이전 상태로 돌려줘야 하기 때문입니다.

 

이러한 경우 우리는 대부분 @Transaction의 기본 옵션인 REQUIRED를 사용하게 되는데요 그 외의 옵션에는 무엇이 있나 이번 포스팅에서 한번 살펴보려 합니다.

 

첫 번째로는 위에서도 말한 REQUIRED 입니다.

이는 @Transaction 어노테이션을 사용했을 때, 기본적으로 적용되어 있는 옵션입니다.

현재 트랜잭션이 존재하면 그 트랜잭션을 사용하고, 없으면 새 트랜잭션을 시작합니다.

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 트랜잭션 시작
    methodB();
    // 트랜잭션 커밋 또는 롤백
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
    // methodA의 트랜잭션을 사용
}

옵션값을 REQUIRED로 나타낸 이유는 가시적으로 보여주기 위함입니다. propagation을 설정하지 않는다면 REQUIRED가 기본적으로 사용됩니다.

 

두 번째는 SUPPORTS 입니다.

현재 트랜잭션이 존재하면 그 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행됩니다.

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 트랜잭션 시작
    methodB();
    // 트랜잭션 커밋 또는 롤백
}

@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
    // methodA의 트랜잭션을 사용
}

public void methodC() {
    // 트랜잭션 없음
    methodB(); // 트랜잭션 없이 실행
}

위 주석과 같이 methodB가 methodA에서 시작되는 것을 확인하실 수 있습니다

methodA가 실행되어 트랜잭션이 생성되면, methodB가 실행될 때, Propagation이 SUPPORTS 이므로 methodA의 트랜잭션을 같이 사용하게 됩니다.

 

세 번째는 MANDATORY 입니다.

이 트랜잭션도 두 번째 옵션인 SUPPORTS와 거의 유사하게 사용되는데요 다른 점은 반드시 트랜잭션 내에서 실행되어야 하며, 현재 트랜잭션이 존재하지 않으면 IllegalTransactionStataeException 이라는 예외가 발생하게 됩니다.

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 트랜잭션 시작
    methodB();
    // 트랜잭션 커밋 또는 롤백
}

@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
    // methodA의 트랜잭션을 사용
}

public void methodC() {
    methodB(); // 예외 발생: 트랜잭션이 없음
}

 

네 번째 옵션은 REQUIRES_NEW 입니다.

이 옵션은 항상 새로운 트랜잭션으로 시작하게 됩니다. 기존 트랜잭션이 존재하면 일시 중단하고 새 트랜잭션을 실행합니다

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 트랜잭션A 시작
    methodB();
    // 트랜잭션A 재개, 커밋 또는 롤백
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 새로운 트랜잭션B 시작
    // 트랜잭션B 커밋 또는 롤백
}

이 부분에서 제가 들었던 의문점은 "methodB가 실패하면 methodA는 정상적으로 흘러갈까?" 였습니다.

앞서 말씀 드렸다시피 REQUIRES_NEW를 실행하면 methodB는 methodA와 다른 트랜잭션에서 실행됩니다. 이는 methodB의 트랜잭션이 methodA의 트랜잭션과 독립적임을 의미합니다.

따라서 논리적으로 보면 methodB가 실패하면 methodB의 트랜잭션은 롤백 처리되며, methodB의 실패로 인한 예외가 methodA로 전파됩니다. 그러나 methodA의 트랜잭션은 자동으로 롤백되지 않습니다.

하지만 methodA는 methodB를 실행했기에 methodB에서 발생한 예외가 methodA에 전파되고 이 예외를 methodA에서 적절히 처리하였다면 methodA는 롤백되지 않고 실행됩니다! 다만 methodB에서 발생한 예외를 처리하지 않으면 methodA에서도 롤백이 일어납니다(예외가 전파되기 때문입니다.)

@Service
public class ExampleService {

    @Autowired
    private AnotherService anotherService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        // methodA의 작업 수행
        try {
            anotherService.methodB();
        } catch (Exception e) {
            // methodB의 실패를 처리
            // 여기서 예외를 처리하면 methodA의 트랜잭션은 계속 진행됨
            System.out.println("methodB 실패, 하지만 methodA는 계속 진행: " + e.getMessage());
        }
        // methodA의 나머지 작업 수행
    }
}

@Service
public class AnotherService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // methodB의 작업 수행
        throw new RuntimeException("methodB 실패");
    }
}

코드를 살펴보시면 다음과 같이 나타낼 수 있습니다.

 

다음 옵션으로는 NOT_SUPPORTED가 있습니다

이는 트랜잭션 없이 실행되며, 현재 트랜잭션이 존재하면 일시 중지됩니다.

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 트랜잭션 시작
    methodB();
    // 트랜잭션 재개, 커밋 또는 롤백
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodB() {
    // 트랜잭션 없이 실행
}

트랜잭션이 없이 실행된다면 이는 DB에 commit이 즉시 이뤄짐을 의미합니다.

 

여섯번 째 옵션으로는 NEVER이 존재합니다

트랜잭션이 없이 실행되며, NOT_SUPPORTED와 다른 점은 기존 트랜잭션이 존재하면 예외(IllegalTransactionStateException)를 발생시킵니다.

public void methodA() {
    // 트랜잭션 없음
    methodB(); // 정상 실행
}

@Transactional(propagation = Propagation.NEVER)
public void methodB() {
    // 트랜잭션 없이 실행
}

@Transactional
public void methodC() {
    methodB(); // 예외 발생: 트랜잭션이 존재함
}

methodC는 @Transaction을 사용하여 기본 트랜잭션 옵션인 REQUIRED를 사용하게 됩니다. 따라서 methodC를 사용하면 Transaction이 자동으로 생성되는데, methodB의 옵션이 NEVER 이므로 이는 에러를 발생시킵니다.

 

마지막은 NESTED 옵션입니다.

현재 트랜잭션이 있으면 중첩 트랜잭션을 생성하고, 없으면 REQUIRED와 동일하게 동작합니다.

이는 외부 트랜잭션 내에 세이브포인트를 사용하여 중첩 트랜잭션을 생성하여 동작합니다.

이와 같은 중첩 트랜잭션의 특징은 부분 롤백이 가능하며, 외부 트랜잭션이 롤백되면 내부 트랜잭션인 중첩 트랜잭션 또한 롤백됩니다.

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 트랜잭션A 시작
    methodB();
    // 트랜잭션A 커밋 또는 롤백
}

@Transactional(propagation = Propagation.NESTED)
public void methodB() {
    // 중첩 트랜잭션B 시작
    // 트랜잭션B 커밋 또는 롤백
}

사용법은 다음과 같은데 확 와닿지 않습니다.

그래서 코드 하나를 더 살펴보겠습니다

@Service
public class ExampleService {

    @Autowired
    private AnotherService anotherService;

    @Transactional
    public void outerMethod() {
        System.out.println("외부 트랜잭션 시작");
        
        try {
            anotherService.nestedMethod();
        } catch (Exception e) {
            System.out.println("중첩 트랜잭션 실패, 하지만 외부 트랜잭션은 계속 진행");
        }
        
        System.out.println("외부 트랜잭션 종료");
    }
}

@Service
public class AnotherService {

    @Transactional(propagation = Propagation.NESTED)
    public void nestedMethod() {
        System.out.println("중첩 트랜잭션 실행");
        // 일부 데이터베이스 작업 수행
        throw new RuntimeException("중첩 트랜잭션 내 에러 발생");
    }
}

nestedMethod는 세이브포인트를 생성하여 중첩 트랜잭션을 생성합니다.

따라서 고의적으로 RuntimeException을 발생시켜 중첩 트랜잭션 내에서 에러가 발생했음에도 outerMethod는 정상적으로 동작하게 됩니다.

필자는 처음에 이해할 때 REQUIRES_NEW 와 굉장히 비슷하여 혼동을 많이 하였는데요

잘 보시면 REQUIRES_NEW는 새로운 트랜잭션 자체를 생성하여 각기 다른 트랜잭션이라 이해를 하면 됩니다.

중첩 트랜잭션은 outerMethod의 트랜잭션과 부모-자식 관계를 형성한 트랜잭션이라고 이해하시면 언뜻 이해하시기 편할 것이라고 생각이 듭니다.

 

이상으로 오늘은 트랜잭션 전파에 관해 포스팅 하였습니다.

항상 REQUIRED를 기본으로만 사용하고 있던 저에게 이런 옵션들에 대해 공부할 수 있어 좋았습니다.

다른분들도 건승하기를 기원합니다!


References

https://mangkyu.tistory.com/269

 

[Spring] 스프링의 트랜잭션 전파 속성(Transaction propagation) 완벽하게 이해하기

아래의 내용은 김영한님의 디비 접근 기술 2편 강의와 토비의 스프링 등을 바탕으로 정리한 내용입니다. 1. 트랜잭션의 시작과 종료 및 전파 속성(Transaction Propagation) [ 트랜잭션의 시작과 종료 ]

mangkyu.tistory.com

https://n1tjrgns.tistory.com/266

 

[Spring] @Transactional - 1 전파 레벨(propagation)

@Transactional 사용시 주의사항 @Transactional을 클래스 또는 메소드 레벨에 명시하면 해당 메소드 호출시 지정된 트랜잭션이 작동하게 된다. 단, 해당 클래스의 Bean을 다른 클래스의 Bean에서 호출할

n1tjrgns.tistory.com

 

'Spring' 카테고리의 다른 글

ThreadLocal  (2) 2024.06.09
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유