이미 트랜잭션이 진행 중일 때 새 트랜잭션 메서드를 만나면 어떻게 할지 정하는 규칙 (propagation).
왜 필요한가
@Transactional
public void order() { // 트랜잭션 A 시작
saveOrder();
sendNotification(); // ← 이 안에도 @Transactional 이 있다면?
} // A에 참여? 따로 시작? 무시?
이 질문에 답하는 것이 전파 속성이다.
주요 옵션
| 옵션 | 진행 중인 트랜잭션이 있으면 | 없으면 |
|---|---|---|
| REQUIRED (기본) | 참여 | 새로 시작 |
| REQUIRES_NEW | 일시 중단하고 새로 시작 | 새로 시작 |
| SUPPORTS | 참여 | 트랜잭션 없이 실행 |
| NOT_SUPPORTED | 일시 중단하고 트랜잭션 없이 | 트랜잭션 없이 |
| MANDATORY | 참여 | 예외 |
| NEVER | 예외 | 트랜잭션 없이 |
| NESTED | 세이브포인트 생성 | 새로 시작 |
실무에서 쓰는 것은 사실상 REQUIRED와 REQUIRES_NEW 둘이다.
REQUIRED — 하나로 묶인다
order() ─── 트랜잭션 A 시작
└─ saveOrder() A에 참여
└─ sendNotification() A에 참여
모두 A. 하나라도 실패하면 전부 롤백
물리 트랜잭션은 하나, 논리 트랜잭션은 여럿이라는 구조다. 가장 바깥이 커밋을 결정한다.
가장 헷갈리는 지점 — 내부에서 예외를 잡아도 롤백된다
@Transactional
public void outer() {
try {
inner(); // 여기서 예외 → 롤백 표시(rollback-only)
} catch (Exception e) {
log.warn("무시하고 계속"); // 잡았으니 괜찮다고 생각하지만
}
save(); // 저장한 것 같지만
} // 커밋 시점: UnexpectedRollbackException — 전부 롤백된다 ✗
왜 그런가 — 같은 물리 트랜잭션이므로, 안쪽에서 실패하면 트랜잭션 전체에 rollback-only 플래그가 찍힌다. 바깥에서 예외를 삼켜도 이 플래그는 지워지지 않는다.
"일부만 실패해도 계속 진행"이 필요하면 REQUIRES_NEW를 써야 한다.
REQUIRES_NEW — 완전히 분리된다
order() ─── 트랜잭션 A
└─ writeAuditLog() ─── A를 잠시 멈추고 트랜잭션 B 시작
B 커밋 (독립적)
└─ A 계속 → A가 롤백돼도 B는 남는다
용도 — 감사 로그, 실패 이력처럼 본 작업이 롤백돼도 남아야 하는 기록.
대가
- 커넥션을 2개 동시에 점유한다 (A는 멈춘 채로 유지)
- 커넥션 풀 크기보다 중첩이 깊어지면 데드락
- 풀 크기 10인데 10개 요청이 각각 REQUIRES_NEW 를 부르면
- 전부 두 번째 커넥션을 기다리며 멈춘다 ★
★ 이것이 실무에서 실제로 발생하는 사고다. REQUIRES_NEW를 남발하면 안 된다.
NESTED — 세이브포인트
- A 트랜잭션 안에서 세이브포인트를 만든다
- 안쪽이 실패하면 세이브포인트까지만 롤백, 바깥은 계속
- 바깥이 롤백되면 안쪽도 함께 롤백된다 (REQUIRES_NEW와 다른 점) JDBC 세이브포인트를 지원해야 하며, JPA에서는 잘 쓰이지 않는다.
롤백 규칙 — 또 하나의 함정
@Transactional
public void save() throws IOException {
repo.save(entity);
throw new IOException(); // checked 예외 → 롤백되지 않는다!
}
기본값은 unchecked(RuntimeException, Error)에만 롤백한다.
@Transactional(rollbackFor = Exception.class) // 명시해야 한다
"예외가 났는데 데이터가 저장돼 있다"의 원인 대부분이 이것이다.
readOnly
@Transactional(readOnly = true)
- 하이버네이트가 스냅샷을 만들지 않아 더티 체킹을 건너뛴다 → 메모리·성능 이득
- JDBC 드라이버·DB에 읽기 전용 힌트 전달
- 읽기 전용 복제본으로 라우팅하는 근거로 쓸 수 있다
조회 전용 서비스에는 붙이는 것이 좋다.
프록시 기반이라는 전제
AOP 프록시로 동작하므로 자기 호출에는 전파가 적용되지 않는다.
@Transactional
public void outer() { this.inner(); } // inner의 @Transactional 무시
@Transactional(propagation = REQUIRES_NEW)
public void inner() { } // 그냥 outer의 트랜잭션에서 실행
클래스를 분리해야 한다.