-
스프링 트랜잭션 전파Spring&SpringBoot 2025. 11. 20. 12:54
1. commit()
@Slf4j @SpringBootTest public class BasicTxTest { @Autowired PlatformTransactionManager txManager; @TestConfiguration static class config { @Bean public PlatformTransactionManager txManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } @Test void commit() { log.info("트랜잭션 시작"); TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition()); log.info("트랜잭션 커밋 시작"); txManager.commit(status); log.info("트랜잭션 커밋 완료"); } }
2. rollback()
@Test void rollback() { log.info("트랜잭션 시작"); TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition()); log.info("트랜잭션 롤백 시작"); txManager.rollback(status); log.info("트랜잭션 롤백 완료"); }
3. double_commit()
@Test void double_commit() { log.info("트랜잭션1 시작"); TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionDefinition()); log.info("트랜잭션1 커밋 "); txManager.commit(tx1); log.info("트랜잭션2 시작"); TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionDefinition()); log.info("트랜잭션2 커밋 "); txManager.commit(tx2); }
- 트랜잭션1을 시작하고, 커넥션 풀에서 conn0 커넥션을 획득했다
- 트랜잭션1을 커밋하고, 커넥션 풀에 conn0 커넥션을 반납했다
- 트랜잭션2를 시작하고, 커넥션 풀에서 conn0 커넥션을 획득했다
- 트랜잭션2를 커밋하고, 커넥션 풀에 conn0 커넥션을 반납했다
- 히카리 커넥션이 풀이 반환해주는 커넥션을 다루는 프록시 객체의 주소가 서로 다르다
- 결과적으로 conn0을 통해 커넥션이 재사용 된 것을 확인할 수 있고 각각의 주소를 통해 커넥션 풀에서 커넥션을 조회했다
4. double_commit_rollback()
@Test void double_commit_rollback() { log.info("트랜잭션1 시작"); TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("트랜잭션1 커밋"); txManager.commit(tx1); log.info("트랜잭션2 시작"); TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("트랜잭션2 롤백"); txManager.rollback(tx2); }- 트랜잭션1은 커밋하고, 트랜잭션2는 롤백한다

- 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다
- 물리 트랜잭션은 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다
- 논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타난다
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다
5. inner_commit()
@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); }

- txManager.getTransaction() 호출해서 외부 트랜잭션을 시작한다
- 트랜잭션 매니저는 datasource를 통해 커넥션을 생성한다
- 생성한 커넥션을 수동 커밋 모드로 설정한다 - 물리 트랜잭션 시작
- 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션을 보관한다
- 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 반환하는데 신규 트랜잭션 여부가 담겨있다
- 로직1이 사용되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용한다
- txManager.getTransaction() 호출해서 내부 트랜잭션이 시작한다
- 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해 트랜잭션이 존재하는지 확인한다
- 기존 트랜잭션이 존재하므로 기존 트랜잭션에 참여한다
- 이미 물리 트랜잭션이 진행중이므로 로직이 기존에 시작된 트랜잭션을 자연스럽게 사용한다
- 이후 로직은 트랜잭션 동기화 매니저에 보관된 기존 커넥션을 사용한다
- 트랜잭션 매니저는 트랜잭션 생성 결과를 TransactionStatus에 담아 반환한다
- 로직2가 사용되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 외부 트랜잭션이 보관한 커넥션을 획득해 사용한다

- 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋한다
- 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않는다
- 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다
- 외부 트랜잭션은 신규 트랜잭션이기 때문에 DB 커넥션에 실제 커밋을 호출한다
- 실제 데이터베이스에 커밋이 반영되고 물리 트랜잭션도 끝난다
5. outer_rollback()
@Test void outer_rollback() { log.info("외부 트랜잭션 시작"); TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("내부 트랜잭션 시작"); TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("내부 트랜잭션 커밋"); txManager.commit(inner); log.info("외부 트랜잭션 롤백"); txManager.rollback(outer); }
- 외부 트랜잭션이 물리 트랜잭션을 시작하고 롤백한다
- 내부 트랜잭션은 직접 물리 트랜잭션에 관여하지 않는다
6. inner_rollback()
@Test void inner_rollback() { log.info("외부 트랜잭션 시작"); TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("내부 트랜잭션 시작"); TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("내부 트랜잭션 롤백"); txManager.rollback(inner); log.info("외부 트랜잭션 커밋"); txManager.commit(outer); assertThatThrownBy(() -> txManager.commit(outer)) .isInstanceOf(UnexpectedRollbackException.class); }
- 외부 트랜잭션을 커밋할 때 UnexpectedRollbackException이 발생한다
7. inner_rollback_requires_new()

- 물리 트랜잭션을 분리하려면 내부 트랜잭션을 시작할 때 REQUIRES_NEW 옵션을 사용한다
- 외부 트랜잭션과 내부 트랜잭션이 각각 별도의 물리 트랜잭션을 가진다
- 별도의 물리 트랜잭션을 갖는다는 말은 DB 커넥션 또한 각각 사용한다는 뜻이다
- 최종적으로 로직2는 롤백되고 로직1은 커밋된다
@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()); log.info("내부 트랜잭션 롤백"); txManager.rollback(inner); //롤백 log.info("외부 트랜잭션 커밋"); txManager.commit(outer); //커밋 }


- 내부 트랜잭션을 시작할 때 전파 옵션인 propagationBehavior에 PROPAGATION_REQUIRES_NEW 옵션을 줬다
- 내부 트랜잭션을 시작할 때 기존 트랜잭션에 참여하는 것이 아니라 새로운 물리 트랜잭 션을 만들어서 시작하게 된다
8. 전파 옵션
- 전파 옵션에 별도의 설정을 하지 않으면 REQUIRED가 기본으로 사용된다
- 실무에서는 대부분 REQUIRED 옵션을 사용하며 가끔 REQUIRED_NEW를 사용한다
- REQUIRED
- 기존 트랜잭션 없음 : 새로운 트랜잭션을 생성한다
- 기존 트랜잭션 있음 : 기존 트랜잭션에 참여한다
- REQUIRED_NEW
- 기존 트랜잭션 없음 : 새로운 트랜잭션을 생성한다
- 기존 트랜잭션 없음 : 새로운 트랜잭션을 생성한다
728x90'Spring&SpringBoot' 카테고리의 다른 글
데이터 접근 기술 - 스프링 데이터 JPA (0) 2025.11.20 트랜잭션 커밋/롤백과 예외 (0) 2025.11.20 @Transaction (0) 2025.11.13 트랜잭션, DB 락 (0) 2025.11.12 JDBC (0) 2025.11.12