Spring DB : 스프링 트랜잭션 전파 기본
- Spring/Spring
- 2023. 2. 1.
들어가기 전
이 글은 인프런 영한님의 강의를 복습하며 작성한 글입니다.
스프링 트랜잭션 전파
이 글에서는 다음 목표를 가진다.
- 스프링에서 트랜잭션이 두 개 이상 존재할 때, 어떻게 동작하는지 확인.
- 스프링이 제공하는 트랜잭션 전파 개념 숙지
스프링 트랜잭션 전파1 - 커밋, 롤백
- 코드는 이곳(https://github.com/chickenchickenlove/springdb2/blob/chapter-9-spring-transaction-propagation-basic/src/test/java/hello/springtx/propagation/BasicTxTest.java)에서 확인할 수 있음.
먼저 1개의 트랜잭션만 있을 때, 트랜잭션을 각각 커밋하고 롤백하면 어떤 결과가 나오는지를 확인해보고자 한다. 아래 코드를 이용해서 살펴볼 수 있다.
@Autowired
PlatformTransactionManager txManager;
@TestConfiguration
static class MyConfig{
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Test
void commit() {
log.info("트랜잭션 시작");
TransactionStatus tx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 커밋 시작");
txManager.commit(tx);
log.info("트랜잭션 커밋 완료");
}
@Test
void rollback() {
log.info("트랜잭션 시작");
TransactionStatus tx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 롤백 시작");
txManager.rollback(tx);
log.info("트랜잭션 롤백 완료");
}
- 스프링부트는 현재 사용하고 있는 데이터 접근 기술을 바탕으로 적절한 트랜잭션 매니저를 생성해서 스프링 빈으로 등록해준다.
- 만약 개발자가 직접 트랜잭션 매니저를 등록하면 스프링부트는 트랜잭션 매니저 빈을 따로 등록하지 않는다. 그리고 개발자가 등록한 트랜잭션 매니저를 사용한다.
- commit(), rollback() 테스트의 실행 결과를 살펴보며 트랜잭션의 기본 이해를 해본다.
// commit() 실행 로그
BasicTxTest : 트랜잭션 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:3dd93989-eb2f-40a4-8d0e-db921b46d1e2 user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:3dd93989-eb2f-40a4-8d0e-db921b46d1e2 user=SA] to manual commit
BasicTxTest : 트랜잭션 커밋 시작
DataSourceTransactionManager : Initiating transaction commit
DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:3dd93989-eb2f-40a4-8d0e-db921b46d1e2 user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:3dd93989-eb2f-40a4-8d0e-db921b46d1e2 user=SA] after transaction
BasicTxTest : 트랜잭션 커밋 완료
// rollback() 실행 로그
위는 commit() / rollback() 실행 로그다. 이 중에서 commit() 로그만 해석해보고자 한다.
- 트랜잭션이 시작하면서 새로운 트랜잭션을 생성한다. 이 때, 트랜잭션 전파는 REQUIRED로 설정된다.
- 트랜잭션을 생성할 때 커넥션 풀에서 커넥션 conn0를 가져왔다. 그리고 conn0를 HikariProxyConnection 객체로 감쌌다. DB와 통신을 할 때는 이녀석을 사용하게 된다.
- 트랜잭션 커밋이 시작되면서 Commiting JDBC transaction on Connection이 나오는 것을 확인할 수 있다. 이 때, conn0와 함께 커밋 된 것을 볼 수 있다.
- 커밋이 완료되면 JdbcConnection을 릴리즈한다. 이 때 conn0이 커넥션 풀로 반환된다.
스프링 트랜잭션 전파 2 - 트랜잭션 두번 커밋
- 코드는 이곳에서 확인할 수 있음. (https://github.com/chickenchickenlove/springdb2/blob/chapter-9-spring-transaction-propagation-basic/src/test/java/hello/springtx/propagation/BasicTxTest.java)
위에서는 트랜잭션이 1개만 사용되는 경우를 살펴봤다. 이번에는 트랜잭션 2개가 따로 사용되는 경우를 확인한다. 이 때, 트랜잭션 1번이 완전히 끝나면 트랜잭션 2번이 시작된다.
@Test
void double_commit() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션1 커밋 시작");
txManager.commit(tx1);
log.info("트랜잭션1 커밋 완료");
log.info("트랜잭션2 시작");
TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션2 커밋 시작");
txManager.commit(tx2);
log.info("트랜잭션2 커밋 완료");
}
위의 코드를 이용해서 확인할 수 있다. 로그를 바탕으로 트랜잭션을 살펴보고자 한다.
BasicTxTest : 트랜잭션1 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] to manual commit
BasicTxTest : 트랜잭션1 커밋 시작
DataSourceTransactionManager : Initiating transaction commit
DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] after transaction
BasicTxTest : 트랜잭션1 커밋 완료
BasicTxTest : 트랜잭션2 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1396538465 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1396538465 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] to manual commit
BasicTxTest : 트랜잭션2 커밋 시작
DataSourceTransactionManager : Initiating transaction commit
DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@1396538465 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1396538465 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] after transaction
BasicTxTest : 트랜잭션2 커밋 완료
로그는 위와 같이 나온다. 각 로그를 살펴보면 다음과 같이 동작하는 것을 살펴볼 수 있다.
트랜잭션 1
- 트랜잭션 1을 시작한다. 이 때 커넥션 풀에서 conn0를 획득하고, 이것을 HikariProxyConnection으로 감싼다.
- 트랜잭션을 커밋한다. 이 때 conn0으로 커밋을 하고 커밋이 완료되면 JdbcConnection을 Release한다.
트랜잭션 2
- 트랜잭션 2을 시작한다. 이 때 커넥션 풀에서 conn0를 획득하고, 이것을 HikariProxyConnection으로 감싼다.
- 트랜잭션을 커밋한다. 이 때 conn0으로 커밋을 하고 커밋이 완료되면 JdbcConnection을 Release한다.
트랜잭션1,2의 로그를 살펴보면 각 트랜잭션이 같은 커넥션 conn0를 사용하고 있는 것을 볼 수 있다. 이것은 트랜잭션 매니저가 커넥션에서 트랜잭션을 가져오기 때문이다. 만약 요청이 올 때 마다 커넥션을 생성하는 구조였다면, 트랜잭션1, 2는 각각 conn1, conn2를 사용했을 것이다.
커넥션 풀을 이용해서 트랜잭션에 필요한 커넥션을 얻어오고 트랜잭션이 완료되면 커넥션을 반환한다. 따라서 트랜잭션1, 2가 얻었던 conn0는 물리적으로는 같은 커넥션이지만, 논리적으로는 서로 다른 커넥션이다. 왜냐하면 사용을 다 끝내고 반환하고 다시 얻어서 사용하기 때문이다.
두 커넥션을 논리적으로 구분하는 방법은 HikariProxyConnection의 객체 주소를 바라보는 것이다. 트랜잭션 매니저는 Connection(conn0)를 바로 사용하는 것이 아니라, HikariProxyConnection으로 한번 감싸서 사용한다.
위의 그림은 위 코드에서 각 트랜잭션이 동작하는 방식을 의미한다.
- 트랜잭션이 각각 수행되면서 사용되는 DB 커넥션도 달라진다. (비록 같은 conn0을 사용한 것처럼 보이지만, 서로 다른 conn0를 사용했다)
- 트랜잭션을 각자 관리하기 때문에 전체 트랜잭션을 묶을 수 없다. 예를 들어서 "트랜잭션 1 - 커밋 / 트랜잭션 2 - 롤백인 경우라면 트랜잭션 1 데이터 → 저장, 트랜잭션 2 데이터 → 롤백"으로 각각 동작하게 된다.
스프링 트랜잭션 전파 2 - 트랜잭션 1개 롤백 + 1개 커밋
이번에는 두 개의 트랜잭션이 각각 동작할 때, 하나는 롤백되고 하나는 커밋되는 상황을 테스트 코드로 살펴보고자 한다. 결론부터 이야기하면 이 구조에서는 각 트랜잭션은 서로에게 영향을 미치지 못한다.
@Test
void double_commit_rollback() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션1 커밋 시작");
txManager.commit(tx1);
log.info("트랜잭션1 커밋 완료");
log.info("트랜잭션2 시작");
TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션2 롤백 시작");
txManager.rollback(tx2);
log.info("트랜잭션2 롤백 완료");
}
위 코드를 실행하고 로그를 살펴보며 트랜잭션에 대해서 이해를 해본다.
BasicTxTest : 트랜잭션1 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] to manual commit
BasicTxTest : 트랜잭션1 커밋 시작
DataSourceTransactionManager : Initiating transaction commit
DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@403649458 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] after transaction
BasicTxTest : 트랜잭션1 커밋 완료
BasicTxTest : 트랜잭션2 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1396538465 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1396538465 wrapping conn0: url=jdbc:h2:mem:7cf7c5dd-dd94-4236-8467-6ebf53eeac20 user=SA] to manual commit
BasicTxTest : 트랜잭션2 롤백 시작
DataSourceTransactionManager : Initiating transaction rollback
DataSourceTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1580467617 wrapping conn0: url=jdbc:h2:mem:9eafb703-b794-4b9a-b27d-10f4f2807d64 user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1580467617 wrapping conn0: url=jdbc:h2:mem:9eafb703-b794-4b9a-b27d-10f4f2807d64 user=SA] after transaction
BasicTxTest : 트랜잭션2 롤백 완료
로그에서는 다음 상황이 있음을 확인할 수 있다.
- 트랜잭션 1은 정상적으로 커밋되었다.
- 트랜잭션 1 커밋이 완료된 후에, 트랜잭션 2 롤백이 완료되었다.
그리고 위 상황은 아래 그림으로 도식화 할 수 있다. 각 트랜잭션은 서로 다른 DB 커넥션을 이용하기 때문에 트랜잭션끼리 영향을 줄 수 없게 되는 것이다.
- 트랜잭션 1은 JdbcConnection - conn1을 이용해서 DB와 통신을 했고 커밋했다.
- 트랜잭션 2는 JdbcConnection - conn2를 이용해서 DB와 통신했고 롤백했다.
스프링 트랜잭션 전파 3 - 트랜잭션 전파 기본
앞에서 본 예제에서는 트랜잭션을 2개 사용하지만 트랜잭션이 독립적으로 동작했다. 그렇다면 트랜잭션이 이미 수행중인데, 여기에 추가로 트랜잭션이 수행된다면 어떻게 될까? 이 때 스프링은 트랜잭션 전파 (Transaction Propagation) 설정 값에 따라 다음과 같이 동작한다.
- 새로운 트랜잭션을 생성하고 독립적으로 동작한다. (REQUIRES_NEW)
- 이미 수행중인 트랜잭션에 합류한다. (REQUIRED)
지금부터 설명하는 내용은 트랜잭션 전파의 기본 옵션인 REQUIRED를 기준으로 설명한다.
외부 트랜잭션이 수행중인데, 내부 트랜잭션이 추가로 수행됨
- 외부 트랜잭션이 수행중이고, 아직 끝나지 않았는데 내부 트랜잭션이 수행된다.
- 외부 트랜잭션의 의미는 상대적으로 밖에 있기 때문이다. 외부 트랜잭션 = 처음 시작된 트랜잭션으로 이해하면 된다.
- 내부 트랜잭션은 외부 트랜잭션 수행 되는 중에 생성된다. 따라서 내부에 있는 것으로 보이기 때문에 내부 트랜잭션으로 명칭된다.
- 스프링은 외부 트랜잭션과 내부 트랜잭션이 존재한다면, 두 트랜잭션을 하나로 묶어 하나의 트랜잭션으로 만들어준다. 내부 트랜잭션이 외부 트랜잭션에 참여하도록 하는데, 이것이 기본 동작이다.
- 내부 트랜잭션이 외부 트랜잭션에 참여하는 것이기 때문에 하나의 트랜잭션으로 동작하는 것으로 보인다.
물리 트랜잭션, 논리 트랜잭션
- 스프링은 트랜잭션의 이해를 돕기 위해 '논리 트랜잭션'과 '물리 트랜잭션' 개념으로 나눈다.
- 다수의 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다.
- 물리 트랜잭션은 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다. JdbcConnection을 통해서 트랜잭션을 시작(setAutoCommit(false))하고 실제 커넥션을 통해서 커밋, 롤백하는 단위이다.
- 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위다.
- 이러한 논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에만 나타난다. 단순히 트랜잭션이 하나인 경우 논리/물리 트랜잭션을 구분하지 않는다. (더 정확히는 'REQUIRED' 전파 옵션을 사용하는 경우에 나타남)
물리 트랜잭션 개념을 도입하면서 여러 트랜잭션이 존재할 때 사용할 수 있는 대원칙을 도입할 수 있게 되었다. 이 대원칙은 아래에서 볼 수 있듯이 매우 단순하다.
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
- 모든 논리 트랜잭션 커밋 성공 → 물리 트랜잭션 실제로 커밋
- 논리 트랜잭션 하나 커밋 성공, 하나는 롤백 → 물리 트랜잭션 롤백
스프링 트랜잭션 전파 4 - 전파 예제
위에서 트랜잭션 전파 개념을 이야기하며 논리 트랜잭션 / 물리 트랜잭션을 이야기했다. 이번 예제에서는 논리 트랜잭션 / 물리 트랜잭션의 각각의 로그를 살펴보며 동작을 확인해보고자 한다. 먼저 살펴보기 전에 논리 / 물리 트랜잭션 등장과 함께 나타난 개념이 있기 때문에 이것을 살펴보고자 한다.
boolean isTrue = outerTx.isNewTransaction();
트랜잭션 매니저에게서 얻은 트랜잭션을 isNewTransaction() 인지를 확인할 수 있다. 트랜잭션이 새로운 트랜잭션이냐는 의미다. 여기서 새로운 트랜잭션의 의미는 '처음 시작된 트랜잭션'이라는 의미를 가진다. 처음 시작된 트랜잭션은 다음 역할을 한다.
- 처음 시작된 트랜잭션이 커밋될 때, 물리 트랜잭션이 커밋을 검토한다.
- 처음 시작되지 않은 논리 트랜잭션은 커밋되거나 롤백되어도, 그 순간에는 물리 트랜잭션에 영향을 미치지 않는다. 그 영향은 처음 시작된 트랜잭션이 커밋될 때 검토된다.
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outerTx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outerTx.isNewTransaction = {}", outerTx.isNewTransaction());
log.info("내부 트랜잭션 시작");
TransactionStatus innerTx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("innerTx.isNewTransaction = {}", innerTx.isNewTransaction());
log.info("내부 트랜잭션 커밋");
txManager.commit(innerTx);
log.info("외부 트랜잭션 커밋");
txManager.commit(outerTx);
}
코드는 위에서 살펴볼 수 있다.
- 외부 트랜잭션이 수행중인데 내부 트랜잭션을 추가로 수행했다.
- 외부 트랜잭션은 처음 수행된 트랜잭션이다. 이 경우 신규 트랜잭션(isNewTransaction=true)이 된다.
- 내부 트랜잭션을 시작하는 시점에는 외부 트랜잭션이 진행중인 상태다. 이 경우 내부 트랜잭션은 외부 트랜잭션에 참여한다. (REQUIRED)
- 트랜잭션 참여
- 내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 의미다.
- 다른 관점으로 보면 외부 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻이다.
- 다른 관점으로 보면 외부에서 시작된 물리적인 트랜잭션 범위가 내부 트랜잭션까지 넓어진다는 뜻이다.
- 정리하면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것을 의미한다.
- 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여한다. 이 때 내부 트랜잭션은 신규 트랜잭션이 아니다.
정리하면 스프링은 두 개의 논리 트랜잭션을 하나의 물리 트랜잭션으로 묶어서 관리했다. 그렇다면 스프링이 어떻게 하나의 트랜잭션으로 묶어서 관리하는지를 살펴봐야하겠다.
스프링의 물리 트랜잭션 관리 방법
먼저 위의 코드를 살펴보면 트랜잭션을 두 번 커밋했다. 그렇다면 코드 실행 로그를 살펴보면 커밋 로그가 두 번 나와야 한다는 것이다. 로그를 살펴보자.
// inner_commit() 실행 로그
BasicTxTest : 외부 트랜잭션 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:aca957e4-0df3-4745-ada6-369235ad0eda user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:aca957e4-0df3-4745-ada6-369235ad0eda user=SA] to manual commit
BasicTxTest : outerTx.isNewTransaction = true // 외부 트랜잭션 == 신규 트랜잭션
BasicTxTest : 내부 트랜잭션 시작
DataSourceTransactionManager : Participating in existing transaction // 내부 트랜잭션 시작 시, 단순히 참여함.
BasicTxTest : innerTx.isNewTransaction = false // 내부 트랜잭션 != 신규 트랜잭션
BasicTxTest : 내부 트랜잭션 커밋 // 내부 트랜잭션 커밋했지만, 트랜잭션 매니저는 커밋하지 않음.
BasicTxTest : 외부 트랜잭션 커밋
DataSourceTransactionManager : Initiating transaction commit
DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:aca957e4-0df3-4745-ada6-369235ad0eda user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:aca957e4-0df3-4745-ada6-369235ad0eda user=SA] after transaction
로그를 살펴보면 다음 내용을 이해할 수 있다.
- 내부 트랜잭션은 시작해도 트랜잭션 매니저가 새로운 트랜잭션을 시작하는 로그를 남기지 않는다. 대신 트랜잭션 매니저는 내부 트랜잭션이 이미 존재하는 트랜잭션에 참여한다는 것을 알려준다. (Participating in existing transaction)
- 실행 결과를 보면 외부 트랜잭션 시작 / 커밋할 때는 물리 트랜잭션이 시작/커밋되는 것을 확인할 수 있다. 그렇지만 내부 트랜잭션을 시작/커밋할 때는 DB 커넥션을 통해 커밋/로그 하는 것을 확인할 수 없다.
- 정리하면 외부 트랜잭션만 물리 트랜잭션을 시작하고 커밋한다. 만약 내부 트랜잭션이 물리 트랜잭션을 커밋하면 물리 트랜잭션이 그 순간 끝나고, 외부 트랜잭션은 커밋을 할 수 없게 된다. 따라서 외부 트랜잭션이 시작 / 커밋하는 순간에 물리 트랜잭션이 시작 / 커밋하도록 동작한다.
- 스프링은 여러 트랜잭션이 함께 사용될 때, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다.
- 개념적으로 두 개의 논리 트랜잭션은 위와 같이 하나의 물리 트랜잭션으로 묶인다.
- 물리 트랜잭션은 트랜잭션이 처음 시작된 외부 트랜잭션에 의해서 관리된다.
- 모든 논리 트랜잭션이 커밋 되어야 물리 트랜잭션은 커밋된다. 그렇지 않으면 물리 트랜잭션을 롤백된다.
위의 내용을 바탕으로 스프링은 트랜잭션의 중복 커밋을 방지한다.
트랜잭션 전파의 전체 내용을 하나씩 살펴보면 다음과 같다.
요청 흐름 - 외부 트랜잭션
- 1. txManager.getTransaction()을 호출해서 외부 트랜잭션을 시작한다.
- 2. 트랜잭션 매니저는 Datasource를 통해 커넥션을 생성한다.
- 3. 생성한 커넥션을 수동 커밋 모드 (setAutoCommit(false))로 설정한다. → 물리 트랜잭션 시작.
- 4. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 생성한 커넥션을 보관한다.
- 5. 트랜잭션 매니저는 생성한 트랜잭션 결과를 TransactionStatus에 담아서 반환하는데, 여기에 신규 트랜잭션 여부가 담겨있다. isNewTransaction을 통해 신규 트랜잭션 여부를 확인할 수 있다. 트랜잭션을 처음 시작했으므로 신규 트랜잭션이다.
- 6. 로직1이 사용되고, 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션(conn0)를 획득해서 사용한다.
요청 흐름 - 내부 트랜잭션
- 7. txManager.getTransaction()를 호출해서 내부 트랜잭션을 시작한다.
- 8. 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해서 기존 트랜잭션이 존재하는지 확인한다.
- 9. 기존 트랜잭션이 존재하므로 기존 트랜잭션에 참여한다. 기존 트랜잭션에 참여한다는 뜻은 사실 아무것도 하지 않는다는 뜻이다.
- 이미 기존 트랜잭션인 외부 트랜잭션에서 물리 트랜잭션을 시작했다. 그리고 물리 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 담아두었다.
- 따라서 이미 물리 트랜잭션이 진행중이므로 그냥 두면 이후 로직이 기존에 시작된 트랜잭션을 자연스럽게 사용하게 되는 것이다.
- 이후 로직은 자연스럽게 트랜잭션 동기화 매니저에 보관된 기존 커넥션을 사용하게 된다.
- 10. 트랜잭션 매니저는 생성한 트랜잭션 결과를 TransactionStatus에 담아서 반환하는데, 여기에 신규 트랜잭션 여부가 담겨있다. isNewTransaction()을 통해 신규 트랜잭션 여부를 확인할 수 있다. 기존 트랜잭션에 참여했기 때문에 신규 트랜잭션이 아니다. (false)
- 11. 로직2가 사용되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 외부 트랜잭션이 보관한 커넥션을 획득해서 사용한다.
응답 흐름 - 내부 트랜잭션
- 12. 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋한다.
- 13. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않는다. 이 부분이 중요한데, 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다. 아직 트랜잭션이 끝난 것이 아니기 때문에 실제 커밋을 호출하면 안된다. 물리 트랜잭션은 외부 트랜잭션을 종료할 때 까지 이어져야 한다.
응답 흐름 - 외부 트랜잭션
- 14. 로직 1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다.
- 15. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 외부 트랜잭션은 신규 트랜잭션이다. 따라서 DB 커넥션에 실제 커밋을 호출한다.
- 16. 트랜잭션 매니저에 커밋하는 것이 논리적 커밋이라면, DB에 커밋하는 것을 물리 커밋이라고 할 수 있다. 이 때, 실제 데이터베이스에 커밋이 반영되고 물리 트랜잭션이 끝난다.
핵심정리
- 트랜잭션 매니저에 커밋을 호출한다고 해서, 항상 DB 커넥션에 물리 커밋이 발생하지 않음.
- 신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행한다. 신규 트랜잭션이 아니면 실제 물리 커넥션을 사용하지 않음.
- 트랜잭션이 내부에서 추가로 사용되었을 때, 내부 트랜잭션을 트랜잭션 매니저에 커밋하는 것이 항상 물리 커밋으로 이어지지 않는다. 그래서 논리 트랜잭션과 물리 트랜잭션을 나누게 된다.
- 여러 트랜잭션이 존재한다면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리한다. 그리고 모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.
스프링 트랜잭션 전파5 - 외부 롤백
앞에서는 두 개의 논리 트랜잭션의 커밋이 성공해서 물리 트랜잭션이 커밋되는 상황을 살펴봤다. 이번에는 내부 트랜잭션은 커밋되지만, 외부 트랜잭션은 롤백되는 상황을 알아보자.
물리 트랜잭션이 커밋되기 위해서는 물리 트랜잭션에 참여하는 모든 논리 트랜잭션이 정상적으로 커밋되어야 한다. 그렇지 않은 경우라면 물리 트랜잭션은 롤백된다. 물리 트랜잭션이 롤백되면, 트랜잭션 안에서 저장된 DB 데이터도 모두 사라지게 된다.
@Test
void outer_rollback() {
log.info("외부 트랜잭션 시작");
TransactionStatus outerTx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outerTx.isNewTransaction = {}", outerTx.isNewTransaction());
log.info("내부 트랜잭션 시작");
TransactionStatus innerTx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("innerTx.isNewTransaction = {}", innerTx.isNewTransaction());
log.info("내부 트랜잭션 커밋");
txManager.commit(innerTx);
log.info("외부 트랜잭션 롤백");
txManager.rollback(outerTx);
}
위 코드를 이용해서 살펴볼 수 있다.
- 외부 트랜잭션 안에서 내부 트랜잭션이 시작된다.
- 내부 트랜잭션은 커밋하지만 외부 트랜잭션은 롤백한다.
위의 코드를 살펴보면 외부 트랜잭션에서 시작한 물리 트랜잭션의 범위가 내부 트랜잭션까지 사용된다. 이 때 외부 트랜잭션이 롤백되는데, 따라서 물리 트랜잭션도 함께 롤백되어버린다. 아래에서 실행 로그를 살펴볼 수 있다.
BasicTxTest : 외부 트랜잭션 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@908043384 wrapping conn0: url=jdbc:h2:mem:8a01ef41-3c0e-4ed9-afb4-64f59a3d4d9c user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@908043384 wrapping conn0: url=jdbc:h2:mem:8a01ef41-3c0e-4ed9-afb4-64f59a3d4d9c user=SA] to manual commit
BasicTxTest : outerTx.isNewTransaction = true
BasicTxTest : 내부 트랜잭션 시작
DataSourceTransactionManager : Participating in existing transaction
BasicTxTest : innerTx.isNewTransaction = false
BasicTxTest : 내부 트랜잭션 커밋
BasicTxTest : 외부 트랜잭션 롤백
DataSourceTransactionManager : Initiating transaction rollback // 외부 트랜잭션 롤백
//물리 트랜잭션 롤백됨
DataSourceTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@908043384 wrapping conn0: url=jdbc:h2:mem:8a01ef41-3c0e-4ed9-afb4-64f59a3d4d9c user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@908043384 wrapping conn0: url=jdbc:h2:mem:8a01ef41-3c0e-4ed9-afb4-64f59a3d4d9c user=SA] after transaction
- 외부 트랜잭션이 시작하면서 물리 트랜잭션이 conn0과 함께 시작된다.
- 내부 트랜잭션이 시작하지만, 물리 트랜잭션에 영향은 주지 않는다. 대신 Participating in existing transaction 로그를 남기며 물리 트랜잭션에 참여한다. 내부 트랜잭션은 정상적으로 커밋하고, 트랜잭션 매니저에 내부 트랜잭션 정상 커밋을 남긴다.
- 외부 트랜잭션을 트랜잭션 매니저를 통해 롤백한다. 트랜잭션 매니저는 물리 트랜잭션을 커밋하기 전에 논리 트랜잭션들의 커밋 상태를 살펴본다. 이 때, 외부 트랜잭션이 롤백되었으므로 물리 트랜잭션은 롤백된다.
그림으로 응답 흐름을 살펴보면 다음과 같다.
응답 (내부 트랜잭션)
- 1. 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋한다.
- 2. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 현재 내부 트랜잭션은 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않는다. 이 부분이 중요한데, 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다. 아직 트랜잭션이 끝난 것이 아니기 때문에 실제 커밋을 호출하면 안된다. 물리 트랜잭션은 외부 트랜잭션을 종료할 때까지 이어져야 한다.
응답 (외부 트랜잭션)
- 3. 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 롤백한다.
- 4. 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 외부 트랜잭션은 신규 트랜잭션이다. 따라서 DB 커넥션에 실제 롤백을 호출한다.
- 5. 트랜잭션 매니저에 롤백하는 것이 논리적 롤백이라면, 실제 커넥션에 롤백하는 것을 물리 롤백이라고 할 수 있다. 실제 DB에 롤백이 반영되고 물리 트랜잭션도 끝난다.
전체적으로 다 롤백이 된다는 것으로 이해할 수 있다.
스프링 트랜잭션 전파6 - 내부 롤백
앞에서는 내부 트랜잭션 커밋 - 외부 트랜잭션 롤백 → 물리 트랜잭션 롤백이 되는 상황을 알아봤다. 이번에는 내부 트랜잭션은 롤백 - 외부 트랜잭션 커밋 → 물리 트랜잭션 롤백되는 상황을 확인해본다.
이 상황은 단순하지 않다. 내부 트랜잭션이 롤백을 했지만, 내부 트랜잭션은 물리 트랜잭션에 바로 영향을 주지 않는다. 그리고 외부 트랜잭션은 커밋을 해버린다. 지금까지 학습한 내용을 돌아보면 외부 트랜잭션만 물리 트랜잭션에 영향을 주기 때문에 물리 트랜잭션이 커밋될 것 같다. 전체를 롤백해야하는데 스프링은 이 문제를 어떻게 해결할까? 결론부터 이야기하면 '모든 논리 트랜잭션이 커밋되지 않았기 때문에 물리 트랜잭션은 커밋되지 않는다'
@Test
void inner_rollback() {
log.info("외부 트랜잭션 시작");
TransactionStatus outerTx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outerTx.isNewTransaction = {}", outerTx.isNewTransaction());
log.info("내부 트랜잭션 시작");
TransactionStatus innerTx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("innerTx.isNewTransaction = {}", innerTx.isNewTransaction());
log.info("내부 트랜잭션 롤백");
txManager.rollback(innerTx);
log.info("외부 트랜잭션 커밋");
assertThatThrownBy(() -> txManager.commit(outerTx)).isInstanceOf(UnexpectedRollbackException.class);
}
위 코드로 내부 트랜잭션 롤백 - 외부 트랜잭션 커밋을 테스트한다.
- 외부 트랜잭션을 커밋하는 순간, 스프링 트랜잭션의 대원칙에 따라 물리 트랜잭션이 롤백된다. 따라서 UnexcpectedRollbackException이 발생한다.
아래에서 로그와 함께 실행 결과를 살펴본다.
BasicTxTest : 외부 트랜잭션 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:b7aa1419-6687-454a-95b1-c36e1062331e user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:b7aa1419-6687-454a-95b1-c36e1062331e user=SA] to manual commit
BasicTxTest : outerTx.isNewTransaction = true
BasicTxTest : 내부 트랜잭션 시작
DataSourceTransactionManager : Participating in existing transaction
BasicTxTest : innerTx.isNewTransaction = false
BasicTxTest : 내부 트랜잭션 롤백
// 내부 트랜잭션 롤백되면서 롤백 온리 마킹함.
DataSourceTransactionManager : Participating transaction failed - marking existing transaction as rollback-only
DataSourceTransactionManager : Setting JDBC transaction [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:b7aa1419-6687-454a-95b1-c36e1062331e user=SA] rollback-only
BasicTxTest : 외부 트랜잭션 커밋
// 외부 트랜잭션 커밋 시, 물리 트랜잭션에 롤백 온리 마킹 확인함.
DataSourceTransactionManager : Global transaction is marked as rollback-only but transactional code requested commit
DataSourceTransactionManager : Initiating transaction rollback
DataSourceTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:b7aa1419-6687-454a-95b1-c36e1062331e user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@365999192 wrapping conn0: url=jdbc:h2:mem:b7aa1419-6687-454a-95b1-c36e1062331e user=SA] after transaction
- 내부 트랜잭션을 롤백하면, 새로운 로그가 발생한다. participating transaction failed - marking ... as rollback-only라는 로그가 남는다. 내부 트랜잭션이 롤백했기 때문에 물리 트랜잭션은 'rollback만 해야한다'라는 flag를 마킹하는 것이다.
- 외부 트랜잭션을 커밋한다. 물리 트랜잭션을 커밋하기 위해서 살펴보는 도중 Global Transaction에 rollback-only가 있는 것을 확인했다. 따라서 내부에서 사용된 논리 트랜잭션에 문제가 있어서 롤백이 되었으므로, 물리 트랜잭션도 롤백한다.
그림과 함께 위의 동작을 이해해본다.
응답흐름 - 내부 트랜잭션
- 1. 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백한다.
- 2. 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 이 경우 신규 트랜잭션이 아니기 때문에 실제 롤백을 호출하지 않는다. 이 부분이 중요한데, 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다. 아직 트랜잭션이 끝난 것이 아니기 때문에 실제 롤백을 호출하면 안된다. 물리 트랜잭션은 외부 트랜잭션을 종료할 때까지 이어져야 한다.
- 3. 내부 트랜잭션은 물리 트랜잭션을 롤백하지 않는 대신에 트랜잭션 동기화 매니저에 커넥션(con)과 관련된 것은 rollbackOnly=ture 라는 표시를 해둔다. 왜냐하면 내부 트랜잭션은 물리 트랜잭션에 어떠한 일도 하지 않기 때문이다.
응답흐름 - 외부 트랜잭션
- 4. 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다. 왜냐하면 앞선 내부 트랜잭션의 롤백이 물리 트랜잭션에 어떠한 영향도 아직까지는 주지 않기 때문이다.
- 5. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 외부 트랜잭션은 신규 트랜잭션이다. 따라서 DB 커넥션에 실제 커밋을 호출해야한다. 이 때 트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly=true) 표시가 있는지 확인한다. 롤백 전용 표시가 있으면 물리 트랜잭션을 롤백한다.
- 6. 실제 DB에 롤백이 반영되고, 물리 트랜잭션도 끝난다.
- 7. 트랜잭션 매니저에 커밋을 호출한 개발자 입장에서는 분명 커밋을 기대했는데 롤백 전용 표시로 인해 실제로는 롤백이 되어버렸다.
시스템 입장에서는 커밋을 호출했지만 롤백이 되었다는 것은 분명히 개발자 / 사용자에게 알려줘야한다. 예를 들어 고객은 주문이 성공했다고 생각했는데, 실제로는 물리 트랜잭션이 롤백되어 주문이 생성되지 않은 것이다. 따라서 스프링은 이 경우 UnexpectedRollbackException 런타임 예외를 던진다. 그래서 커밋을 시도했지만, 기대하지 않은 롤백이 발생했다는 것을 명확하게 알려준다.
정리
- 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다.
- 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 커넥션에 표시한다. (rollback-only)
- 외부 트랜잭션을 커밋할 때 롤백 전용 마크(rollback-only)를 확인한다. 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고, UnexpectedRollbackException 예외를 던진다.
스프링 트랜잭션 전파7 - REQUIRES_NEW
이번에는 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 사용하는 방법을 알아보자. 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 별도의 물리 트랜잭션을 사용하는 방법이다. 별도의 트랜잭션을 사용하기 때문에 트랜잭션 별로 커밋과 롤백이 이루어지게 된다.
이 방법은 내부 트랜잭션에서 문제가 발생해서 롤백해도 외부 트랜잭션에는 영향을 주지 않는다. 마찬가지로 외부 트랜잭션에 문제가 발생해도 내부 트랜잭션에 영향을 주지 않는다. 왜냐하면 서로 다른 물리 트랜잭션을 사용하기 때문이다. 그렇지만 한 가지 문제점이 있다. 물리 트랜잭션이 2개 시작된다는 것은 한 쓰레드가 DB 커넥션을 2개를 사용한다는 것이다. 최악의 경우 이 메서드의 점유 시간이 증가하면, 불필요하게 DB 커넥션을 많이 물고 있는 쓰레드가 양산될 수 있다. 즉, DB 커넥션이 빨리 고갈될 수 있음을 의미한다.
- 내부/외부 논리 트랜잭션을 각각 물리 트랜잭션으로 분리하려면 트랜잭션을 시작할 때 (반드시) REQUIRES_NEW 옵션을 사용하면 된다.
- 내부 트랜잭션 롤백 - 물리 트랜잭션 커밋하면, 내부 트랜잭션은 실제로 롤백되고 물리 트랜잭션은 실제로 커밋된다.
아래에서 코드와 함께 살펴보려고 한다.
@Test
void inner_rollback_requires_new() {
log.info("외부 트랜잭션 시작");
TransactionStatus outerTx = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outerTx.isNewTransaction = {}", outerTx.isNewTransaction());
log.info("내부 트랜잭션 시작");
DefaultTransactionAttribute defaultTransactionAttribute = new DefaultTransactionAttribute();
// 트랜잭션 전파 옵션 REQUIRES_NEW
defaultTransactionAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus innerTx = txManager.getTransaction(defaultTransactionAttribute);
log.info("innerTx.isNewTransaction = {}", innerTx.isNewTransaction());
log.info("내부 트랜잭션 롤백");
txManager.rollback(innerTx);
log.info("외부 트랜잭션 커밋");
txManager.commit(outerTx);
}
- 내부 트랜잭션을 시작할 때, 전파 옵션은 REQUIRES_NEW로 줬다.
- 이 전파 옵션을 사용하면 내부 트랜잭션을 시작할 때, 새로운 물리 트랜잭션을 만들어서 시작한다.
실행 결과를 로그로 살펴본다.
BasicTxTest : 외부 트랜잭션 시작
DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1923130893 wrapping conn0: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1923130893 wrapping conn0: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA] to manual commit
BasicTxTest : outerTx.isNewTransaction = true
BasicTxTest : 내부 트랜잭션 시작
// 현재 사용하고 있던 물리트랜잭션 1을 잠시 미뤄둔다. (suspending)
DataSourceTransactionManager : Suspending current transaction, creating new transaction with name [null]
DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@36437323 wrapping conn1: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA] for JDBC transaction
DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@36437323 wrapping conn1: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA] to manual commit
BasicTxTest : innerTx.isNewTransaction = true
BasicTxTest : 내부 트랜잭션 롤백 -> 물리 트랜잭션2 정상 롤백
DataSourceTransactionManager : Initiating transaction rollback
DataSourceTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@36437323 wrapping conn1: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@36437323 wrapping conn1: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA] after transaction
DataSourceTransactionManager : Resuming suspended transaction after completion of inner transaction
BasicTxTest : 외부 트랜잭션 커밋 -> 물리 트랜잭션1 정상 커밋
DataSourceTransactionManager : Initiating transaction commit
DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@1923130893 wrapping conn0: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA]
DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1923130893 wrapping conn0: url=jdbc:h2:mem:598cb497-7481-4145-9e3f-c4b0b6ce48f1 user=SA] after transaction
- 내부 트랜잭션을 시작할 때, 'Suspending current transaction, creating new transaction with name' 로그가 나온다. 즉, 현재 진행 중이던 트랜잭션(conn0)를 잠시 대기하게 한다. 그리고 conn1을 새로 획득하고 트랜잭션을 셋팅한다.
- 내부 트랜잭션이지만 새로운 물리 트랜잭션을 생성했고, 그렇기 때문에 isNewTransaction() = True가 나오는 것을 볼 수 있다.
- 내부 트랜잭션을 롤백하면 conn1 커넥션을 반환 + 릴리즈한다. 그리고 'Resuming suspended transaction after completion of inner transaction' 로그가 나오는데, 대기 시켰던 트랜잭션을 다시 시작한다.
그림으로 살펴보면 다음과 같이 동작한다.
요청 흐름 - 외부 트랜잭션
- 1. 트랜잭션 매니저를 통해서 외부 트랜잭션을 시작한다.
- 2. 트랜잭션 매니저는 데이터소스를 통해 커넥션(conn1)을 생성한다.
- 3. 생성한 커넥션을 setAutoCommit(false)로 설정한다. (물리 트랜잭션1 시작)
- 4. 생성한 커넥션을 트랜잭션 동기화 매니저에 보관한다.
- 5. 트랜잭션 매니저는 생성한 트랜잭션을 TransactionStatus에 담아서 반환한다. 여기에 신규 트랜잭션 여부가 담겨 있다. isNewTransaction()를 통해서 신규 트랜잭션 여부를 확인할 수 있다. 트랜잭션을 처음 시작했으므로 신규 트랜잭션이다.
- 6. 로직1이 사용되고, 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용한다.
요청 흐름 - 내부 트랜잭션
- 7. REQUIRES_NEW 옵션과 함께 트랜잭션 매니저를 통해 내부 트랜잭션을 시작한다.
- 트랜잭션 매니저는 REQUIRES_NEW 옵션을 확인하고, 새로운 트랜잭션을 시작한다.
- 8. 트랜잭션 매니저는 데이터소스를 통해 커넥션(conn2)을 생성한다.
- 9. 생성한 커넥션을 setAutoCommit(false)로 설정한다. - 물리 트랜잭션2 시작
- 10. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션을 보관한다.
- 이 때 잠시 con1은 보류되고, 지금부터는 내부 트랜잭션이 완료될 때 까지 con2가 사용된다.
- 11. 트랜잭션 매니저는 TransactionStatus에 생성한 트랜잭션을 넣어서 반환한다. 이 때 isNewTransaction = True이다.
- 12. 로직 2가 사용되고, 커넥션이 필요한 경우 트랜잭션 동기화 매니저에 있는 con2 커넥션을 획득해서 사용한다.
응답 흐름 - 내부 트랜잭션
- 1. 로직 2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백한다. (로직 2에 문제가 있어서 롤백한다고 가정한다)
- 2. 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 현재 트랜잭션은 신규 트랜잭션이기 때문에 물리 트랜잭션 롤백을 호출한다.
- 3. 내부 트랜잭션이 conn2 (물리 트랜잭션)을 롤백한다.
- 트랜잭션이 종료되고, con2는 종료되어 커넥션 풀에 반환된다.
- con1의 보류(suspending)이 끝나고 con1을 사용한다.
응답 흐름 - 외부 트랜잭션
- 4. 외부 트랜잭션에서 커밋을 요청한다.
- 5. 외부 트랜잭션은 신규 트랜잭션이기 때문에 물리 트랜잭션을 커밋한다.
- 6. 트랜잭션 매니저는 트랜잭션을 커밋하기 전에 rollback-only를 체크한다. rollback-only 설정이 없으므로 커밋한다.
- 7. 트랜잭션 매니저는 물리 트랜잭션 1(conn1)을 커밋한다.
- 트랜잭션이 종료되고 con1은 종료되거나 커넥션 풀에 반환된다.
스프링 트랜잭션 전파8 - 다양한 전파 옵션
실무에서는 대부분 REQUIRED 옵션을 사용한다. 그리고 아주 가끔 REQUIERS_NEW를 사용하고 나머지는 거의 사용하지 않는다. 그래서 나머지 옵션은 이런 것이 있다는 정도로만 알아두고 필요할 때 찾아보자.
REQUIRED
- 기존 트랜잭션이 있으면 참여, 없으면 생성함.
- 트랜잭션이 필수라는 의미로 이해하면 됨.
REQUIRES_NEW
- 항상 새로운 트랜잭션을 생성한다.
- DB 커넥션을 많이 가져가게 됨.
SUPPORT
- 트랜잭션을 지원한다는 뜻이다.
- 기존 트랜잭션 없음 : 트랜잭션 없이 진행.
- 기존 트랜잭션 있음 : 기존 트랜잭션에 참여함.
NOT_SUPPORT
- 트랜잭션을 지원하지 않는다.
- 기존 트랜잭션 없음 : 트랜잭션 없이 진행.
- 기존 트랜잭션 있음 : 기존 트랜잭션은 보류한다. (트랜잭션 없이 진행한다)
MANDATORY
- 트랜잭션이 반드시 있어야 한다. 기존 트랜잭션이 없으면 예외가 발생한다.
- 기존 트랜잭션 없음 : IllegalTransactionStateException 예외 발생
- 기존 트랜잭션 없음 : 기존 트랜잭션에 참여
NEVER
- 트랜잭션을 사용하지 않는다는 의미
- 기존 트랜잭션 없음 : 트랜잭션 없이 진행된다.
- 기존 트랜잭션 있음 : IllegalTransactionStateException 예외 발생
NESTED
- 기존 트랜잭션 없음 : 새로운 트랜잭션을 생성함.
- 기존 트랜잭션 있음 : 중첩 트랜잭션을 생성함.
- 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만, 중첩 트랜잭션은 외부에 영향을 주지 않는다.
- 중첩 트랜잭션이 롤백 되어도 외부 트랜잭션은 커밋할 수 있다.
- 외부 트랜잭션이 롤백되면 중첩 트랜잭션도 함께 롤백된다.
- 참고
- 이것은 JDBC savepoint 기능을 사용한다. DB 드라이버에서 해당 기능을 지원하는지 확인이 필요하다.
- 중첩 트랜잭션은 JPA에서는 사용할 수 없다.
주의할 점!
isolation, timeout, readOnly는 트랜잭션이 처음 시작될 때만 적용된다. 트랜잭션에 참여하는 경우에는 적용되지 않는다. 예를 들어서 REQUIRED를 통한 트랜잭션 시작, REQUIRES_NEW를 통한 트랜잭션 시작 시점에만 적용된다.
참고
어플리케이션 개발에서 중요한 기본 원칙은 모호함을 제거하는 것이다. 개발은 명확해야 한다. 이렇게 커밋을 호출했는데, 내부에서 롤백이 발생한 경우 모호하게 두면 아주 심각한 문제가 발생한다. 이렇게 기대한 결과가 다른 경우 예외를 발생시켜서 명확하게 문제를 알려주는 것이 좋은 설계다.
'Spring > Spring' 카테고리의 다른 글
Spring Tomcat 관련 테스트 (0) | 2023.07.30 |
---|---|
Spring : 동일 타입 여러 스프링 빈 주입 받기 (0) | 2023.02.07 |
Spring DB : 각 데이터 접근기술 활용 방안 (0) | 2023.01.29 |
스프링 DB : Query DSL 관련 (0) | 2023.01.28 |
스프링 AOP : 프록시 기술과 한계 (0) | 2022.02.06 |