스프링 트랜잭션(@Transactional) 전파레벨 7가지

2026. 2. 13. 00:44·스프링

0. 들어가며

프로젝트를 진행하면서 @Transactional은 정말 자주 사용해왔습니다. 
데이터 정합성이 중요한 로직이라면 거의 당연하다는 듯이 어노테이션을 붙였고 문제가 발생하면 "트랜잭션이 있으니 괜찮겠지"라고 안일하게 생각했었습니다.

하지만 토비의 스프링 5장 서비스 추상화를 읽고 문득 이런 생각이 들었습니다.

"나는 트랜잭션을 사용하고 있는 걸까, 아니면 그냥 붙이고 있는 걸까?"

특히 하나의 트랜잭션 메서드 안에서 다른 트랜잭션 메서드를 호출할 때 어떤 경우에는 전체가 함께 롤백되고 어떤 경우에는 일부만 롤백되는 이유를 명확하게 설명하지 못하는 저를 발견하였습니다.

그동안 막연하게 사용해왔던 @Transactional을 조금 더 명확하게 이해해보고 싶다는 생각이 들어 트랜잭션 전파 레벨 7가지를 정리해보려고 합니다.

저처럼 @Transactional을 사용해왔지만 내부 동작까지는 깊이 고민해보지 않으셨다면, 이 글이 함께 정리해보는 좋은 계기가 되길 바랍니다.


0. 트랜잭션 전파란 무엇인가?

트랜잭션 전파는 이미 트랜잭션이 진행중일 때, 새로운 트랜잭션 메서드를 호출하면 어떻게 동작할지를 결정하는 옵션입니다.

즉 트랜잭션을 같이 쓸지, 새로 만들지, 무시할지 결정하는 정책입니다.

 

 

예를 들어 주문 생성 메서드가 트랜잭션을 시작했고, 그 안에서 결제 처리 메서드를 호출한다고 가정해봅시다. 이때 결제 처리 메서드는 주문 생성의 트랜잭션에 합류할까요, 아니면 독자적인 트랜잭션을 시작할까요? 이런 상황을 제어하는 것이 바로 전파 레벨입니다.
전파 레벨은 총 7가지가 존재합니다.

하나씩 살펴 보겠습니다. 


1.  REQUIRED (기본값) - 있으면 같이, 없으면 새로 시작

REQUIRED는 스프링에서 가장 기본이 되는 전파 레벨입니다.

별도의 설정을 하지 않으면 기본값으로 적용되는 옵션이기도 합니다

현재 진행 중인 트랜잭션이 있으면 그 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 만듭니다.

 

주문 생성 서비스를 예로 들어보겠습니다.

OrderService의 createOrder 메서드가 트랜잭션을 시작하고 그 안에서 PaymentService의 processPayment를 호출한다고 해봅시다. 두 메서드 모두 REQUIRED를 사용한다면 하나의 트랜잭션으로 묶입니다. 주문 저장, 결제 처리, 재고 업데이트가 모두 같은 트랜잭션에서 실행되기 때문에 어느 하나라도 실패하면 전체가 롤백됩니다. 실제 로그를 보면 "Creating new transaction"으로 트랜잭션이 시작되고, 내부 메서드를 호출할 때는 "Participating in existing transaction"이라는 메시지가 나타납니다. 이는 새로운 트랜잭션을 만들지 않고 기존 트랜잭션에 참여했다는 의미입니다. 이 방식은 대부분의 비즈니스 로직에서 사용됩니다. 계좌 이체를 예로 들면, 출금과 입금이 하나의 트랜잭션으로 묶여야 합니다. 둘 중 하나만 성공하면 안 되기 때문이죠. REQUIRED는 바로 이런 "전체 성공 또는 전체 실패"가 필요한 상황에 적합합니다.

 

설정방법

// 1. 어노테이션 방식 (가장 일반적)
@Transactional(propagation = Propagation.REQUIRED)
public void someMethod() {
    // 로직
}

 


 REQUIRED 장점

