트랜잭션 전파 옵션 이해

리본24·2025년 2월 5일

Spring

목록 보기
5/7
post-thumbnail

1. 트랜잭션 전파 옵션 이해

전파 옵션(Transaction Propagation Options)은 Spring 프레임워크에서 트랜잭션이 메서드 간에 어떻게 전파되어(즉, 이어져서) 실행되는지를 결정하는 중요한 설정입니다. 이 옵션은 하나의 트랜잭션 경계 내에서 여러 비즈니스 로직을 실행할 때, 호출하는 메서드와 호출되는 메서드가 동일한 트랜잭션을 공유할지, 아니면 별도의 트랜잭션을 사용할지를 정합니다. 이를 통해 개발자는 서로 다른 비즈니스 요구사항에 맞추어 트랜잭션의 범위와 격리 수준을 유연하게 관리할 수 있습니다.

1.1 전파 옵션의 종류 및 개념

1. REQUIRED

  • 개념: 현재 실행 중인 트랜잭션이 있으면 해당 트랜잭션을 재사용하고, 없다면 새로 생성합니다.
  • 세부 동작:
    • 존재하는 트랜잭션 참여: 만약 호출하는 메서드가 이미 트랜잭션 경계를 가지고 있다면, 호출된 메서드도 동일한 트랜잭션 내에서 실행됩니다.
    • 신규 트랜잭션 생성: 트랜잭션이 없는 상태라면, 호출된 메서드는 새 트랜잭션을 시작합니다.
  • 사용 예:
    • 일반적인 데이터 저장, 수정, 삭제 작업
    • 여러 하위 작업들이 하나의 원자적 단위로 실행되어야 할 때
  • 주의 사항:
    • 하위 메서드에서 예외가 발생하면 전체 트랜잭션이 롤백되므로, 부모 메서드의 작업까지 영향을 받을 수 있습니다.

2. REQUIRES_NEW

  • 개념: 항상 새로운 독립적인 트랜잭션을 생성합니다. 만약 이미 트랜잭션이 진행 중이라면, 이를 일시 중단한 후 새 트랜잭션 내에서 실행합니다.
  • 세부 동작:
    • 기존 트랜잭션 일시 중단: 호출하는 메서드의 트랜잭션이 존재하면, 그 트랜잭션을 잠시 보류(suspend)하고 호출된 메서드는 별도의 트랜잭션을 시작합니다.
    • 독립적 커밋/롤백: 새로 생성된 트랜잭션은 독립적으로 커밋하거나 롤백되며, 호출하는 메서드의 트랜잭션 상태와는 별개로 처리됩니다.
  • 사용 예:
    • 로그 기록, 감사(audit) 기록, 결제 처리와 같이 메인 비즈니스 로직과 분리되어 처리되어야 하는 작업
    • 메인 트랜잭션의 실패 여부와 무관하게 반드시 수행되어야 하는 작업
  • 주의 사항:
    • 트랜잭션이 중첩되지 않고 완전히 분리되므로, 성능 오버헤드가 발생할 수 있습니다.
    • 트랜잭션 동기화나 데이터 정합성 관리에 주의가 필요합니다.

3. NESTED

  • 개념: 현재 트랜잭션 내부에 ‘중첩 트랜잭션’으로 실행됩니다. 부모 트랜잭션의 일부로 동작하지만, 내부적으로는 Savepoint를 사용하여 부분 롤백이 가능합니다.
  • 세부 동작:
    • Savepoint 생성: NESTED 옵션을 사용하면 부모 트랜잭션 내에서 Savepoint가 생성되고, 이 Savepoint 이후의 작업에 문제가 생기면 해당 부분만 롤백할 수 있습니다.
    • 부모 트랜잭션 종속: 최종적으로 부모 트랜잭션이 커밋되어야 NESTED 트랜잭션의 결과도 확정됩니다. 반대로, 부모 트랜잭션이 롤백되면 NESTED 트랜잭션의 변경 사항도 모두 롤백됩니다.
  • 사용 예:
    • 하나의 큰 트랜잭션 내에서 특정 부분만 선택적으로 롤백하고자 할 때
    • 복잡한 비즈니스 로직 중 일부 작업만 별도로 취소할 필요가 있을 때
  • 주의 사항:
    • 데이터베이스가 Savepoint를 지원해야 하며, 지원하지 않는 경우 NESTED 옵션은 정상 동작하지 않습니다.

