전파 옵션(Transaction Propagation Options)은 Spring 프레임워크에서 트랜잭션이 메서드 간에 어떻게 전파되어(즉, 이어져서) 실행되는지를 결정하는 중요한 설정입니다. 이 옵션은 하나의 트랜잭션 경계 내에서 여러 비즈니스 로직을 실행할 때, 호출하는 메서드와 호출되는 메서드가 동일한 트랜잭션을 공유할지, 아니면 별도의 트랜잭션을 사용할지를 정합니다. 이를 통해 개발자는 서로 다른 비즈니스 요구사항에 맞추어 트랜잭션의 범위와 격리 수준을 유연하게 관리할 수 있습니다.
1. REQUIRED
2. REQUIRES_NEW
3. NESTED
4. SUPPORTS
REQUIRED 사용 사례
REQUIRES_NEW 사용 사례
NESTED 사용 사례
SUPPORTS 사용 사례
아래는 각 전파 옵션(Propagation Options)을 활용한 간단한 실습 예제입니다. 예제에서는 스프링의 @Transactional 어노테이션과 전파 옵션을 사용하여 부모 메서드와 자식 메서드 간의 트랜잭션 동작을 확인할 수 있도록 구성했습니다.
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의 상태를 변경하고 커밋합니다.전체 트랜잭션 흐름
orderRequest 메서드 시작:orderProcess 메서드 호출 (비동기):processQueueById 호출:orderProcess의 트랜잭션에 속해 작업을 진행합니다.findByIdForUpdate로 TaskQueue를 잠그고 상태를 업데이트합니다.orderProcess 메서드의 트랜잭션이 커밋되면 모든 변경 사항이 반영됩니다.예상되는 트랜잭션 동작
| 메서드 | 트랜잭션 전파 | 트랜잭션 상태 | 트랜잭션 범위 |
|---|---|---|---|
orderRequest | REQUIRED | 새로운 트랜잭션 생성 | 메서드 내에서만 트랜잭션 유지 |
orderProcess (비동기) | REQUIRED | 새로운 트랜잭션 생성 (비동기) | orderProcess 전체가 트랜잭션 범위 |
processQueueById | REQUIRED | orderProcess 트랜잭션에 속함 | processQueueById 내 모든 작업 포함 |
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);
});
}
트랜잭션 흐름
orderRequest 메서드:Propagation.REQUIRED에 의해 기존 트랜잭션을 생성하고 관리합니다.orderProcess 호출 시 트랜잭션 종료: 비동기 메서드 호출 후, orderRequest의 트랜잭션은 별개로 유지됩니다.orderProcess:Propagation.REQUIRES_NEW는 기존 트랜잭션을 무시하고 새 트랜잭션을 시작합니다.orderProcess 내부에서 발생하는 모든 데이터베이스 변경은 독립적으로 관리됩니다.processQueueById 메서드:processQueueById는 orderProcess 메서드의 트랜잭션 범위에 포함됩니다.processQueueById의 모든 변경 사항은 orderProcess의 새로운 트랜잭션 내에서 처리됩니다.orderProcess의 트랜잭션 전체가 롤백됩니다.상세 트랜잭션 흐름
| 메서드 | 트랜잭션 전파 | 트랜잭션 상태 | 트랜잭션 범위 |
|---|---|---|---|
orderRequest | REQUIRED | 새로운 트랜잭션 생성 | orderRequest 메서드 내에서만 유지 |
orderProcess (비동기) | REQUIRES_NEW | 새로운 트랜잭션 생성 (비동기) | orderProcess 메서드 내에서만 유지 |
processQueueById | REQUIRES_NEW | 새로운 트랜잭션 생성 | processQueueById 내 모든 작업 포함 |
트랜잭션의 특징
orderRequest에서의 트랜잭션과 비동기 orderProcess 트랜잭션은 서로 독립적입니다.orderRequest에서 오류가 발생해도 orderProcess의 트랜잭션은 롤백되지 않습니다.orderProcess 내에서 예외가 발생하면 해당 메서드의 트랜잭션은 완전히 롤백됩니다.orderRequest 트랜잭션에는 전혀 영향을 미치지 않습니다.orderRequest: 비동기 메서드 호출 후 트랜잭션이 바로 커밋되거나 롤백됩니다.orderProcess: 비동기 메서드 내의 작업이 끝난 후 별도의 커밋이 진행됩니다.orderProcess 트랜잭션만 롤백됩니다.시나리오
정상 흐름:
orderRequest에서 트랜잭션 시작 → 비동기 메서드 호출 → 트랜잭션 커밋됨.orderProcess가 비동기적으로 실행되면서 새로운 트랜잭션 시작.processQueueById 실행 후 모든 작업이 정상적으로 완료 → orderProcess의 트랜잭션 커밋.예외 발생 시:
orderRequest에서 예외 발생:orderRequest의 트랜잭션은 롤백되지만 비동기 orderProcess는 영향을 받지 않고 계속 진행됩니다.orderProcess에서 예외 발생:orderProcess의 트랜잭션만 롤백됩니다.orderRequest의 트랜잭션에는 영향을 미치지 않음.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);
});
}
트랜잭션 흐름
orderRequest 메서드:Propagation.REQUIRED에 의해 최상위 트랜잭션이 시작됩니다.orderProcess:Propagation.NESTED로 인해 기존 최상위 트랜잭션의 서브 트랜잭션이 생성됩니다.processQueueById 메서드:orderProcess의 서브 트랜잭션 범위 내에서 실행됩니다.processQueueById에서 문제가 발생하면 서브 트랜잭션만 롤백되고, 최상위 트랜잭션에는 영향을 미치지 않습니다.상세 트랜잭션 흐름
| 메서드 | 트랜잭션 전파 | 트랜잭션 상태 | 트랜잭션 범위 |
|---|---|---|---|
orderRequest | REQUIRED | 최상위 트랜잭션 시작 | 메서드 내에서 트랜잭션 유지 |
orderProcess (비동기) | NESTED | 서브 트랜잭션 생성 | orderProcess 메서드 내에서 서브 트랜잭션 유지 |
processQueueById | REQUIRED | orderProcess 서브 트랜잭션에 속함 | processQueueById 내 모든 작업 포함 |
1. 정상 흐름:
orderRequest 트랜잭션 → orderProcess 서브 트랜잭션 → processQueueById 작업 완료 후 서브 트랜잭션 커밋 → 최상위 트랜잭션 커밋2. orderProcess에서 예외 발생:
orderRequest 트랜잭션 → orderProcess 서브 트랜잭션 → 서브 트랜잭션 롤백 → 최상위 트랜잭션은 정상적으로 커밋됨orderProcess 내 작업은 DB에 반영되지 않지만, orderRequest는 성공적으로 커밋됨.3. orderRequest에서 예외 발생:
orderRequest 트랜잭션 → orderProcess 서브 트랜잭션 → 서브 트랜잭션 커밋 → 최상위 트랜잭션 롤백NESTED의 장단점
| 장점 | 단점 |
|---|---|
| 서브 트랜잭션 단위로 커밋/롤백 가능 → 부분 실패를 처리할 수 있음 | 최상위 트랜잭션이 롤백되면 서브 트랜잭션도 무조건 롤백됨 (부분적으로 살릴 수 없음) |
REQUIRES_NEW와 달리 최상위 트랜잭션의 컨텍스트를 그대로 활용할 수 있음 | 전체 트랜잭션이 큰 경우, 서브 트랜잭션이 자주 발생하면 성능에 부정적인 영향을 미칠 수 있음 |
Propagation.NESTED는 부분적으로 실패를 허용하면서도 최상위 트랜잭션에 대한 제어를 유지해야 할 때 적합합니다.orderProcess의 작업이 최상위 트랜잭션과 강하게 결합되어 있어야 하면서도 부분적으로 예외를 처리해야 한다면 NESTED가 적합합니다.REQUIRES_NEW가 더 적합합니다.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);
}
}
두 가지 호출 예시:
트랜잭션 없이 호출
트랜잭션이 없으므로 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));
}
트랜잭션 내에서 호출
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 옵션은 트랜잭션이 필수가 아닌 경우(예: 단순 조회)에 적합합니다.전파옵션 정리
각 전파 옵션을 활용하면 비즈니스 로직에 맞추어 트랜잭션 경계를 유연하게 구성할 수 있습니다.
