트랜잭션이 이미 진행중인 로직에 추가로 트랜잭션을 수행할 경우 트랜잭션이 어떻게 동작할지 결정하는 것을 트랜잭션 전파(transaction propagation)이라고 한다. 가장 자주 사용하고 디폴트 옵션인 REQUIRED옵션을 기준으로 설명하도록 하겠다.

외부 트랜잭션을 먼저 실행된 트랜잭션이라고 생각하자.
트랜잭션에는 두 가지 원칙이 존재한다.
스프링은 이해를 위해 논리 트랜잭션과 물리 트랜잭션이라는 개념을 도입한다. 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다. 위의 사진에서 외부트랜잭션은 논리 트랜잭션이다. 처음으로 실행된 트랜잭션이므로 물리 트랜잭션도 생성한다고 개념적으로 이해하면 옳다.
처음으로 시작한 트랜잭션은 물리 트랜잭션을 구성한다. 만약 그 후 Next 트랜잭션이 호출될 경우 해당 트랜잭션은 기존 물리 트랜잭션에 참여하게된다.(REQUIRE 옵션 기준)
이 트랜잭션 뭉텅이는 한 몸이 된다고 생각하면 편하다. 모두가 각각 커밋되어야 모두 최종 커밋될 수 있고, 하나라도 롤백된다면 모두가 롤백되는 것이다.
커밋할 경우 트랜잭션은 끝이 나는데 개념적으로 이해가 힘들다면 실제로 어떻게 동작하는 지를 이해하면 감이 잡힌다.
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
log.info("outer.isNewTransaction() = {}", outer.isNewTransaction());
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionDefinition()); // 외부 트랜잭션에 참여한다.
log.info("inner.isNewTransaction() = {}", inner.isNewTransaction());
log.info("내부 트랜잭션 커밋");
txManager.commit(inner); // 여기서는 아무 일도 하지 않는다. (외부 트랜잭션이 존재하기 때문에...)
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
// 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리 하도록 한다.
}

로그를 확인해보면 내부 트랜잭션 커밋에 대한 로그 이후 txManager.commit(inner)에 대한 로그가 찍히지 않는다. 트랜잭션은 isNewTransaction()을 통해 처음 트랜잭션과 그렇지 않은 트랜잭션을 구분할 수 있는데 물리 트랜잭션 내부의 isNewTransaction()이 false인 트랜잭션을 커밋할 경우 이 커밋명령은 무시된다.(그래서 커밋 관련 로그가 찍히지 않는다.)
그리고 isNewTransaction()이 true인 처음 트랜잭션을 커밋할 경우 커밋이 완료된다. 즉 이 트랜잭션 군집의 커밋에 대한 결정권은 첫 트랜잭션이 가지는 것이다.
내부(next) 트랜잭션은 커밋해도 반응이 없었다. 커밋 결정권이 외부(first)에 있기 때문이다. 그렇다면 내부 트랜잭션이 롤백되면 어떤 상황이 벌어질까.
@Test
void inner_rollback() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
log.info("outer.isNewTransaction() = {}", outer.isNewTransaction());
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionDefinition()); // 외부 트랜잭션에 참여한다.
log.info("inner.isNewTransaction() = {}", inner.isNewTransaction());
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner);
log.info("외부 트랜잭션 커밋");
Assertions.assertThatThrownBy(() -> txManager.commit(outer))
.isInstanceOf(UnexpectedRollbackException.class);
}

이 마크가 적용된 물리 트랜잭션의 모든 트랜잭션은 최종적으로 롤백된다. 정리하면 first가 아닌 트랜잭션은 롤백시 롤백온리 마크를 남기고 커밋시 first트랜잭션의 커밋까지 기다린다.
만약 내부 트랜잭션은 가만히 있고 외부 트랜잭션을 롤백한다면 당연히 최종 결정권자라는 것에 따라 모두 롤백된다.
왜 내부 트랜잭션 롤백 후 외부 트랜잭션 롤백을 남기는가?
세분화된 롤백 처리: 내부 트랜잭션 롤백 시, 외부 트랜잭션이 여전히 커밋 상태를 유지할 수 있도록 하려는 의도이다. 이는 외부 트랜잭션이 완전히 롤백되기 전까지 내부 트랜잭션이 독립적으로 실패할 수 있도록 한다.
트랜잭션 경계 유지: REQUIRES_NEW로 시작한 내부 트랜잭션은 별도의 트랜잭션으로 관리되며, 이 내부 트랜잭션의 실패는 외부 트랜잭션의 롤백을 트리거하지만 외부 트랜잭션이 롤백되지 않는 한, 내부 트랜잭션만 롤백하고 외부 트랜잭션은 커밋될 수 있다.
트랜잭션의 복원력: 외부 트랜잭션이 롤백 플래그를 가지는 이유는 롤백이 완료될 때까지 트랜잭션의 상태를 명확히 유지하기 위함이다. 내부 트랜잭션이 실패했을 때, 외부 트랜잭션이 rollback-only 상태로 남게 되면, 외부 트랜잭션이 나중에 커밋되지 않도록 보장한다.
내부 트랜잭션 롤백시 트랜잭션 동기화 매니저에 rollbackOnly=true를 남긴다. 만약 이 상황에서 외부 트랜잭션이 커밋을 시도한다면 rollbackOnly=true에 의해 롤백되면서 UnexpectedRollbackException이라는 런타임 예외를 던진다. 롤백이 되어야 할 상황에 커밋을 시도했다는 것은 시스템적으로는 잘못된 흐름이다. 예로 고객은 주문이 성공했다고 생각했는데 입금 트랜잭션이 성공하지 못해 롤백되어 주문이 생성되지 않은 것이다. 입금처리가 완료되지 않았는데 주문생성 성공을 위한 커밋을 시도한다는 것은 분명하게 크리티컬한 이슈일 수 있다.

TransactionStatus에 isNewTransaction 여부 저장TransactionStatus의 isNewTransaction 통해 확인.가장 많이 사용하는 기본 설정이다. 기존 트랜잭션이 없으면 생성하고, 있으면 참여한다.
항상 새로운 트랜잭션을 생성한다.
더 많은 옵션이 있지만 실무에서는 REQUIRED를 대부분 사용하며 아주 가끔 REQUIRES_NEW를 사용한다. SUPPORT, NOT_SUPPORT, MANDATORY..등 나머지 옵션들은 개발인생동안 마주치지 않을 가능성이 높기에 생략한다.
Service에 Transaction을 적용하라는 것이 보통의 개념이지만 만약 Service를 거치지 않고 다른 클라이언트가 Repository를 직접적으로 이용한다면, 그리고 그 Repository에는 트랜잭션이 걸리지 않았다면 이는 문제가 될 수 있다.
이때 트랜잭션 전파로 인해 Repository에도 트랜잭션을 걸어도 괜찮은 상황이 나온다. Service입장에서는 자신에게도 트랜잭션이 걸리고 내부 로직인 Repository 메서드를 사용할 때도 트랜잭션이 걸리지만 중복해서 트랜잭션이 걸릴 때 어차피 두번째 트랜잭션인 Repository의 로직은 내부 트랜잭션으로 외부 트랜잭션인 Service에 참여하기 때문에 로직적으로 문제가 되지 않으면서
다른 클라이언트가 Service가 아닌 직접적으로 Repostory를 이용해도 트랜잭션을 걸어 사용할 수 있는 것이다.
이렇게 트랜잭션 전파 개념은 트랜잭션 범위에 대한 자율성을 부여한다.