4. SUPPORTS

  • 개념: 현재 트랜잭션이 존재하면 그 트랜잭션에 참여하고, 존재하지 않으면 트랜잭션 없이 실행됩니다.
  • 세부 동작:
    • 유연한 트랜잭션 적용: 트랜잭션 경계가 이미 설정되어 있다면 해당 트랜잭션을 그대로 사용하지만, 트랜잭션이 없을 경우에도 별도의 트랜잭션 없이 실행되므로, 호출한 메서드 내에서 별도의 커밋이나 롤백 관리가 이루어지지 않습니다.
  • 사용 예:
    • 단순 조회 작업과 같이 데이터 변경 없이 읽기만 수행하는 경우
    • 트랜잭션 경계를 반드시 요구하지 않는 경우
  • 주의 사항:
    • 데이터의 정합성이 중요한 쓰기 작업에는 적합하지 않습니다. 트랜잭션 없이 실행되면 예기치 않은 데이터 불일치 문제가 발생할 수 있습니다.

1.2 전파 옵션의 활용 및 중요성

  • 메서드 간 트랜잭션 경계 설정: 하나의 비즈니스 로직이 여러 하위 메서드 호출로 구성된 경우, 전파 옵션에 따라 전체 작업을 하나의 트랜잭션으로 묶을 것인지, 아니면 각 메서드를 독립된 트랜잭션으로 처리할 것인지를 결정할 수 있습니다.
  • 예외 및 롤백 관리: 전파 옵션은 어떤 메서드에서 발생한 예외가 전체 트랜잭션에 영향을 미칠지, 아니면 해당 부분만 롤백할지를 결정하는 데 중요한 역할을 합니다. 예를 들어, REQUIRES_NEW를 사용하면 내부 메서드에서 예외가 발생해도 부모 트랜잭션에는 영향을 미치지 않을 수 있습니다.
  • 비즈니스 요구사항 반영: 각 전파 옵션은 서로 다른 비즈니스 요구사항(예: 로그 기록, 데이터 무결성, 성능 등)을 충족시키기 위해 설계되었습니다. 개발자는 각 상황에 맞는 전파 옵션을 선택하여 올바른 트랜잭션 경계를 설정해야 합니다.

1.3 사용 사례와 주의 사항

REQUIRED 사용 사례

  • 일반적인 비즈니스 로직에서 가장 많이 사용됩니다.
  • 예: 고객의 주문 생성, 결제 처리, 재고 차감 등의 작업.

REQUIRES_NEW 사용 사례

  • 기존 트랜잭션과 분리된 작업이 필요한 경우 사용됩니다.
  • 예: 결제 정보를 로그로 저장하거나, 독립적으로 동작해야 하는 보조 작업.

NESTED 사용 사례

  • 부모 트랜잭션 내에서 부분 롤백이 필요한 경우 적합합니다.
  • 예: 한 트랜잭션 내에서 여러 단계의 작업 중 하나만 롤백하려는 경우.

SUPPORTS 사용 사례

  • 단순히 데이터를 조회하거나, 트랜잭션이 필요하지 않은 작업에 적합합니다.
  • 예: 상품 목록 조회, 통계 데이터 계산.

2. 트랜잭션 전파 옵션 실습

아래는 각 전파 옵션(Propagation Options)을 활용한 간단한 실습 예제입니다. 예제에서는 스프링의 @Transactional 어노테이션과 전파 옵션을 사용하여 부모 메서드와 자식 메서드 간의 트랜잭션 동작을 확인할 수 있도록 구성했습니다.

2.1 REQUIRED

Propagation.REQUIRED 옵션은 부모 트랜잭션이 존재하면 이를 재사용하고, 없으면 새 트랜잭션을 생성합니다. 아래 예제에서는 부모 메서드와 자식 메서드 모두 REQUIRED 옵션을 사용하므로, 동일한 트랜잭션 내에서 실행됩니다.

