info

Spring의 트랜잭션 전파 옵션은 현재 트랜잭션에 참여할지, 새 트랜잭션을 만들지, 트랜잭션 없이 수행할지를 결정하는 정책이다. 실무에서는 특히 REQUIRED, REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER, NESTED 의 차이를 이해하고, 실패 격리와 롤백 범위를 정확히 설계하는 것이 중요하다.


🧩 트랜잭션 전파(Propagation)란?

트랜잭션 전파는 하나의 @Transactional 메서드 안에서 또 다른 @Transactional 메서드가 호출될 때, 기존 트랜잭션을 어떻게 처리할지 결정하는 규칙이다.

예를 들어 아래와 같은 상황이 있다.

  • 주문 저장
  • 결제 이력 저장
  • 알림 이력 저장
  • 감사 로그 저장

이때 모든 작업을 하나의 트랜잭션으로 묶을 수도 있고,
일부 작업은 실패해도 별도 커밋되도록 분리할 수도 있다.


🧭 기본 전파 옵션 종류

옵션 설명
REQUIRED 기존 트랜잭션이 있으면 참여, 없으면 새로 생성
REQUIRES_NEW 기존 트랜잭션과 무관하게 새 트랜잭션 생성
SUPPORTS 기존 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 수행
MANDATORY 기존 트랜잭션이 반드시 있어야 함
NOT_SUPPORTED 기존 트랜잭션이 있으면 중단하고 트랜잭션 없이 수행
NEVER 트랜잭션이 있으면 예외 발생
NESTED 기존 트랜잭션 내부에 중첩 트랜잭션 생성 (Savepoint 기반)

tip

일반적으로 실무에서 가장 많이 보는 조합은 REQUIRED + 일부 로그성/감사성 작업에 REQUIRES_NEW 이다.


1️⃣ REQUIRED

정의

REQUIRED 는 Spring의 기본값이다.
현재 트랜잭션이 존재하면 그 트랜잭션에 참여하고, 없으면 새로 생성한다.

예시

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
        paymentService.approvePayment(order);
    }
}
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;

    @Transactional
    public void approvePayment(Order order) {
        paymentRepository.save(new Payment(order.getId(), order.getAmount()));
    }
}

동작

  • createOrder() 에서 트랜잭션 시작
  • approvePayment() 는 같은 트랜잭션에 참여
  • 둘 중 하나라도 예외 발생 시 전체 롤백

사용 시점

  • 주문 + 결제 + 재고 차감처럼 원자성이 필요한 업무
  • 모두 성공하거나 모두 실패해야 하는 경우

2️⃣ REQUIRES_NEW

정의

REQUIRES_NEW기존 트랜잭션이 있어도 잠시 보류시키고, 항상 새로운 트랜잭션을 시작한다.

즉, 외부 트랜잭션과 별개로 커밋/롤백된다.

대표적인 실무 예시

  • 주문 실패 여부와 무관하게 감사 로그 저장
  • 메인 업무 실패와 별도로 알림 발송 이력 저장
  • 예외 발생 시 실패 로그만큼은 꼭 남기기

예시

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final AuditLogService auditLogService;

    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);

        auditLogService.saveAuditLog("주문 생성 시작");

        if (true) {
            throw new RuntimeException("주문 처리 중 오류");
        }
    }
}
@Service
@RequiredArgsConstructor
public class AuditLogService {

    private final AuditLogRepository auditLogRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveAuditLog(String message) {
        auditLogRepository.save(new AuditLog(message));
    }
}

동작

  • createOrder() 트랜잭션 시작
  • saveAuditLog() 호출 시 기존 트랜잭션 보류
  • 감사 로그는 새 트랜잭션 에서 커밋
  • 이후 createOrder() 에서 예외 발생
  • 주문 데이터는 롤백
  • 감사 로그는 이미 커밋되어 남음

핵심 포인트

warning

REQUIRES_NEW메인 트랜잭션과 완전히 분리된 커밋 단위 이다. 따라서 남아도 되는 데이터인지 반드시 검토해야 한다.