REQUIRED의 가장 큰 장점은 단순하고 직관적이라는 것입니다.
여러 메서드를 하나의 논리적 작업 단위로 묶을 수 있어서 데이터 일관성을 보장하기 쉽습니다. 코드를 작성하는 개발자 입장에서도 "이 메서드들은 모두 함께 성공하거나 함께 실패한다"는 것을 명확하게 표현할 수 있습니다. 또한 기본값이기 때문에 명시적으로 propagation을 지정하지 않아도 됩니다.

 

 REQUIRED 단점

모든 작업이 하나로 묶이기 때문에 부분적인 실패 처리가 불가능합니다. 예를 들어 주문 생성은 성공했는데 알림 발송이 실패했을 때, 알림만 재시도하고 싶어도 전체를 다시 실행해야 합니다. 또한 트랜잭션이 길어지면 락을 오래 보유하게 되어 동시성 문제가 발생할 수 있습니다. 한 메서드에서 예외가 발생하면 전체가 롤백되기 때문에 세밀한 제어가 어렵습니다.


어떤 상황에 사용해야 할까?

일반적인 비즈니스 로직에 사용합니다. 은행 계좌 이체처럼 출금과 입금이 원자적으로 처리되어야 하는 경우, 주문 생성과 재고 차감이 함께 성공하거나 실패해야 하는 경우가 대표적입니다. 여러 테이블에 데이터를 삽입하는데 모든 삽입이 성공해야 의미가 있는 경우에도 적합합니다. 쇼핑몰에서 주문을 생성하면서 주문 테이블, 주문 상품 테이블, 배송 테이블에 동시에 데이터를 넣어야 하는 상황을 생각하면 됩니다.

 


2. REQUIRES_NEW - 무조건 새로 시작, 기존은 잠시 대기

REQUIRES_NEW는 항상 새로운 트랜잭션을 생성합니다.

이미 진행 중인 트랜잭션이 있다면 그것을 일시 중단하고 새로운 트랜잭션을 시작합니다.

새 트랜잭션이 완료되면 중단했던 기존 트랜잭션이 재개됩니다.

 

감사 로그를 기록하는 경우를 예를 들어보겠습니다.
주문 생성 중에 로그를 기록하는데 로그 기록이 실패했다고 해서 주문까지 실패 처리할 필요는 없습니다. 반대로 주문 생성이 실패했더라도 "주문 시도"라는 이력은 남겨야 할 수 있습니다. 이럴 때 로그 기록 메서드에 REQUIRES_NEW를 사용하면 완전히 독립적인 트랜잭션으로 동작합니다.

 

 시퀀스나 카운터를 업데이트할 때도 유용합니다. 방문자 수 카운터를 생각해보면, 메인 비즈니스 로직이 실패하더라도 "방문 시도"라는 카운트는 증가시켜야 할 수 있습니다. 알림 발송 이력도 마찬가지입니다. 알림 발송 자체는 성공했는데 다른 이유로 전체 트랜잭션이 롤백되더라도 "알림을 보냈다"는 이력은 남겨두고 싶을 때 REQUIRES_NEW를 사용합니다.

 

다만 주의할 점이 있습니다. 두 개의 트랜잭션이 같은 데이터에 접근하려고 하면 데드락이 발생할 수 있습니다.

예를 들어 외부 트랜잭션이 User 테이블에 락을 걸어둔 상태에서 REQUIRES_NEW로 시작한 내부 트랜잭션도 같은 User 데이터에 접근하려고 하면 서로를 기다리다가 데드락에 걸릴 수 있습니다.


설정방법

 @Transactional(propagation = Propagation.REQUIRES_NEW)

REQUIRES_NEW 장점