orderRequest 메서드

@Transactional(propagation = Propagation.REQUIRED)
public void orderRequest(OrderRequest request) {
    TaskQueue taskQueue = taskQueueService.requestQueue(TaskType.ORDER);
    orderProcess(taskQueue.getId(), request);
}
  • 트랜잭션 흐름:
    • Propagation.REQUIRED: 이미 존재하는 트랜잭션이 없으므로 새 트랜잭션이 시작됩니다.
    • 이 트랜잭션은 메서드 종료 시 커밋되거나 롤백됩니다.
  • 비동기 호출:
    • orderProcess@Async로 비동기 호출되기 때문에 새로운 스레드에서 실행되고, 호출된 스레드와 트랜잭션을 공유하지 않습니다.

비동기 메서드 orderProcess

@Async
@Transactional(propagation = Propagation.REQUIRED)
public void orderProcess(Long taskQueueId, OrderRequest request) {
    taskQueueService.processQueueById(taskQueueId, (taskQueue) -> {
      Order order = save(request.getUserId());

      taskQueue.setEventId(order.getId());

      List<OrderItem> orderItems = orderProcessService.createOrderItems(request, order);

      BigDecimal totalPrice = orderProcessService.calculateTotalPrice(orderItems);
      order.setTotalPrice(totalPrice);
    });
}
  • 트랜잭션 흐름:

    • @Async로 인해 비동기 스레드에서 별도로 실행되므로, 기존 트랜잭션을 상속받지 않고 새로운 트랜잭션이 시작됩니다.
    • Propagation.REQUIRED: 새로운 트랜잭션이 시작되며, 비동기 메서드 내 모든 작업은 이 트랜잭션 내에서 실행됩니다.
    • 메서드 종료 시 트랜잭션이 커밋되거나 롤백됩니다.

    processQueueById 메서드 (TaskQueue 트랜잭션 관리)

@Transactional(propagation = Propagation.REQUIRED)
public void processQueueById(Long taskQueueId, Consumer<TaskQueue> task) {
    TaskQueue taskQueue = taskQueueRepository.findByIdForUpdate(taskQueueId)
        .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_TASK));

    updateStatus(taskQueue, TaskStatus.PROCESSING);

    task.accept(taskQueue);  // 여기서 orderProcess 내부 로직 수행

    updateStatus(taskQueue, TaskStatus.COMPLETED);
}
  • 트랜잭션 흐름:
    • Propagation.REQUIRED: 비동기 호출된 orderProcess 메서드에서 이 메서드를 호출하면 기존 트랜잭션을 그대로 사용합니다.
      즉, 이 메서드 내 모든 작업은 orderProcess의 트랜잭션에 속합니다.
    • findByIdForUpdate: 데이터베이스 레벨에서 행을 잠급니다(비관적 잠금).
    • 트랜잭션이 유지되는 동안 TaskQueue의 상태를 변경하고 커밋합니다.

전체 트랜잭션 흐름

  1. orderRequest 메서드 시작:
    • 새로운 트랜잭션이 시작되지만, 비동기 메서드 호출 후 트랜잭션이 바로 종료됩니다.
  2. orderProcess 메서드 호출 (비동기):
    • 새로운 스레드에서 새로운 트랜잭션이 시작됩니다.
  3. processQueueById 호출:
    • 이 메서드는 orderProcess의 트랜잭션에 속해 작업을 진행합니다.
    • 트랜잭션 내에서 findByIdForUpdate로 TaskQueue를 잠그고 상태를 업데이트합니다.
  4. 작업 완료 시:
    • orderProcess 메서드의 트랜잭션이 커밋되면 모든 변경 사항이 반영됩니다.
    • 만약 예외가 발생하면 트랜잭션이 롤백됩니다.

예상되는 트랜잭션 동작