언제 써야 하나?

  • 실패 여부와 관계없이 무조건 남겨야 하는 로그
  • 별도 재처리 기준이 되는 상태 기록
  • 메인 로직과 분리된 감사/이력성 테이블 저장

남용하면 안 되는 이유

  • 트랜잭션이 쪼개지면서 데이터 정합성 이해가 어려워짐
  • 커넥션 사용량 증가 가능
  • “왜 메인 데이터는 롤백됐는데 로그는 남았지?” 같은 운영 이슈 발생 가능

3️⃣ SUPPORTS

정의

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

예시

@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Order findOrder(Long orderId) {
    return orderRepository.findById(orderId)
            .orElseThrow(() -> new IllegalArgumentException("주문 없음"));
}

사용 시점

  • 조회성 로직
  • 트랜잭션이 있으면 같이 타고, 없어도 무방한 경우

주의

  • 쓰기 작업에 사용하면 의도하지 않게 트랜잭션 없이 수행될 수 있음
  • 보통 조회 메서드에 한정 하는 편이 안전하다

4️⃣ MANDATORY

정의

MANDATORY반드시 기존 트랜잭션이 있어야 한다.
없으면 예외가 발생한다.

예시

@Transactional(propagation = Propagation.MANDATORY)
public void decreaseStock(Long productId, int quantity) {
    productRepository.decreaseStock(productId, quantity);
}

사용 시점

  • 반드시 상위 서비스 트랜잭션 안에서만 실행돼야 하는 로직
  • 단독 호출되면 안 되는 내부 업무 메서드

장점

  • 트랜잭션 경계가 깨지는 것을 강하게 방지 가능
  • 실수로 단독 호출되는 것을 차단 가능

5️⃣ NOT_SUPPORTED

정의

NOT_SUPPORTED 는 기존 트랜잭션이 있으면 보류시키고, 트랜잭션 없이 실행한다.

예시

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void callLegacyApi() {
    legacyClient.send();
}

사용 시점

  • 외부 API 호출
  • 오래 걸리는 배치성 조회
  • 트랜잭션을 오래 물고 있으면 안 되는 작업

실무 포인트

DB 트랜잭션은 커넥션과 락을 점유할 수 있다.
그래서 외부 연동처럼 응답 시간이 긴 작업은 트랜잭션 바깥으로 빼는 설계 가 유리할 때가 많다.


6️⃣ NEVER

정의

NEVER트랜잭션이 존재하면 오히려 예외를 발생 시킨다.

예시

@Transactional(propagation = Propagation.NEVER)
public void validateBatchEnvironment() {
    // 트랜잭션 안에서 수행되면 안 되는 검사 로직
}

사용 시점

  • 트랜잭션 안에서 실행되면 안 되는 로직을 강제할 때
  • 특수한 운영성 검증 로직

비고

일반 업무 서비스에서는 자주 쓰이지 않는다.


7️⃣ NESTED

정의

NESTED 는 기존 트랜잭션 안에서 Savepoint 기반의 중첩 트랜잭션 처럼 동작한다.

예시

@Transactional
public void processOrders(List<Order> orders) {
    for (Order order : orders) {
        orderItemService.saveOrderItem(order);
    }
}
@Transactional(propagation = Propagation.NESTED)
public void saveOrderItem(Order order) {
    orderItemRepository.save(new OrderItem(order.getId()));

    if (order.isInvalid()) {
        throw new RuntimeException("잘못된 주문");
    }
}

동작 개념

  • 상위 트랜잭션 존재
  • 하위 작업마다 savepoint 생성
  • 특정 하위 작업만 rollback 가능
  • 단, 최종적으로 상위 트랜잭션이 롤백되면 전체 롤백

주의

  • JPA 환경에서는 기대한 대로 항상 동작하지 않을 수 있음
  • 주로 JDBC/DataSourceTransactionManager 기반에서 savepoint 지원 여부를 확인해야 함

warning

NESTED 는 이름만 보고 REQUIRES_NEW 와 같은 개념으로 이해하면 안 된다. NESTED 는 부모 트랜잭션에 종속되고, REQUIRES_NEW 는 완전히 독립 이다.