완전한 독립성이 가장 큰 장점입니다. 외부 트랜잭션의 성공 여부와 관계없이 자신의 작업을 커밋할 수 있습니다. 이는 감사 로그처럼 "실패한 시도도 기록해야 하는" 상황에서 매우 유용합니다. 또한 외부 트랜잭션이 길어져도 내부 트랜잭션은 빠르게 커밋되기 때문에 락을 오래 보유하지 않습니다. 

 

REQUIRES_NEW 단점

새로운 트랜잭션을 만들기 때문에 오버헤드가 큽니다. 커넥션 풀에서 새로운 커넥션을 가져와야 할 수도 있고, 트랜잭션을 중단하고 재개하는 비용도 발생합니다. 데드락 위험도 높아집니다. 외부 트랜잭션이 테이블 A에 락을 걸고, 내부 트랜잭션도 테이블 A에 접근하려고 하면 서로를 기다리다가 데드락에 걸릴 수 있습니다. 또한 두 트랜잭션이 독립적이기 때문에 데이터 불일치 상황을 주의 깊게 관리해야 합니다.


어떤 상황에 사용해야 할까

감사 로그나 시스템 로그를 기록할 때 사용합니다. 로그인 시도, API 호출 이력, 주문 시도 같은 것들은 메인 로직이 실패해도 기록으로 남겨야 합니다. 알림 발송 이력도 마찬가지입니다. 이메일을 보냈다는 사실 자체는 주문이 취소되더라도 이력으로 남아있어야 나중에 추적할 수 있습니다. 카운터나 통계 업데이트에도 적합합니다. 방문자 수, 조회 수, 다운로드 횟수 같은 것들은 메인 작업의 성공 여부와 무관하게 증가해야 합니다. 또한 배치 작업에서 각 아이템의 처리 결과를 독립적으로 저장할 때도 사용합니다.


3.SUPPORTS - 있으면 같이, 없어도 그냥 실행

SUPPORTS는 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 실행됩니다.
트랜잭션이 있어도 좋고 없어도 괜찮은 유연한 상황에 사용합니다.
주로 조회 전용 메서드에 사용됩니다.

 

상품 목록을 조회하는 메서드가 있다고 해봅시다. 이 메서드를 단독으로 호출하면 트랜잭션 없이 빠르게 조회만 하고, 만약 다른 트랜잭션 메서드 안에서 호출되면 그 트랜잭션에 참여해서 일관된 데이터를 보장받습니다.

 

리포트 생성 상황을 예로 들어보겠습니다. 리포트 생성은 트랜잭션이 필요하지만, 그 안에서 주문 데이터를 조회할 때는 SUPPORTS를 사용할 수 있습니다. 리포트 생성의 트랜잭션에 자연스럽게 합류하면서도  만약 단순 조회만 필요할 때는 트랜잭션 오버헤드 없이 동작할 수 있습니다. SUPPORTS는 readOnly 속성과 함께 사용하면 더욱 효과적입니다. 읽기 전용으로 설정하면 데이터베이스가 추가 최적화를 수행할 수 있기 때문입니다.

 

 @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public List<Product> searchProducts(String keyword) {
        return productRepository.findByNameContaining(keyword);
    }

1) 트랜잭션이 있는 상황 - 함께 실행

리포트 생성과 같이 트랜잭션이 필요한 메서드 안에서 호출하면, searchProducts는 기존 트랜잭션에 참여합니다.

 
@Transactional
public void generateReport() {
    List<Product> products = productService.searchProducts("사과"); // generateReport 트랜잭션에 참여
    // 리포트 생성 로직...
}
  • 이 경우, 일관성 있는 데이터를 보장받으며 조회할 수 있습니다.
  • 트랜잭션 안에서 호출되기 때문에 데이터 정합성이 유지됩니다.

  2) 트랜잭션이 없는 상황 - 그냥 실행

반대로, 단순 조회만 필요할 때는 트랜잭션 없이 그대로 실행됩니다.

 
// 트랜잭션 없이 단독 호출
List<Product> products = productService.searchProducts("사과");
System.out.println(products.size());
  • 트랜잭션이 없어도 조회가 가능하며, 불필요한 트랜잭션 오버헤드가 발생하지 않습니다.
  • 단순 조회나 캐시 조회처럼 일관성을 강하게 요구하지 않는 작업에 적합합니다.

