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 미지원

경계 조건: 감사 로그 시나리오

@Transactional인 OrderService.placeOrder()가 다른 빈 AuditLogService.save()를 호출하고, 주문이 실패해도 감사 로그는 남아야 하는 상황에서 헷갈리기 쉬운 경계다. 원문은 이 시나리오를 직접 다루지 않는다. @Transactional 제거, 예외 전달, 호출자 catch 항목은 원문의 프록시 흐름과 롤백 규칙에서 도출한 Wiki 해석이다.

호출자가 예외를 잡을 때

감사 로그 저장이 실패해도 주문은 커밋되게 하려고 placeOrder()가 save() 호출을 try-catch로 감싸고 정상 반환하는 경우다. 결과는 save()의 전파 속성에 따라 갈린다.

save() 전파 예외가 지나간 뒤 상태 주문 결과
REQUIRES_NEW 감사 로그 트랜잭션만 안쪽 프록시가 롤백했고, 주문 트랜잭션에는 표시가 없다 커밋
REQUIRED 예외가 save()의 프록시를 통과하며 공유 트랜잭션에 롤백 전용 표시가 남는다 커밋 시점에 UnexpectedRollbackException, 롤백

관련

출처


  1. Transaction.md — "트랜잭션이 이미 진행 중인 상태에서 또 다른 트랜잭션 메서드가 호출될 때, 어떻게 트랜잭션을 합치거나 나눌지 결정하는 설정입니다.", 전파 속성 표의 REQUIRED, REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER, NESTED 7개 행. ↩︎ ↩︎

  2. Transaction.md — "스프링의 선언적 트랜잭션은 **Spring AOP(관점 지향 프로그래밍)**를 바탕으로 동작합니다.", "클라이언트가 메서드를 호출하면 프록시 객체의 메서드가 대신 먼저 호출되며, 내부의 PlatformTransactionManager를 통해 DB 트랜잭션을 시작(begin)합니다.", "트랜잭션이 열린 상태에서 실제 Target 객체의 비즈니스 로직 메서드를 실행합니다." ↩︎ ↩︎

  3. Transaction.md — "REQUIRED | 기본값. 진행 중인 트랜잭션이 있으면 참여하고, 없으면 새로 생성합니다.", "SUPPORTS | 진행 중인 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 진행합니다." ↩︎ ↩︎ ↩︎ ↩︎

  4. Transaction.md — "REQUIRES_NEW | 진행 중인 트랜잭션 여부와 상관없이 항상 새로운 트랜잭션을 시작합니다. 기존 트랜잭션은 보류(Suspend)됩니다." ↩︎ ↩︎ ↩︎

  5. Transaction.md — "MANDATORY | 반드시 이미 존재하는 트랜잭션 내에서만 실행되어야 합니다. 진행 중인 트랜잭션이 없으면 예외가 발생합니다.", "NOT_SUPPORTED | 트랜잭션을 지원하지 않습니다. 진행 중인 트랜잭션이 있으면 보류하고 트랜잭션 없이 실행합니다.", "NEVER | 트랜잭션을 사용하지 않습니다. 진행 중인 트랜잭션이 있으면 예외가 발생합니다." ↩︎ ↩︎ ↩︎ ↩︎

  6. Transaction.md — "NESTED | 이미 진행 중인 트랜잭션이 있으면 중첩(Nested) 트랜잭션을 시작합니다. 자식 트랜잭션 롤백은 부모에게 영향을 주지 않지만, 부모가 롤백되면 자식도 함께 롤백됩니다. (DB Savepoint 기술 사용) | JPA에서는 미지원" ↩︎ ↩︎ ↩︎

  7. Transaction.md — "트랜잭션이 열린 상태에서 실제 Target 객체의 비즈니스 로직 메서드를 실행합니다.", "메서드 실행 중 예외(Exception)가 발생하면 예외 종류에 따라 롤백(Rollback) 또는 커밋을 수행합니다.", sequence diagram의 "Proxy-->>Client: 호출 결과 반환 (또는 예외 전달)", "RuntimeException과 그 하위 클래스들 ... 자동으로 트랜잭션을 롤백합니다." 감사 로그 시나리오와 @Transactional 제거, REQUIRES_NEW 내부 예외의 전달과 호출자 catch, 예외를 잡는 위치에 따른 차이는 원문에 없으며, 이 흐름을 적용한 Wiki 해석이다. ↩︎ ↩︎ ↩︎ ↩︎

  8. 인증 실패 응답·좋아요 중복 방지 노트 — "Spring: 리포지토리에서 예외가 나는 순간 바깥 트랜잭션을 "롤백 전용"으로 표시한다. 조회를 안 해도 커밋할 때 UnexpectedRollbackException이 난다.", Postgres vs MySQL 표의 "@Transactional + catch | 500 | 그래도 500 (Spring 롤백 전용 표시 때문)", "save()와 delete()에는 Spring Data가 기본으로 @Transactional을 붙여 둔다." ↩︎ ↩︎