트랜잭션 전파 - 트랜잭션 두번 사용
@Slf4j
@SpringBootTest
public class BasicTxTest {
@Autowired
PlatformTransactionManager txManager;
@Autowired
private DataSourceTransactionManager transactionManager;
@TestConfiguration
static class Config {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Test
void commit() {
log.info("트랜잭션 시작");
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 커밋 시작");
txManager.commit(status);
log.info("트랜잭션 커밋 완료");
}
@Test
void rollback() {
log.info("트랜잭션 시작");
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 롤백 시작");
txManager.rollback(status);
log.info("트랜잭션 롤백 완료");
}
@Test
void double_commit() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션1 커밋 시작");
txManager.commit(tx1);
log.info("트랜잭션2 시작");
TransactionStatus tx2 = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션2 커밋 시작");
txManager.commit(tx2);
}
@Test
void double_commit_rollback() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션1 커밋 시작");
txManager.commit(tx1);
log.info("트랜잭션2 시작");
TransactionStatus tx2 = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션2 롤백 시작");
txManager.rollback(tx2);
}
}
로그를 보면, 트랜잭션1과 트랜잭션2 모두 conn0 커넥션을 획득했다.
그 이유는 커넥션 풀을 사용하기 때문에 1에서 사용한 conn0을 반납하고 2에서 또 반납된 conn0을 획득했기 때문이다.
그렇다고해서 트랜잭션1과 트랜잭션2의 커넥션이 같은 커넥션은 아니다.
히카리 커넥션 풀에서 커넥션을 획득하면 실제 커넥션을 그대로 반환하지 않고, 히카리 프록시 커넥션이라는 객체를 생성해서 반환한다.

conn0은 같지만, 인스턴스는 다른것을 확인할 수 있다.
트랜잭션 전파 - 전파 기본
트랜잭션을 각각 사용하는거시 아니라, 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행한다면?
(REQUIRED 기준 설명)

- 스프링은 이해를 돕기위해 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눈다.
- 논리 트랜잭션은 하나의 물리 트랜잭션으로 묶인다.
- 물리 트랜잭션은 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다. (실제 커넥션을 통해서 트랜잭션 시작 setAUtoCommit(false)하고, 커밋, 롤백하는 단위
- 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이다.
트랜잭션이 사용중일 때 다른 트랜잭션이 내부에 사용되면 여러가지 복잡한 상황이 발생한다. 이 때 논리 트랜잭션 개념을 도입하면 단순한 원칙을 만들 수 있다.
원칙
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.

스프링 트랜잭션 전파 - 전파 예제
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", inner.isNewTransaction());
log.info("내부 트랜잭션 커밋");
txManager.commit(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
- 외부 트랜잭션이 수행중인데, 내부 트랜잭션을 추가로 수행한다.
- 외부 트랜잭션은 처음 수행된 트랜잭션이다. 이 경우 신규 트랜잭션이다. (isNewTransaction=true)
- 내부 트랜잭션을 시작하는 시점에는 이미 외부 트랜잭션이 진행중인 상태이다. 이 경우 내부 트랜잭션은 외부 트랜잭션에 참여한다.
하나의 커넥션에 커밋은 한번만 호출할 수 있다. inner에서 이미 한번 커밋됐고 outer에서 또 커밋이 수행된다.

내부 트랜잭션을 시작할 때 Participating in existing transaction이라는 메세지를 확인할 수 있다. 이 메세지는 내부 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 뜻이다.
내부 트랜잭션 커밋 이후엔 로그를 확인할 수 없다. 내부 트랜잭션은 물리 트랜잭션을 시작하거나, 커밋할 수 없다.
즉 외부 트랜잭션만 물리 트랜잭션을 시작, 커밋할 수 있다. 이를 통해 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 해 트랜잭션 중복 커밋 문제를 해결한다.


그림을 순서대로 천천히 읽어보면 결국 하나의 커넥션에서, 외부 트랜잭션만이 물리 트랜잭션을 커밋할 수 있는것이다.
트랜잭션 매니저에서 하는 커밋이 논리적인 커밋이라면, 실제 커넥션에 커밋하는것은 물리 커밋이라 할 수 있고, 이것이 실제 DB에 반영이 되고 물리 트랜잭션이 끝나는 것이다.
트랜잭션 전파 - 외부 롤백

(간단요약)
물리 트랜잭션 - 다 롤백됨
트랜잭션 전파 - 내부 롤백
원칙을 떠올려보면 모든 논리 트랜잭션이 커밋이 되어야 물리 트랜잭션도 커밋이 된다는 원칙이 있었다.
내부에서 롤백이 되면 당연히 물리 트랜잭션은 롤백된다. 그 과정을 알아보자


내부 트랜잭션 롤백 부분에 rollback-only라는 문구가 보인다.
내부 트랜잭션에 문제가 생겨 롤백을 해야하는 상황인데, 내부 트랜잭션은 신규 트랜잭션이 아니다. 여기서 물리 트랜잭션에 접근해 실제 롤백하면 안된다. 따라서 트랜잭션 동기화 매니저에 rollbackOnly=true를 남겨두고 외부 트랜잭션으로 넘어가게 된다.
외부 트랜잭션에서 커밋하는 과정에, 트랜잭션 동기화 매니저에 rollbackOnly 옵션을 확인한다. true인 경우 물리 트랜잭션을 롤백한다.
그리고 exception을 던진다.

개발자는 커밋이 되기를 기대했는데, 롤백되었기 때문에 시스템에서 롤백되었다는것을 분명하게 알려줘야 하기 때문에 에러를 던진다.
정리
- 논리 트랜잭션이 하나라도 롤백이면 물리 트랜잭션은 롤백된다.
- 그 과정에 rollbackOnly 옵션이 있다.
스프링 트랜잭션 전파 - REQUIRES_NEW
외부 트랜잭션과 내부 트랜잭션을 별도의 물리 트랜잭션으로 분리해서 사용하는 방법이 있다.

@Test
void inner_rollback_requires_new() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
log.info("내부 트랜잭션 시작");
DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus inner = txManager.getTransaction(definition);
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // true
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner); // 롤백
log.info("외부 트랜잭션 커밋");
txManager.commit(outer); // 커밋
}

외부 트랜잭션 시작 후, 내부 트랜잭션 시작 지점에서 Suspending current transaction부분에서, conn0을 잠시 보류하고 conn1가 사용된다. 그 이후 내부 트랜잭션의 conn1이 롤백 되고 난 후에 Resuming suspendded transaction ~~ 시점에서 다시 conn0이 사용되고 커밋 -> 커넥션 릴리즈

물리 트랜잭션이 2개인것이다. 그게 전부다. 그러나 여기서 다른점은 데이터 베이스 커넥션이 동시에 2개 사용된다는 점을 주의해라.
'Spring' 카테고리의 다른 글
| 스프링 트랜잭션 이해 (0) | 2025.10.30 |
|---|---|
| 데이터 접근 기술 - MyBatis (0) | 2025.10.21 |
| 데이터 접근 기술 - 테스트 (0) | 2025.10.20 |
| 데이터 접근 기술 - 스프링 JdbcTemplate (0) | 2025.10.18 |
| 스프링과 문제 해결 - 예외처리, 반복 (0) | 2025.10.17 |