SUPPORTS 장점

SUPPORTS는 유연성이 가장 큰 장점입니다.

트랜잭션이 필요한 상황에서는 참여하고 불필요한 상황에서는 오버헤드 없이 실행됩니다. 조회 전용 메서드를 만들 때 특히 유용한데, 단독으로 호출될 때는 빠르게 동작하면서도 다른 트랜잭션 안에서 호출되면 데이터 일관성을 보장받을 수 있습니다. 성능과 일관성 사이의 균형을 잘 맞출 수 있는 옵션입니다.


SUPPORTS 단점

동작이 호출 컨텍스트에 따라 달라지기 때문에 예측하기 어려울 수 있습니다.

같은 메서드를 호출해도 어떤 때는 트랜잭션 안에서, 어떤 때는 밖에서 실행됩니다. 이는 디버깅을 복잡하게 만들 수 있습니다. 또한 개발자가 현재 트랜잭션 컨텍스트를 정확히 이해하고 있어야 합니다. 트랜잭션이 없을 때 Lazy Loading이 실패할 수 있다는 점도 주의해야 합니다.


어떤 상황에 사용해야 할까

주로 조회 전용 서비스 메서드에 사용합니다. 상품 검색, 사용자 프로필 조회, 주문 내역 조회 같은 것들이 대표적입니다. 이런 메서드들은 단독으로 호출될 때가 많지만 가끔 다른 트랜잭션 안에서 호출될 수도 있습니다. 리포트나 통계 데이터를 조회할 때도 적합합니다. 데이터를 읽기만 하는 헬퍼 메서드나 유틸리티 메서드에도 좋습니다. readOnly 속성과 함께 사용하면 데이터베이스가 읽기 최적화를 할 수 있어서 성능도 향상됩니다.


4. NOT_SUPPORTED - 트랜잭션은 잠시 멈춰, 나는 없이 갈게

NOT_SUPPORTED는 항상 트랜잭션 없이 실행됩니다. 현재 진행 중인 트랜잭션이 있다면 일시 중단합니다. 대용량 데이터를 조회하는 경우를 생각해보겠습니다. 수십만 건의 주문 데이터를 조회하는데 트랜잭션을 유지하면 타임아웃이 발생할 수 있습니다. 또한 불필요하게 오랜 시간 동안 락을 보유하게 되어 다른 트랜잭션을 방해할 수 있습니다. 이럴 때 NOT_SUPPORTED를 사용하면 트랜잭션 없이 빠르게 데이터를 읽어올 수 있습니다. 외부 API를 호출할 때도 유용합니다. 외부 시스템과 통신하는 것은 트랜잭션과 무관한 작업입니다. 네트워크 지연이 발생할 수 있는데 그동안 트랜잭션을 유지할 이유가 없죠. 트랜잭션을 일시 중단하고 API 호출을 수행한 후, 다시 트랜잭션을 재개하는 것이 효율적입니다. 배치 작업에서 대량의 데이터를 처리할 때도 NOT_SUPPORTED가 적합합니다. 트랜잭션 오버헤드 없이 순수하게 데이터만 처리하는 것이 성능상 유리할 수 있습니다. SUPPORTS와의 차이점을 명확히 하자면, SUPPORTS는 트랜잭션이 있으면 참여하지만 NOT_SUPPORTED는 트랜잭션이 있어도 무시하고 중단시킵니다.

 

설정방법

@Transactional(propagation = Propagation.NOT_SUPPORTED)

NOT_SUPPORTED장점