메서드트랜잭션 전파트랜잭션 상태트랜잭션 범위
orderRequestREQUIRED새로운 트랜잭션 생성메서드 내에서만 트랜잭션 유지
orderProcess (비동기)REQUIRED새로운 트랜잭션 생성 (비동기)orderProcess 전체가 트랜잭션 범위
processQueueByIdREQUIREDorderProcess 트랜잭션에 속함processQueueById 내 모든 작업 포함

2.2 REQUIRES_NEW

Propagation.REQUIRES_NEW 옵션은 항상 새로운 트랜잭션을 생성합니다. 이미 부모 트랜잭션이 존재하더라도 이를 일시 중단하고 별도의 트랜잭션에서 실행되므로, 자식 메서드의 커밋/롤백이 부모 트랜잭션과 독립적으로 처리됩니다.

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void orderProcess(Long taskQueueId, OrderRequest request) {
    taskQueueService.processQueueById(taskQueueId, (taskQueue) -> {
        Order order = save(request.getUserId());

        taskQueue.setEventId(order.getId());

        List<OrderItem> orderItems = orderProcessService.createOrderItems(request, order);

        BigDecimal totalPrice = orderProcessService.calculateTotalPrice(orderItems);
        order.setTotalPrice(totalPrice);
    });
}

트랜잭션 흐름

  1. orderRequest 메서드:
    • 트랜잭션 시작: Propagation.REQUIRED에 의해 기존 트랜잭션을 생성하고 관리합니다.
    • orderProcess 호출 시 트랜잭션 종료: 비동기 메서드 호출 후, orderRequest의 트랜잭션은 별개로 유지됩니다.
  2. 비동기 메서드 orderProcess:
    • 새로운 트랜잭션 시작: Propagation.REQUIRES_NEW는 기존 트랜잭션을 무시하고 새 트랜잭션을 시작합니다.
    • orderProcess 내부에서 발생하는 모든 데이터베이스 변경은 독립적으로 관리됩니다.
    • 이 트랜잭션은 메서드 종료 시 커밋되거나 롤백됩니다.
  3. processQueueById 메서드:
    • processQueueByIdorderProcess 메서드의 트랜잭션 범위에 포함됩니다.
      즉, processQueueById의 모든 변경 사항은 orderProcess의 새로운 트랜잭션 내에서 처리됩니다.
    • 예외가 발생하면 orderProcess의 트랜잭션 전체가 롤백됩니다.

상세 트랜잭션 흐름

메서드트랜잭션 전파트랜잭션 상태트랜잭션 범위
orderRequestREQUIRED새로운 트랜잭션 생성orderRequest 메서드 내에서만 유지
orderProcess (비동기)REQUIRES_NEW새로운 트랜잭션 생성 (비동기)orderProcess 메서드 내에서만 유지
processQueueByIdREQUIRES_NEW새로운 트랜잭션 생성processQueueById 내 모든 작업 포함

트랜잭션의 특징

  1. 독립된 트랜잭션 보장:
    • orderRequest에서의 트랜잭션과 비동기 orderProcess 트랜잭션은 서로 독립적입니다.
    • 즉, orderRequest에서 오류가 발생해도 orderProcess의 트랜잭션은 롤백되지 않습니다.
  2. 비동기 메서드에서 발생하는 예외 처리:
    • orderProcess 내에서 예외가 발생하면 해당 메서드의 트랜잭션은 완전히 롤백됩니다.
    • 하지만 기존의 orderRequest 트랜잭션에는 전혀 영향을 미치지 않습니다.
  3. 트랜잭션 커밋 및 롤백 흐름
    • orderRequest: 비동기 메서드 호출 후 트랜잭션이 바로 커밋되거나 롤백됩니다.
    • orderProcess: 비동기 메서드 내의 작업이 끝난 후 별도의 커밋이 진행됩니다.
      예외 발생 시 orderProcess 트랜잭션만 롤백됩니다.

시나리오

정상 흐름:

  1. orderRequest에서 트랜잭션 시작 → 비동기 메서드 호출 → 트랜잭션 커밋됨.
  2. orderProcess가 비동기적으로 실행되면서 새로운 트랜잭션 시작.
  3. processQueueById 실행 후 모든 작업이 정상적으로 완료 → orderProcess의 트랜잭션 커밋.

