Transaction Propagation
전파 속성(Propagation)은 **"이미 진행 중인 트랜잭션이 있을 때 새 호출이 어떻게 행동할 것인가"**를 결정한다. Spring은 @Transactional의 propagation 옵션으로 7가지 전략을 제공한다.[1] Spring Transaction의 선언적 트랜잭션은 Spring AOP 프록시가 호출을 먼저 받아 트랜잭션을 시작한 뒤 실제 메서드를 실행하는 방식으로 동작한다.[2] 따라서 전파 결정도 프록시가 메서드를 위임할 때 적용된다고 볼 수 있지만, 이는 두 설명을 연결한 이 Wiki의 해석이며 원문이 직접 진술한 내용은 아니다.[1:1][2:1]
7가지 전파 옵션
| 옵션 | 동작 | 실무 유스케이스 |
|---|---|---|
REQUIRED(Default) |
진행 중인 트랜잭션이 있으면 참여, 없으면 새로 생성[3] | 대부분의 단일 비즈니스 흐름. 부모-자식이 한 트랜잭션으로 묶임. |
REQUIRES_NEW |
항상 새로운 트랜잭션 생성, 기존 트랜잭션은 보류(Suspend)[4] | 실패해도 반드시 기록해야 하는 [[Wiki/backend/patterns/audit-log.md |
MANDATORY |
반드시 이미 존재하는 트랜잭션 내에서만 실행. 없으면 예외[5] | 부모 트랜잭션 존재를 강제할 때 |
SUPPORTS |
진행 중이면 참여, 없으면 트랜잭션 없이 진행[3:1] | |
NOT_SUPPORTED |
트랜잭션 지원 안 함. 진행 중이면 보류하고 non-transactional로 실행[5:1] | |
NEVER |
트랜잭션 사용 안 함. 진행 중이면 예외[5:2] | 트랜잭션 존재 자체를 금지할 때 |
NESTED |
진행 중이면 중첩(Savepoint) 트랜잭션 시작. 자식 롤백은 부모에 영향 안 주지만, 부모 롤백은 자식도 롤백[6] | 자식 실패가 부모에 영향 주지 않아야 할 때. 단, JPA 미지원 |
REQUIRED는 기본값이며, 기존 트랜잭션에 참여하거나 없으면 새로 생성한다.[3:2]REQUIRES_NEW는 진행 중인 트랜잭션과 무관하게 항상 새 트랜잭션을 시작하고, 기존 트랜잭션은 보류된다.[4:1]NESTED는 DB Savepoint 기반으로 자식 롤백이 부모에 영향을 주지 않지만, JPA에서는 미지원된다.[6:1]NOT_SUPPORTED/NEVER는 트랜잭션을 쓰지 않거나 존재를 금지할 때 쓴다.MANDATORY는 반드시 기존 트랜잭션이 있어야 한다.[5:3]
경계 조건: 감사 로그 시나리오
@Transactional인 OrderService.placeOrder()가 다른 빈 AuditLogService.save()를 호출하고, 주문이 실패해도 감사 로그는 남아야 하는 상황에서 헷갈리기 쉬운 경계다. 원문은 이 시나리오를 직접 다루지 않는다. @Transactional 제거, 예외 전달, 호출자 catch 항목은 원문의 프록시 흐름과 롤백 규칙에서 도출한 Wiki 해석이다.
- 기본값
REQUIRED에서save()는placeOrder()의 트랜잭션에 참여하므로, 주문이 롤백되면 감사 로그도 함께 롤백된다.[3:3] save()에서@Transactional을 지워도 감사 로그는 독립되지 않는다. 바깥 프록시가 트랜잭션을 연 상태에서 비즈니스 로직이 실행되므로, 그 안에서 호출된save()의 DB 작업도 같은 트랜잭션에서 실행된다고 볼 수 있다. 독립시키려면 어노테이션을 지우는 대신propagation을 바꿔야 한다.[7]NESTED는 부모가 롤백되면 자식도 롤백되므로 이 요구사항에 맞지 않는다. 항상 새 트랜잭션을 시작하는REQUIRES_NEW가 맞다.[4:2][6:2]REQUIRES_NEW는 트랜잭션을 분리할 뿐 예외를 분리하지 않는다.save()에서RuntimeException이 나면save()의 프록시가 감사 로그 트랜잭션을 롤백하고 예외를 호출자에게 전달한다.placeOrder()가 이를 잡지 않으면 예외가placeOrder()의 프록시에 도달해 주문 트랜잭션도 롤백된다.[7:1]
호출자가 예외를 잡을 때
감사 로그 저장이 실패해도 주문은 커밋되게 하려고 placeOrder()가 save() 호출을 try-catch로 감싸고 정상 반환하는 경우다. 결과는 save()의 전파 속성에 따라 갈린다.
save() 전파 |
예외가 지나간 뒤 상태 | 주문 결과 |
|---|---|---|
REQUIRES_NEW |
감사 로그 트랜잭션만 안쪽 프록시가 롤백했고, 주문 트랜잭션에는 표시가 없다 | 커밋 |
REQUIRED |
예외가 save()의 프록시를 통과하며 공유 트랜잭션에 롤백 전용 표시가 남는다 |
커밋 시점에 UnexpectedRollbackException, 롤백 |
REQUIRES_NEW에서는 예외가 바깥 프록시에 도달하지 않으므로 주문이 커밋된다. 트랜잭션이 이미 분리돼 있어서 catch만으로 주문을 지킬 수 있다.[7:2]REQUIRED에서는 바깥에서 잡고 정상 반환해도 롤백 전용 표시가 지워지지 않는다. 프록시가 커밋을 시도하면 실제로는 롤백되고UnexpectedRollbackException이 나므로, 컨트롤러는 정상 응답 대신 예외(보통 500)를 받는다.[8] 원문은 리포지토리에서 난 예외를 다루며,REQUIRED로 참여한 다른 서비스 빈에 같은 동작을 적용한 것은 Wiki 해석이다.- 표시가 생기는 기준은 예외가 어디서 났는지가 아니라
@Transactional메서드 밖으로 그 프록시를 통과해 나갔는지다. 안쪽@Transactional메서드가Integer.parseInt()같은 일반 코드의 예외를 자기 안에서 잡고 정상 반환하면 표시가 남지 않아 주문은 커밋된다.[7:3] 반대로 Spring Data 리포지토리save()처럼 그 자체가@Transactional인 메서드의 예외는 서비스에서 잡아도 표시가 남는다.[8:1] 이 경계는 Spring Transaction의 롤백 규칙 절에 있다.
관련
- Spring Transaction — 전파 속성은 트랜잭션 경계 정책의 한 축이다(나머지는 롤백 규칙, 격리 수준).
- Spring AOP — 트랜잭션은 프록시가 호출을 받아 시작하므로, 전파 결정도 프록시 위임 시점에 적용된다고 볼 수 있다. 이는 도입 문단과 같은 이 Wiki의 해석이다.
- Audit Log —
REQUIRES_NEW의 대표 유스케이스. 부모 트랜잭션이 롤백돼도 감사 로그는 살아남아야 한다.
출처
Transaction.md — "트랜잭션이 이미 진행 중인 상태에서 또 다른 트랜잭션 메서드가 호출될 때, 어떻게 트랜잭션을 합치거나 나눌지 결정하는 설정입니다.", 전파 속성 표의
REQUIRED,REQUIRES_NEW,MANDATORY,SUPPORTS,NOT_SUPPORTED,NEVER,NESTED7개 행. ↩︎ ↩︎Transaction.md — "스프링의 선언적 트랜잭션은 **Spring AOP(관점 지향 프로그래밍)**를 바탕으로 동작합니다.", "클라이언트가 메서드를 호출하면 프록시 객체의 메서드가 대신 먼저 호출되며, 내부의
PlatformTransactionManager를 통해 DB 트랜잭션을 시작(begin)합니다.", "트랜잭션이 열린 상태에서 실제 Target 객체의 비즈니스 로직 메서드를 실행합니다." ↩︎ ↩︎Transaction.md — "REQUIRED | 기본값. 진행 중인 트랜잭션이 있으면 참여하고, 없으면 새로 생성합니다.", "SUPPORTS | 진행 중인 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 진행합니다." ↩︎ ↩︎ ↩︎ ↩︎
Transaction.md — "REQUIRES_NEW | 진행 중인 트랜잭션 여부와 상관없이 항상 새로운 트랜잭션을 시작합니다. 기존 트랜잭션은 보류(Suspend)됩니다." ↩︎ ↩︎ ↩︎
Transaction.md — "MANDATORY | 반드시 이미 존재하는 트랜잭션 내에서만 실행되어야 합니다. 진행 중인 트랜잭션이 없으면 예외가 발생합니다.", "NOT_SUPPORTED | 트랜잭션을 지원하지 않습니다. 진행 중인 트랜잭션이 있으면 보류하고 트랜잭션 없이 실행합니다.", "NEVER | 트랜잭션을 사용하지 않습니다. 진행 중인 트랜잭션이 있으면 예외가 발생합니다." ↩︎ ↩︎ ↩︎ ↩︎
Transaction.md — "NESTED | 이미 진행 중인 트랜잭션이 있으면 중첩(Nested) 트랜잭션을 시작합니다. 자식 트랜잭션 롤백은 부모에게 영향을 주지 않지만, 부모가 롤백되면 자식도 함께 롤백됩니다. (DB Savepoint 기술 사용) | JPA에서는 미지원" ↩︎ ↩︎ ↩︎
Transaction.md — "트랜잭션이 열린 상태에서 실제 Target 객체의 비즈니스 로직 메서드를 실행합니다.", "메서드 실행 중 예외(Exception)가 발생하면 예외 종류에 따라 롤백(Rollback) 또는 커밋을 수행합니다.", sequence diagram의 "Proxy-->>Client: 호출 결과 반환 (또는 예외 전달)", "
RuntimeException과 그 하위 클래스들 ... 자동으로 트랜잭션을 롤백합니다." 감사 로그 시나리오와@Transactional제거,REQUIRES_NEW내부 예외의 전달과 호출자 catch, 예외를 잡는 위치에 따른 차이는 원문에 없으며, 이 흐름을 적용한 Wiki 해석이다. ↩︎ ↩︎ ↩︎ ↩︎인증 실패 응답·좋아요 중복 방지 노트 — "Spring: 리포지토리에서 예외가 나는 순간 바깥 트랜잭션을 "롤백 전용"으로 표시한다. 조회를 안 해도 커밋할 때
UnexpectedRollbackException이 난다.", Postgres vs MySQL 표의 "@Transactional+ catch | 500 | 그래도 500 (Spring 롤백 전용 표시 때문)", "save()와delete()에는 Spring Data가 기본으로@Transactional을 붙여 둔다." ↩︎ ↩︎