트랜잭션 오버헤드를 완전히 제거할 수 있습니다. 트랜잭션이 없기 때문에 락을 걸지 않아서 다른 트랜잭션을 방해하지 않습니다. 오래 걸리는 작업을 수행할 때 트랜잭션 타임아웃 걱정 없이 실행할 수 있습니다. 대용량 데이터를 처리하거나 외부 API를 호출할 때 특히 유용합니다. 메모리도 절약할 수 있는데, 트랜잭션 컨텍스트를 유지하지 않아도 되기 때문입니다.


NOT_SUPPORTED 단점

데이터 일관성을 보장받을 수 없습니다. 트랜잭션이 없기 때문에 롤백이 불가능하고, 다른 트랜잭션이 변경 중인 데이터를 읽을 수도 있습니다. Dirty Read, Non-Repeatable Read 같은 문제가 발생할 수 있습니다. 또한 외부 트랜잭션을 중단시키기 때문에 예상치 못한 동작을 유발할 수 있습니다. ACID 속성을 포기하는 것이므로 신중하게 사용해야 합니다.


어떤 상황에 사용해야 할까

대용량 데이터를 조회하거나 내보낼 때 사용합니다. 수십만 건의 주문 데이터를 CSV로 추출한다거나, 전체 사용자 목록을 가져오는 작업은 트랜잭션이 불필요하고 오히려 방해가 됩니다. 외부 시스템과 통신할 때도 적합합니다. REST API 호출, 메시지 큐 발행, 파일 서버 접근 같은 작업들은 데이터베이스 트랜잭션과 무관합니다. 배치 작업의 초기화 단계나 정리 단계에서도 사용할 수 있습니다. 읽기 전용 작업인데 트랜잭션이 성능 병목이 되는 경우에도 고려해볼 만합니다.


5. MANDATORY - 트랜잭션 없으면 실행 불가

MANDATORY는 이름 그대로 반드시 기존 트랜잭션이 존재해야만 실행되는 옵션입니다.

만약 트랜잭션이 없는 상태에서 호출되면 예외가 발생합니다. 이는 특정 로직이 반드시 상위 트랜잭션 안에서 실행되어야 할 때 유용합니다.

 

예를 들어 계좌 차감이나 포인트 차감과 같이 단독으로 실행되면 안 되는 핵심 도메인 로직에 적용할 수 있습니다. 설계 상 “이 로직은 반드시 트랜잭션 안에서 실행되어야 한다”는 의도를 명확히 표현할 수 있습니다.

 

설정방법

  @Transactional(propagation = Propagation.MANDATORY)

 


MANDATORY 장점

트랜잭션 경계를 명확하게 강제할 수 있습니다.

"이 메서드는 반드시 트랜잭션 안에서만 실행되어야 한다"는 규칙을 코드로 표현할 수 있습니다. 실수로 단독 호출하는 것을 컴파일 타임이 아니라 런타임에라도 잡아낼 수 있어서 버그를 예방합니다. 특히 여러 개발자가 협업하는 큰 프로젝트에서 API 사용 방법을 명확히 하는 데 도움이 됩니다. 문서화의 역할도 합니다.


MANDATORY 단점

유연성이 떨어집니다. 테스트 코드를 작성할 때도 항상 트랜잭션을 시작해야 하기 때문에 단위 테스트가 복잡해질 수 있습니다. 또한 예외가 발생하면 호출자에게 트랜잭션을 시작하라고 요구하는 것이기 때문에 사용하는 쪽에서 부담을 느낄 수 있습니다. 리팩토링할 때도 제약이 됩니다. 메서드를 다른 곳에서 재사용하려고 할 때 항상 트랜잭션을 고려해야 하기 때문입니다.


어떤 상황에 사용해야 할까

핵심 비즈니스 로직에 사용합니다. 계좌에서 돈을 인출하는 메서드, 재고를 차감하는 메서드, 포인트를 사용하는 메서드처럼 절대 단독으로 실행되어서는 안 되는 작업들입니다. 데이터 정합성이 매우 중요한 금융 거래, 재고 관리, 예약 시스템에서 특히 유용합니다. 또한 private 메서드가 아니라 protected나 public 메서드인데 외부에서 직접 호출하면 위험한 경우에 사용합니다. "이 메서드는 파사드 메서드를 통해서만 호출해야 한다"는 규칙을 강제할 때 효과적입니다.


