트랜잭션 전파 — 트랜잭션 안에서 또 다른 트랜잭션을 시작하면 어떻게 될까
주문을 처리하는 트랜잭션 안에서 로그를 저장하는 메서드를 호출했는데, 로그 저장이 실패하면 주문도 롤백되어야 할까요?
@Transactional이 붙은 메서드가 다른 @Transactional 메서드를 호출하면 어떤 일이 벌어질까요? 이때 전파(Propagation) 속성이 트랜잭션 간의 관계를 결정합니다.
개념 정의
** 트랜잭션 전파(Propagation)**는 트랜잭션이 이미 진행 중인 상황에서 새로운 트랜잭션이 요청될 때의 동작 방식을 정의합니다.
@Transactional // 외부 트랜잭션
public void orderProcess() {
orderRepository.save(order);
logService.saveLog(log); // 내부 트랜잭션 — 전파 속성에 따라 동작이 달라짐
}
7가지 전파 속성
| 속성 | 기존 트랜잭션 있을 때 | 기존 트랜잭션 없을 때 |
|---|---|---|
| REQUIRED (기본) | 참여 | 새로 시작 |
| REQUIRES_NEW | 새로 시작 (기존 중단) | 새로 시작 |
| NESTED | 중첩 트랜잭션 | 새로 시작 |
| SUPPORTS | 참여 | 트랜잭션 없이 실행 |
| NOT_SUPPORTED | 중단, 트랜잭션 없이 실행 | 트랜잭션 없이 실행 |
| MANDATORY | 참여 | 예외 발생 |
| NEVER | 예외 발생 | 트랜잭션 없이 실행 |
실무에서 가장 많이 사용하는 것은 REQUIRED, REQUIRES_NEW, NESTED 세 가지입니다.
물리 트랜잭션과 논리 트랜잭션
Spring은 내부적으로 ** 물리 트랜잭션 **과 ** 논리 트랜잭션 **을 구분합니다.
물리 트랜잭션 (실제 DB 커넥션의 트랜잭션)
├── 논리 트랜잭션 1 (외부: orderProcess)
└── 논리 트랜잭션 2 (내부: saveLog, REQUIRED로 참여)
핵심 규칙은 다음과 같습니다.
- ** 모든 논리 트랜잭션이 커밋되어야** 물리 트랜잭션이 커밋됩니다.
- ** 하나라도 롤백되면** 물리 트랜잭션이 롤백됩니다.
REQUIRED (기본값)
@Service
public class OrderService {
@Transactional // 물리 트랜잭션 시작
public void createOrder(OrderRequest request) {
orderRepository.save(order);
memberService.updatePoint(member); // REQUIRED → 기존 트랜잭션에 참여
}
}
@Service
public class MemberService {
@Transactional // 기존 트랜잭션이 있으므로 참여
public void updatePoint(Member member) {
member.addPoint(100);
}
}
내부 롤백 시 주의점
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(order);
try {
logService.saveLog(log); // REQUIRED, 내부에서 예외 발생 → 롤백 마크!
} catch (Exception e) {
// 예외를 잡아도 이미 rollback-only 마크가 설정됨
}
// 여기서 커밋을 시도하면 UnexpectedRollbackException 발생!
}
REQUIRED로 참여한 내부 트랜잭션이 롤백되면, 같은 물리 트랜잭션에 rollback-only 마크가 설정됩니다. 외부에서 try-catch로 예외를 잡아도 커밋 시점에 UnexpectedRollbackException이 발생합니다.
REQUIRES_NEW
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Log log) {
// 완전히 독립된 새 물리 트랜잭션에서 실행
logRepository.save(log);
}
}
물리 트랜잭션 1 (외부: orderProcess) ─── 일시 중단
물리 트랜잭션 2 (내부: saveLog) ─── 독립 실행
물리 트랜잭션 1 재개
REQUIRES_NEW의 특징은 다음과 같습니다.
- ** 별도의 물리 트랜잭션 **(별도의 DB 커넥션)을 사용합니다.
- 내부 트랜잭션의 롤백이 외부 트랜잭션에 영향을 주지 않습니다.
- DB 커넥션을 2개 사용하므로 커넥션 풀 부족에 주의해야 합니다.
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(order);
try {
logService.saveLog(log); // REQUIRES_NEW
} catch (Exception e) {
// 로그 저장 실패해도 주문은 커밋 가능
log.warn("로그 저장 실패", e);
}
}
NESTED
@Transactional(propagation = Propagation.NESTED)
public void saveLog(Log log) {
// SAVEPOINT가 설정된 중첩 트랜잭션
logRepository.save(log);
}
NESTED는 ** 같은 물리 트랜잭션** 내에서 SAVEPOINT를 사용합니다.
- 내부 트랜잭션 롤백 → SAVEPOINT까지만 롤백, 외부는 계속 진행 가능
- 외부 트랜잭션 롤백 → 내부도 함께 롤백
물리 트랜잭션 (1개의 DB 커넥션)
├── 외부 트랜잭션
│ ├── SAVEPOINT 설정
│ ├── 중첩 트랜잭션 (saveLog)
│ │ ├── 성공 → SAVEPOINT 해제, 계속 진행
│ │ └── 실패 → SAVEPOINT까지 롤백, 외부는 계속 가능
│ └── 외부 커밋 또는 롤백
REQUIRES_NEW vs NESTED
| 항목 | REQUIRES_NEW | NESTED |
|---|---|---|
| 물리 트랜잭션 | 별도 생성 | 같은 트랜잭션 |
| DB 커넥션 | 2개 | 1개 |
| 내부 롤백 시 외부 | 영향 없음 | 영향 없음 (SAVEPOINT) |
| 외부 롤백 시 내부 | 영향 없음 | 함께 롤백 |
| JDBC 드라이버 | 제한 없음 | SAVEPOINT 지원 필요 |
실무 활용 패턴
독립적인 로그 저장
@Transactional
public void processPayment(PaymentRequest request) {
// 핵심 비즈니스 로직
paymentRepository.save(payment);
// 로그는 결제와 독립적으로 처리
try {
auditService.saveAuditLog(request); // REQUIRES_NEW
} catch (Exception e) {
log.error("감사 로그 저장 실패", e);
// 결제는 정상 진행
}
}
이벤트 발행과 트랜잭션
@Transactional
public void createOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
// 트랜잭션 커밋 후 이벤트 발행 (TransactionalEventListener)
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
}
주의할 점
1. REQUIRES_NEW는 DB 커넥션을 2개 사용하므로 커넥션 풀 고갈에 주의해야 한다
REQUIRES_NEW는 기존 트랜잭션을 일시 중단하고 새 커넥션으로 독립 트랜잭션을 엽니다. 동시 요청이 많은 상황에서 REQUIRES_NEW가 중첩되면 커넥션 풀이 순식간에 고갈되고, 대기 타임아웃으로 전체 서비스가 멈출 수 있습니다.
2. REQUIRED에서 내부 롤백 시 try-catch로 잡아도 전체가 롤백된다
REQUIRED로 참여한 내부 트랜잭션이 예외를 던지면 물리 트랜잭션에 rollback-only 마크가 설정됩니다. 외부에서 try-catch로 예외를 잡아도 커밋 시점에 UnexpectedRollbackException이 발생합니다. 내부 실패를 무시하고 싶다면 REQUIRES_NEW나 NESTED를 사용해야 합니다.
3. NESTED는 JPA 환경에서 지원되지 않는 경우가 많다
NESTED는 SAVEPOINT를 사용하는데, JPA의 JpaTransactionManager는 기본적으로 SAVEPOINT를 지원하지 않습니다. nestedTransactionAllowed 설정이 필요하고, Hibernate와 DB 드라이버 모두 SAVEPOINT를 지원해야 합니다. 실무에서 JPA를 사용한다면 NESTED 대신 REQUIRES_NEW를 먼저 고려하는 것이 현실적입니다.
정리
- ** 전파(Propagation)**는 트랜잭션이 진행 중일 때 새로운 트랜잭션의 동작 방식을 결정합니다.
- REQUIRED(기본값)는 기존 트랜잭션에 참여하며, 내부 롤백 시 전체가 롤백됩니다.
- REQUIRES_NEW 는 독립된 물리 트랜잭션을 생성하지만, DB 커넥션을 추가로 사용합니다.
- NESTED 는 SAVEPOINT를 사용하여 같은 커넥션 내에서 부분 롤백이 가능합니다.
- REQUIRED에서 내부 롤백 시 rollback-only 마크 때문에 try-catch로도 커밋을 막을 수 없습니다.