트랜잭션은 커넥션을 획득하고, 커밋이나 롤백이 실행되면, 작업이 종료되어 커넥션은 다시 커넥션 풀로 반환된다.
그렇다면 트랜잭션이 이미 진행중인데, 여기서 추가로 트랜잭션을 수행하면 어떻게 될까?
기존의 트랜잭션을 이어받아 트랜잭션을 수행해야 할까? 별도의 트랜잭션을 진행해야 할까?
이처럼
'하나의 트랜잭션이 실행되는 도중 다른 트랜잭션을 실행할 경우 어떻게 동작할지 결정하는 것'
을 트랜잭션 전파(propagation)라고 한다.
먼저 아래의 그림을 보자
외부 트랜잭션이 수행되고 트랜잭션이 끝나지 않았는데, 내부 트랜잭션이 수행이 된다.
외부 트랜잭션은 상대적으로 밖에 있기 때문에 외부 트랙션이라고 하고,
내부 트랜잭션은 외부 트랜잭션이 수행되는 도중에 호출되기 때문에 내부 트랜잭션이라고 한다.
스프링에서는 트랜잭션을 논리 트랜잭션과 물리 트랜잭션으로 나눈다.
스프링은 위와 같은 경우 외부 트랜잭션과 내부 트랜잭션을 하나의 물리 트랜잭션으로 묶어준다.
내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다.
김영한님이 말씀하셨던 것처럼 이런 대원칙을 잡고 가자!
그럼 각 예시로 확인해보자.
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
#JPA log
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG
#JPA SQL
logging.level.org.hibernate.SQL=DEBUG
@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를 반환한다.
반면 내부 트랜잭션은 외부 트랜잭션에 참여하므로 isNewTransaction()가 false를 반환한다.
테스트 수행 결과를 다시 확인해보자
내부 트랜잭션이 시작되고,
Participating in existing transaction이라는 로그를 통해 기존의 트랜잭션에 참여한다는 것을 확인할 수 있다.
내부 트랜잭션은 커밋이 호출되었음에도 아무런 동작이 이뤄지지 않은 반면,
외부 트랜잭션은 커밋이 호출되자 실제로 커밋이 이뤄지고 정보가 DB에 전달되는 것을 확인할 수 있다.
내부 트랜잭션에서 커밋을 하면 트랜잭션이 끝나버려 외부 트랜잭션이 작업을 이어갈 수 없다.
따라서 내부 트랜잭션은 DB커넥션을 통한 물리 트랜잭션을 커밋을 하면 안된다.
이게 트랜잭션 전파의 기본 흐름이다.
그럼 트랜잭션 전파는 어떻게 동작할까?
트랜잭션이 커밋을 될 때의 예시로 트랜잭션 전파 흐름을 짚고 넘어가자.
@Test
void outer_rollback() {
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.rollback(outer);
}
앞서 봤던 내용에서는 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않는다고 하였다. 그럼 스프링에서는 내부 트랜잭션에서 롤백을 호출할 때 어떻게 처리할까?
코드로 확인해보자
void inner_rollback() {
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.rollback(inner);
log.info("외부 트랜잭션 롤백");
Assertions.assertThatThrownBy(() -> txManager.commit(outer))
.isInstanceOf(UnexpectedRollbackException.class);
}
트랜잭션의 전파에 대해 알아보았다.
스프링 트랜잭션 전파란?
하나의 트랜잭션에서 추가로 트랜잭션이 발생하는 경우 이것을 어떻게 처리할 것인지를 말한다.
트랜잭션 전파에는 대원칙이 있는데,
논리 트랜잭션이 하나라도 롤백이 된다면 물리 트랜잭션은 롤백이 되고,
모든 논리 트랜잭션이 커밋이 되어야 물리 트랜잭션도 커밋이 된다는 것이다.
내부 트랜잭션의 경우 커밋이나 롤백으로 물리 트랜잭션에 관여할 수 없는데,
트랜잭션 매니저에서는 트랜잭션이 신규 커넥션인지 기존 트랜잭션인지 여부를 확인하여 신규 트랜잭션일 경우에만 커밋, 롤백 등을 호출하고 처리하기 때문이다.
내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않지만 내부 트랜잭션에서 롤백을 할 경우, 트랜잭션 매니저에서 트랜잭션 동기화 매니저에 롤백 전용 표시를 남긴다. 이후 외부 트랜잭션에서 트랜잭션 매니저를 통해 실제 커밋을 호출하기 전에 트랜잭션 동기화 매니저에 롤백 전용 표시를 확인하고 롤백을 호출한다.
이것으로 트랜잭션 전파와 각 상황에 따른 동작에 대해 알아보았다. 그런데 만약 외부 트랜잭션과 내부 트랜잭션을 각각 분리해서 처리하려면 어떻게 해야할까? 다음 포스팅에서 알아보자.
출처 : 김영한 - 스프링 DB 2편