6. NEVER - 트랜잭션 있으면 실행 불가

NEVER는 반드시 트랜잭션 없이 실행되어야 합니다. 트랜잭션이 있으면 IllegalTransactionStateException이 발생합니다. MANDATORY의 정반대 개념입니다.

자주 사용되지는 않지만  특정 작업이 트랜잭션의 영향을 받아서는 안 되는 경우에 사용할 수 있습니다.

 

이메일 발송을 생각해보겠습니다.

이메일을 보내는 것은 데이터베이스 트랜잭션과 전혀 무관한 작업입니다. 오히려 트랜잭션 안에서 실행되면 문제가 생길 수 있습니다. 이메일 발송에 시간이 걸려서 트랜잭션 타임아웃이 발생할 수 있고, 무엇보다 트랜잭션이 롤백되어도 이미 보낸 이메일은 취소할 수 없습니다. SMS 발송도 마찬가지입니다. 문자가 이미 발송된 후에 트랜잭션이 롤백되면 어떻게 될까요? 문자는 취소할 수 없기 때문에 데이터베이스 상태와 실제 상태가 불일치하게 됩니다. 이런 작업들은 NEVER를 사용해서 트랜잭션 컨텍스트에서 실행되는 것을 막아야 합니다. 파일 입출력 작업도 NEVER가 적합합니다. 파일에 데이터를 쓰는 작업은 트랜잭션 롤백으로 되돌릴 수 없습니다. 캐시를 무효화하는 작업도 마찬가지입니다. 캐시 삭제는 즉시 적용되어야 하는데 트랜잭션 커밋을 기다릴 필요가 없습니다. 만약 트랜잭션 안에서 이런 메서드를 호출하면 "Existing transaction found for transaction marked with propagation 'never'"라는 예외가 발생합니다. 이를 해결하려면 이벤트를 사용해서 트랜잭션 커밋 후에 비동기적으로 실행하도록 구조를 변경해야 합니다.

 

설정방법

Transactional(propagation = Propagation.NEVER)

 


NEVER 장점

트랜잭션이 있으면 안 되는 작업을 명확하게 표시할 수 있습니다. 이메일이나 SMS처럼 롤백할 수 없는 작업, 파일 시스템 조작처럼 트랜잭션 범위 밖의 작업을 보호합니다. 실수로 트랜잭션 안에서 호출하는 것을 방지해서 예상치 못한 부작용을 막을 수 있습니다. 성능상으로도 트랜잭션 오버헤드가 전혀 없습니다. 의도를 명확하게 표현하는 자체 문서화 효과도 있습니다.


NEVER 단점

사용할 수 있는 상황이 매우 제한적입니다. 대부분의 비즈니스 로직은 트랜잭션 안에서 실행되기 때문에 NEVER를 사용하면 통합하기 어려워집니다. 기존 코드에 NEVER를 추가하면 많은 곳에서 리팩토링이 필요할 수 있습니다. 또한 예외가 발생했을 때 해결 방법이 명확하지 않을 수 있습니다. "트랜잭션을 제거하라"는 것인데, 호출하는 쪽에서 트랜잭션이 꼭 필요한 경우라면 구조를 근본적으로 바꿔야 합니다.


어떤 상황에 사용해야 할까

외부 시스템과 통신할 때 사용합니다. 이메일 발송, SMS 전송, 외부 API 호출, 메시지 큐 발행 같은 작업들은 한 번 실행되면 취소할 수 없습니다. 파일 시스템 작업에도 적합합니다. 파일을 쓰거나 삭제하는 것은 데이터베이스 트랜잭션과 무관하게 즉시 반영됩니다. 캐시 조작에도 사용할 수 있습니다. 캐시를 무효화하거나 갱신하는 것은 트랜잭션 커밋을 기다릴 필요가 없고, 롤백해도 의미가 없습니다. 일부 데이터베이스의 DDL 명령어는 자동 커밋되기 때문에 NEVER와 함께 사용하는 것이 적절합니다.


