Srpingboot Transaction
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 기반 부분 롤백
결국 실무에서는 단순히 옵션 이름을 외우는 것보다,
“어디까지 같이 롤백돼야 하는가”와 “무엇은 별도로 남아야 하는가”를 먼저 정의한 뒤 전파 옵션을 선택 해야 한다.
댓글남기기