⚖️ REQUIRES_NEW 와 NESTED 차이

구분 REQUIRES_NEW NESTED
기존 트랜잭션과 관계 분리 부모 내부
커밋 단위 독립 커밋 부모 커밋에 종속
롤백 범위 자기 자신만 savepoint 기준 부분 롤백
최종 부모 롤백 영향 영향 없음 영향 받음

🛠 가장 많이 쓰는 일반적인 시나리오

시나리오 1. 주문 저장 + 감사 로그 저장

  • 주문 저장: REQUIRED
  • 감사 로그 저장: REQUIRES_NEW
@Transactional
public void placeOrder(Order order) {
    orderRepository.save(order);

    try {
        auditLogService.save("주문 생성");
    } catch (Exception e) {
        log.error("감사 로그 저장 실패", e);
    }

    if (order.getAmount() < 0) {
        throw new IllegalArgumentException("잘못된 주문 금액");
    }
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(String message) {
    auditLogRepository.save(new AuditLog(message));
}

결과

  • 주문 저장 실패 → 주문 롤백
  • 감사 로그 저장 성공 → 감사 로그는 남음

시나리오 2. 외부 연동은 트랜잭션 밖에서 호출

@Transactional
public void registerUser(User user) {
    userRepository.save(user);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendWelcomeEmail(User user) {
    emailClient.send(user.getEmail());
}

포인트

사용자 저장 트랜잭션 중에 메일 서버 응답을 기다리면
불필요하게 트랜잭션 시간이 길어진다.
이런 작업은 커밋 이후 이벤트 처리 로 분리하는 게 더 좋다.


⚠️ 실무에서 자주 헷갈리는 포인트

1. 같은 클래스 내부 호출은 트랜잭션 프록시가 안 탄다

@Service
public class OrderService {

    @Transactional
    public void outer() {
        inner(); // 프록시 우회
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void inner() {
    }
}

위 코드는 inner()REQUIRES_NEW 가 기대대로 동작하지 않을 수 있다.
Spring AOP 프록시를 우회하기 때문이다.

해결 방향

  • 별도 Service로 분리
  • 자기 자신 프록시 주입 방식 검토
  • 구조적으로 트랜잭션 경계 분리

2. checked exception은 기본 rollback 대상이 아니다

Spring은 기본적으로 RuntimeException, Error 에 대해서만 rollback 한다.

@Transactional(rollbackFor = Exception.class)
public void process() throws Exception {
    throw new Exception("checked exception");
}

포인트

전파 옵션만 볼 게 아니라 rollback 조건도 함께 설계 해야 한다.


3. REQUIRES_NEW 를 로그에만 제한 없이 쓰면 안 된다

“로그는 무조건 남겨야 하니까 다 REQUIRES_NEW” 식으로 가면
나중에 데이터 흐름이 분산되어 장애 분석이 어려워진다.

즉, 업무 정합성보다 기록 보존이 더 중요한 경우에만 제한적으로 적용 하는 게 맞다.


📌 정리

[!summary] 트랜잭션 전파 옵션의 핵심은 현재 작업을 기존 트랜잭션에 묶을지, 분리할지, 트랜잭션 없이 처리할지 를 설계하는 것이다.

  • REQUIRED : 가장 기본, 하나의 업무 단위로 묶을 때 사용
  • REQUIRES_NEW : 실패와 무관하게 별도 커밋이 필요한 로그/감사 이력에 사용
  • SUPPORTS : 조회성 로직에 적합
  • MANDATORY : 상위 트랜잭션 필수
  • NOT_SUPPORTED : 외부 연동 등 트랜잭션 밖으로 빼고 싶은 작업
  • NEVER : 트랜잭션 존재 자체를 금지
  • NESTED : savepoint 기반 부분 롤백

결국 실무에서는 단순히 옵션 이름을 외우는 것보다,
“어디까지 같이 롤백돼야 하는가”와 “무엇은 별도로 남아야 하는가”를 먼저 정의한 뒤 전파 옵션을 선택 해야 한다.


연결문서

댓글남기기