7. NESTED - 큰 틀 안에서 작은 체크포인트 만들기

NESTED는 현재 트랜잭션이 있으면 중첩 트랜잭션을 만들고, 없으면 REQUIRED처럼 새 트랜잭션을 만듭니다. JDBC SavePoint 메커니즘을 사용합니다.

 

주문 처리 과정을 예로 들어보겠습니다.

주문을 생성하고, 포인트를 적립하고, 쿠폰을 발급한다고 해봅시다.

주문 생성은 반드시 성공해야 하지만, 포인트 적립이나 쿠폰 발급은 실패해도 주문 자체는 완료되어야 합니다. 이럴 때 포인트 적립과 쿠폰 발급에 NESTED를 사용하면 각각 독립적으로 롤백할 수 있습니다. 내부적으로는 SavePoint를 사용합니다. 주문을 저장한 후 SavePoint를 만들고, 포인트 적립을 시도합니다. 포인트 시스템에 장애가 발생하면 SavePoint로 롤백해서 포인트 적립만 취소하고 주문은 그대로 진행합니다. 다시 SavePoint를 만들고 쿠폰 발급을 시도합니다. 쿠폰 발급 한도 초과로 실패하면 쿠폰만 롤백하고 주문은 계속 진행합니다. 로그를 보면 "Creating nested transaction"으로 중첩 트랜잭션이 시작되고, 성공하면 "Releasing nested transaction savepoint", 실패하면 "Rolling back nested transaction to savepoint"가 나타납니다. REQUIRES_NEW와의 차이점이 중요합니다.

 

REQUIRES_NEW는 완전히 독립적인 새 트랜잭션을 만들어서 외부 트랜잭션이 롤백되어도 영향을 받지 않습니다. 하지만 NESTED는 여전히 외부 트랜잭션에 종속되어 있습니다.

 

외부 트랜잭션이 롤백되면 중첩 트랜잭션도 함께 롤백됩니다. 배치 처리에서도 유용합니다. 천 건의 주문을 처리하는데 일부가 실패해도 나머지는 계속 처리하고 싶을 때 각 주문 처리를 NESTED로 감싸면 됩니다. 실패한 주문만 롤백하고 다음 주문을 계속 처리할 수 있습니다. 다만 몇 가지 제약사항이 있습니다. JDBC 3.0 이상에서 SavePoint를 지원해야 하고, JPA를 사용할 때는 영속성 컨텍스트 관리에 주의해야 합니다. NESTED가 롤백되어도 영속성 컨텍스트의 엔티티 상태는 변경된 채로 남아있을 수 있기 때문입니다. 이럴 때는 flush와 clear를 적절히 사용해야 합니다.

 

설정방법

@Transactional(propagation = Propagation.NESTED)

NESTED 장점  

부분적인 롤백이 가능하다는 것이 가장 큰 장점입니다. 핵심 작업은 성공시키면서 부가적인 작업의 실패는 허용할 수 있습니다. SavePoint를 사용하기 때문에 REQUIRES_NEW보다 오버헤드가 적습니다. 같은 커넥션을 사용하고 물리적으로 새로운 트랜잭션을 만들지 않기 때문입니다. 배치 처리에서 특히 유용한데, 수천 건을 처리하다가 일부가 실패해도 나머지는 계속 처리할 수 있습니다. 복잡한 비즈니스 로직을 단계별로 제어할 수 있습니다.


NESTED 단점  

JDBC SavePoint를 지원하는 데이터베이스와 드라이버가 필요합니다.

일부 구형 시스템에서는 동작하지 않을 수 있습니다.

