Post

@Transactional 전파 타입(Propagation) 정리

@Transactional 전파 타입(Propagation) 정리

전파 타입(Propagation) 요약

전파 타입 기존 트랜잭션 있음 기존 트랜잭션 없음
REQUIRED 기존 트랜잭션 참여 새 트랜잭션 생성
REQUIRES_NEW 기존 일시 중지 + 새 트랜잭션 생성 새 트랜잭션 생성
SUPPORTS 기존 트랜잭션 참여 트랜잭션 없이 실행
NOT_SUPPORTED 기존 일시 중지 + 트랜잭션 없이 실행 트랜잭션 없이 실행
MANDATORY 기존 트랜잭션 참여 예외 발생
NEVER 예외 발생 트랜잭션 없이 실행

@Transactional 전파(Propagation)란?

@Transactional 전파(Propagation)는 트랜잭션 메서드가 호출될 때 현재 트랜잭션 존재 여부에 따라
기존 트랜잭션에 참여할지
새로운 트랜잭션을 생성할지
트랜잭션 없이 실행할지
결정하는 규칙입니다.

예를 들어서, A 메서드에서 B 메서드를 호출할 때
A의 트랜잭션을 그대로 사용할지 아니면 B에서 새로운 트랜잭션을 생성할지를 정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
@Transactional
public void methodA() {
    // A의 트랜잭션이 존재하는 상태
    methodB();
}

@Transactional(propagation = Propagation.???)
public void methodB() {
    // 전파 타입에 따라 트랜잭션 동작이 달라진다
}

왜 전파 타입을 알아야 하는가?

기본적으로 @Transactional의 전파 타입은 REQUIRED입니다.

1
2
3
4
5
6
/**
 * The transaction propagation type.
 * <p>Defaults to {@link Propagation#REQUIRED}.
 * @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()
 */
Propagation propagation() default Propagation.REQUIRED;

대부분의 경우 기본값(REQUIRED)으로 충분하지만
아래와 같은 상황에서는 전파 타입을 변경해야 합니다.

  • 로그 기록 : 비즈니스 로직이 실패해도 로그는 반드시 저장해야 할 때
  • 외부 API 호출 : 외부 API 호출 결과를 별도로 관리해야 할 때

전파 타입 종류

REQUIRED (기본값)

기존 트랜잭션이 있으면 참여하고 없으면 새로 생성합니다.
가장 많이 사용되는 기본 전파 타입입니다.

1
2
3
4
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(OrderRequest request) {
    orderRepository.save(request.toEntity());
}

주의할 점: 기존 트랜잭션에 참여했을 때 내부 메서드에서 예외가 발생하면 전체 트랜잭션이 롤백됩니다.

REQUIRES_NEW

항상 새로운 트랜잭션을 생성하고, 기존 트랜잭션은 일시 중단합니다.

1
2
3
4
5
// logService
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
    logRepository.save(new Log(message));
}
  • 기존 트랜잭션 있음 : 기존 트랜잭션 일시 중단 + 새 트랜잭션 생성
  • 기존 트랜잭션 없음 : 새 트랜잭션 생성
1
2
3
4
5
6
7
8
9
@Transactional
public void processOrder(OrderRequest request) {
    orderRepository.save(request.toEntity()); // 주문 저장

    logService.saveLog("주문 생성"); // 별도 트랜잭션으로 실행

    // 여기서 예외 발생해도 로그는 이미 커밋됨
    validateOrder(request);
}

즉, REQUIRES_NEW는 기존 트랜잭션과 완전히 독립적이기 때문에 내부 트랜잭션의 커밋/롤백이 외부 트랜잭션에 영향을 주지 않습니다.

REQUIRES_NEW 주의사항

완전히 독립적이기 때문에 에러가 발생해도 이미 커밋되어 롤백 불가능하기 때문에 데이터 정합성 문제를 유발시킬 수 있습니다.

또한, DB커넥션 고갈 위험도 있습니다.
REQUIRES_NEW는 호출되면 기존 트랜잭션의 커넥션을 유지한 상태로 새로운 커넥션을 추가로 획득합니다.

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void processOrder(OrderRequest request) {
    // 커넥션 1개 사용 중

    logService.saveLog("주문 생성");
    // 기존 커넥션 보유 + 새 커넥션 1개 추가 = 총 2개 점유

    notificationService.sendNotification(request);
    // sendNotification 메서드도 REQUIRES_NEW 일 때,
    // 1개 추가 = 총 3개 점유 가능
}

즉, 하나의 요청이 동시에 2개 이상의 커넥션을 점유하게 됩니다.

너무 자주 사용할 시에는 오버헤드가 발생할 수 있습니다.

1
2
3
4
5
6
7
@Transactional
public void batchProcess(List<Item> items) {
    for (Item item : items) {
        // 1000개 아이템이면 1000개의 트랜잭션 생성 + 커넥션 획득/반환 반복
        itemService.processItem(item); // REQUIRES_NEW
    }
}

왜냐하면 트랜잭션을 새로 생성할 때마다 커넥션 획득/반환 비용, 트랜잭션 시작/커밋 비용(BEGIN/COMMIT), 기존 트랜잭션 일시 중단(suspend) 등 오버헤드를 일으켜 성능을 저하시키기 때문입니다.

SUPPORTS

기존 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행합니다.

1
2
3
4
@Transactional(propagation = Propagation.SUPPORTS)
public List<Order> getOrders() {
    return orderRepository.findAll();
}
  • 기존 트랜잭션 있음 : 기존 트랜잭션에 참여
  • 기존 트랜잭션 없음 : 트랜잭션 없이 실행

NOT_SUPPORTED

트랜잭션 없이 실행하고, 기존 트랜잭션이 있으면 일시 중단합니다.

1
2
3
4
5
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendNotification(String userId) {
    // 트랜잭션 없이 실행
    notificationClient.send(userId, "알림 메시지");
}
  • 기존 트랜잭션 있음 : 기존 트랜잭션 일시 중단 + 트랜잭션 없이 실행
  • 기존 트랜잭션 없음 : 트랜잭션 없이 실행

NOT_SUPPORTED 도 마찬가지로 REQUIRES_NEW 처럼
기존 트랜잭션이 일시 중단(suspend)되면 해당 커넥션은 반환되지 않고 점유된 채
존재하므로 주의해야 합니다.

MANDATORY

반드시 기존 트랜잭션이 필수이고, 없으면 예외가 발생합니다.
항상 트랜잭션 내에서만 호출되어야 하는 메서드에 사용하면
실수로 트랜잭션 없이 호출하는 상황을 방지할 수 있습니다.

1
2
3
4
5
6
@Transactional(propagation = Propagation.MANDATORY)
public void deductStock(Long productId, int quantity) {
    Product product = productRepository.findById(productId)
            .orElseThrow();
    product.deductStock(quantity);
}
  • 기존 트랜잭션 있음 : 기존 트랜잭션에 참여
  • 기존 트랜잭션 없음 : IllegalTransactionStateException 발생
1
2
// 트랜잭션 없이 호출하면 예외 발생!
deductStock(1L, 10); // IllegalTransactionStateException

NEVER

MANDATORY와 정반대 개념으로 트랜잭션이 있으면 예외가 발생하고, 트랜잭션 없이만 실행 가능합니다.

1
2
3
4
@Transactional(propagation = Propagation.NEVER)
public String checkHealthStatus() {
    return "OK";
}
  • 기존 트랜잭션 있음 : IllegalTransactionStateException 발생
  • 기존 트랜잭션 없음 : 트랜잭션 없이 실행
This post is licensed under CC BY 4.0 by the author.