이미 진행 중인 트랜잭션이 있을 때 새 트랜잭션 메서드를 어떻게 처리할지 정하는 옵션.
주요 값
| 값 | 기존 트랜잭션이 있으면 | 없으면 |
|---|---|---|
| REQUIRED(기본) | 참여한다 | 새로 시작 |
| REQUIRES_NEW | 일시 중단하고 새로 시작 | 새로 시작 |
| SUPPORTS | 참여 | 트랜잭션 없이 실행 |
| MANDATORY | 참여 | 예외 |
| NOT_SUPPORTED | 중단하고 트랜잭션 없이 | 트랜잭션 없이 |
| NEVER | 예외 | 트랜잭션 없이 |
| NESTED | 세이브포인트를 만든다 | 새로 시작 |
REQUIRED — 하나로 합쳐진다
@Transactional void outer() { inner(); } // 같은 트랜잭션
@Transactional void inner() { ... }
물리 트랜잭션은 하나다
- inner 가 실패하면 outer 까지 함께 롤백된다
- "로그만 남기고 계속 진행" 이 불가능하다
REQUIRES_NEW — 독립시킨다
@Transactional void outer() {
repo.save(order);
try { logService.write(...); } catch (Exception ignored) { } // 실패해도 주문은 유지
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
void write(...) { ... }
감사 로그처럼 "본 작업과 운명을 같이하면 안 되는" 작업에 쓴다.
REQUIRES_NEW의 비용 — 커넥션을 하나 더 쓴다
바깥 트랜잭션이 커넥션 1개를 쥔 채 대기하고 안쪽이 커넥션을 하나 더 빌린다
- 요청당 커넥션 2개
풀 크기가 10인데 동시 요청 10개가 들어오면
- 전부 첫 커넥션을 쥐고 두 번째를 기다린다 → 데드락
커넥션 풀 크기가 이 패턴의 상한이라는 것을 반드시 계산해야 한다.
자기 호출에서는 전파가 동작하지 않는다
@Transactional
void outer() { this.inner(); } // 프록시를 안 탄다
@Transactional(propagation = REQUIRES_NEW)
void inner() { ... } // 그냥 outer 트랜잭션에서 실행된다
전파 옵션 문제로 보이는 사고의 상당수가 사실 자기 호출이다.
면접 함정
- ❌ "REQUIRES_NEW로 하면 안전하다" → 커넥션을 두 배로 쓴다. 풀 고갈·데드락의 원인이 된다.
- ❌ "NESTED는 어디서나 된다" → JDBC 세이브포인트를 지원하는 환경이어야 하고, JPA에서는 제약이 있다.
지금 트랜잭션 상태를 확인한다
TransactionSynchronizationManager.isActualTransactionActive(); // 트랜잭션 안인가
TransactionSynchronizationManager.getCurrentTransactionName(); // 어느 메서드가 열었나
TransactionSynchronizationManager.isCurrentTransactionReadOnly();
# 로그로 경계를 추적한다
logging.level.org.springframework.transaction.interceptor=TRACE
# Creating new transaction with name [...]
# Participating in existing transaction ← REQUIRED 가 합류한 것
# Suspending current transaction, creating new ← REQUIRES_NEW
"Participating in existing"이 찍히면 새 트랜잭션이 아니라 기존에 합류한 것이다. 전파 옵션이 의도대로 동작하는지 확인하는 가장 확실한 방법이다.
UnexpectedRollbackException이 뜨는 이유
inner(REQUIRED) 가 예외로 실패한다
-
물리 트랜잭션에 rollback-only 표시가 붙는다 outer 가 그 예외를 try-catch 로 삼킨다
-
outer 는 커밋을 시도한다
-
이미 rollback-only 라 커밋할 수 없다
-
UnexpectedRollbackException: Transaction silently rolled back
"예외를 잡았는데 왜 롤백되지" 의 답이다. 삼키고 계속 진행하려면 inner를 REQUIRES_NEW로 독립시켜야 한다.