예외 발생 시:

  • orderRequest에서 예외 발생:
    • orderRequest의 트랜잭션은 롤백되지만 비동기 orderProcess는 영향을 받지 않고 계속 진행됩니다.
  • orderProcess에서 예외 발생:
    • orderProcess의 트랜잭션만 롤백됩니다.
    • orderRequest의 트랜잭션에는 영향을 미치지 않음.

2.3 NESTED

Propagation.NESTED를 사용하면, 기존 트랜잭션 안에서 서브 트랜잭션(sub-transaction)을 생성합니다. 즉, 독립적으로 커밋되거나 롤백될 수 있는 부분 트랜잭션을 제공합니다. 하지만, 최상위 트랜잭션이 롤백되면 서브 트랜잭션도 롤백됩니다.

@Async
@Transactional(propagation = Propagation.NESTED)
public void orderProcess(Long taskQueueId, OrderRequest request) {
    taskQueueService.processQueueById(taskQueueId, (taskQueue) -> {
        Order order = save(request.getUserId());

        taskQueue.setEventId(order.getId());

        List<OrderItem> orderItems = orderProcessService.createOrderItems(request, order);

        BigDecimal totalPrice = orderProcessService.calculateTotalPrice(orderItems);
        order.setTotalPrice(totalPrice);
    });
}

트랜잭션 흐름

  1. orderRequest 메서드:
    • Propagation.REQUIRED에 의해 최상위 트랜잭션이 시작됩니다.
    • 이 트랜잭션은 비동기 메서드 호출 전까지 유지되며, 호출 후에는 비동기 메서드와 별도로 커밋됩니다.
  2. 비동기 메서드 orderProcess:
    • Propagation.NESTED로 인해 기존 최상위 트랜잭션의 서브 트랜잭션이 생성됩니다.
    • 이 서브 트랜잭션은 독립적으로 커밋되거나 롤백될 수 있습니다.
    • 단, 최상위 트랜잭션이 롤백되면 서브 트랜잭션도 함께 롤백됩니다.
  3. processQueueById 메서드:
    • 이 메서드는 orderProcess의 서브 트랜잭션 범위 내에서 실행됩니다.
    • processQueueById에서 문제가 발생하면 서브 트랜잭션만 롤백되고, 최상위 트랜잭션에는 영향을 미치지 않습니다.

상세 트랜잭션 흐름

메서드트랜잭션 전파트랜잭션 상태트랜잭션 범위
orderRequestREQUIRED최상위 트랜잭션 시작메서드 내에서 트랜잭션 유지
orderProcess (비동기)NESTED서브 트랜잭션 생성orderProcess 메서드 내에서 서브 트랜잭션 유지
processQueueByIdREQUIREDorderProcess 서브 트랜잭션에 속함processQueueById 내 모든 작업 포함

1. 정상 흐름:

  • orderRequest 트랜잭션 → orderProcess 서브 트랜잭션 → processQueueById 작업 완료 후 서브 트랜잭션 커밋 → 최상위 트랜잭션 커밋
    결과: 모든 작업이 정상적으로 DB에 반영됨.

2. orderProcess에서 예외 발생:

  • orderRequest 트랜잭션 → orderProcess 서브 트랜잭션 → 서브 트랜잭션 롤백 → 최상위 트랜잭션은 정상적으로 커밋됨
    결과: orderProcess 내 작업은 DB에 반영되지 않지만, orderRequest는 성공적으로 커밋됨.

3. orderRequest에서 예외 발생:

  • orderRequest 트랜잭션 → orderProcess 서브 트랜잭션 → 서브 트랜잭션 커밋 → 최상위 트랜잭션 롤백
    결과: 최상위 트랜잭션이 롤백되므로 서브 트랜잭션의 커밋 내용도 모두 롤백됨.

NESTED의 장단점