JPA나 Hibernate를 사용할 때는 영속성 컨텍스트 관리가 복잡해집니다.

NESTED가 롤백되어도 영속성 컨텍스트의 엔티티는 변경된 상태로 남아있어서 수동으로 관리해야 합니다.

또한 외부 트랜잭션이 롤백되면 중첩 트랜잭션도 함께 롤백되기 때문에 완전한 독립성은 없습니다.

디버깅도 복잡한데,SavePoint가 여러 개 생성되면 어떤 시점으로 롤백됐는지 추적하기 어렵습니다.


8. 전체 정리

전파 레벨                                                                 기존 트랜잭션 존재 시                               핵심 특징
REQUIRED 참여 기본값
REQUIRES_NEW 중단 후 새로 생성 독립 트랜잭션
SUPPORTS 참여 선택적
NOT_SUPPORTED 중단 트랜잭션 제거
MANDATORY 없으면 예외 강제
NEVER 있으면 예외 거의 안 씀
NESTED Savepoint 부분 롤백

마치며

트랜잭션 전파 레벨을 정리하면서 가장 중요하다고 느낀 것은 "각 레벨이 해결하려는 문제가 무엇인가"를 이해하는 것이었습니다. 이제 @Transactional을 단순히 붙이는 것이 아니라, 각 상황에 맞는 적절한 전파 레벨을 선택할 수 있게 되었기를 바랍니다. 트랜잭션을 제대로 이해하고 사용하면 더 견고하고 예측 가능한 애플리케이션을 만들 수 있다는 생각이 들었습니다

 

저작자표시 비영리 (새창열림)

'스프링' 카테고리의 다른 글

Dynamic Proxy vs CGLIB 방식 차이  (0) 2026.02.21
Mockito 핵심 개념 Mock, Stub, Spy(feat : JUnit5)  (0) 2026.02.16
FIRST 원칙으로 바라본 테스트 코드 작성법  (0) 2026.01.30
Spring Validation 완전 정복 - 올바른 유효성 검증 전략  (0) 2026.01.29
스프링에서 스코프(Scope)란 무엇인가?  (0) 2026.01.19
'스프링' 카테고리의 다른 글
  • Dynamic Proxy vs CGLIB 방식 차이
  • Mockito 핵심 개념 Mock, Stub, Spy(feat : JUnit5)
  • FIRST 원칙으로 바라본 테스트 코드 작성법
  • Spring Validation 완전 정복 - 올바른 유효성 검증 전략
깊은바다속꼬북이
깊은바다속꼬북이
  • 깊은바다속꼬북이
    CodeBlossom
    깊은바다속꼬북이
  • 전체
    오늘
    어제
    • 분류 전체보기 (53) N
      • 라이징 캠프 (4)
      • 객채지향 개발론 (3)
      • 스프링 (10) N
      • 네트워크 (2)
      • 자바 (16)
      • 자료구조 (3)
      • 운영체제 (0)
      • 데이터베이스 (4)
      • 디자인패턴 (7)
      • JSP (1)
      • 개발 알쓸신잡 (2)
      • 일반 교양 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    java data area
    디자인 패턴
    개발 철학
    JUnnit5
    자료구조
    java 버전별 특징
    템플릿 메서드 패턴(Template Method Pattern)
    프로그램밍 언어
    JVM
    mockito라이브러리
    jvm 클래스 로더
    디자인패턴
    스프링
    개발자 철학
    어댑터 패턴(Adapter Pattern)
    싱글턴 패턴(Singleton Pattern)
    GC
    spring
    자바 Socket 클래스
    백엔드
    트랜잭션 전파레벨
    전략 패턴(Strategy Pattern)
    객체지향
    개발 교훈
    MySQL 옵티마이저
    junnit5프레임워크
    한국어 검색
    MySQL 파서
    jit-compiler
    java
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
깊은바다속꼬북이
스프링 트랜잭션(@Transactional) 전파레벨 7가지
상단으로

티스토리툴바