장점단점
서브 트랜잭션 단위로 커밋/롤백 가능 → 부분 실패를 처리할 수 있음최상위 트랜잭션이 롤백되면 서브 트랜잭션도 무조건 롤백됨 (부분적으로 살릴 수 없음)
REQUIRES_NEW와 달리 최상위 트랜잭션의 컨텍스트를 그대로 활용할 수 있음전체 트랜잭션이 큰 경우, 서브 트랜잭션이 자주 발생하면 성능에 부정적인 영향을 미칠 수 있음
  • Propagation.NESTED는 부분적으로 실패를 허용하면서도 최상위 트랜잭션에 대한 제어를 유지해야 할 때 적합합니다.
  • 만약 orderProcess의 작업이 최상위 트랜잭션과 강하게 결합되어 있어야 하면서도 부분적으로 예외를 처리해야 한다면 NESTED가 적합합니다.
  • 반대로, 최상위 트랜잭션과 완전히 독립된 트랜잭션이 필요하다면 REQUIRES_NEW가 더 적합합니다.

2.4 SUPPORTS

Propagation.SUPPORTS 옵션은 현재 트랜잭션이 존재하면 그 트랜잭션에 참여하고, 존재하지 않으면 트랜잭션 없이 실행됩니다. 주로 조회나 트랜잭션 경계가 반드시 필요하지 않은 작업에 사용됩니다.

@Service
@RequiredArgsConstructor
public class OrderService {

  private final OrderRepository orderRepository;
  private final UserRepository userRepository;
  
  @Transactional(propagation = Propagation.SUPPORTS)
  public User getUser(Long userId) {
    return userRepository.findById(userId)
        .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_USER));
  }

  @Transactional(propagation = Propagation.SUPPORTS)
  public Order getOrder(Long orderId) {
    return orderRepository.findById(orderId)
        .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_ORDER));
  }

  @Transactional(propagation = Propagation.REQUIRED)
  public Order update(Long orderId, Long userId) {
    User orderUser = getUser(userId);
    Order order = getOrder(orderId);

    order.setUser(orderUser);
    order.setTotalPrice(BigDecimal.ZERO);
    return orderRepository.save(order);
  }
  
}

두 가지 호출 예시:

  1. 트랜잭션 없이 호출

    트랜잭션이 없으므로 SUPPORTS 메서드는 트랜잭션 없이 실행됩니다.

      @Transactional(propagation = Propagation.SUPPORTS)
      public User getUser(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_USER));
      }
    
      @Transactional(propagation = Propagation.SUPPORTS)
      public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_ORDER));
      }
  2. 트랜잭션 내에서 호출

    update 메서드가 트랜잭션을 생성하므로, SUPPORTS 메서드가 해당 트랜잭션에 참여합니다.

      @Transactional(propagation = Propagation.REQUIRED)
      public Order update(Long orderId, Long userId) {
        User orderUser = getUser(userId); // 트랜젝션 적용
        Order order = getOrder(orderId); // 트랜젝션 적용
    
        order.setUser(orderUser);
        order.setTotalPrice(BigDecimal.ZERO);
        return orderRepository.save(order);
      }
  • SUPPORTS 옵션은 트랜잭션이 필수가 아닌 경우(예: 단순 조회)에 적합합니다.
  • 부모의 트랜잭션 존재 여부에 따라 실행 방식이 달라지므로, 데이터 정합성에 주의해야 합니다.

전파옵션 정리

각 전파 옵션을 활용하면 비즈니스 로직에 맞추어 트랜잭션 경계를 유연하게 구성할 수 있습니다.

  • REQUIRED: 기본 옵션으로, 하나의 트랜잭션 내에서 모든 작업이 실행됨
  • REQUIRES_NEW: 별도의 독립 트랜잭션으로 실행하여 부모와 독립적인 커밋/롤백 가능
  • NESTED: 부모 트랜잭션 내에서 Savepoint를 활용해 부분 롤백 가능
  • SUPPORTS: 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행
profile
기록하고 소화해보자! 소화가 안되거나 까먹으면 다시 꺼내서 보자! 오늘의 나는 어제의 나보다 강하다!

1개의 댓글

comment-user-thumbnail
2025년 2월 5일
답글 달기