이번 포스팅은 스프링부트 트랜잭션 AOP와 전파에 대해 알아보자.
트랜잭션 추상화, 선언적 트랜잭션 AOP에 대해선 트랜잭션 포스팅에서 한 번 다뤘었다.
우선 선언적 트랜잭션의 적용과 적용되지 않는 것을 보자.
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired
TxService txService;
@Test
void proxy() {
log.info("proxy class={}", txService.getClass());
}
@Test
void txTest() {
txService.tx();
}
@TestConfiguration
static class TxConfig {
@Bean
TxService txService() {
return new TxService();
}
}
@Slf4j
static class TxService{
@Transactional
public void tx() {
log.info("transaction");
boolean transactionActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("transactionActive={}", transactionActive);
}
public void nonTx() {
log.info("NonTransaction");
boolean transactionActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("transactionActive={}", transactionActive);
}
}
}
알아보기 위한 테스트 코드이다. 코드 자체는 어려운 게 없을 것이다.
TransactionSynchronizationManager.isActualTransactionActive()는 현재 쓰레드에 트랜잭션이 적용되어 있는지 불린 값으로 확인할 수 있다.
INFO 16460 --- [ Test worker] hello.springtx.apply.TxBasicTest : proxy class=class hello.springtx.apply.TxBasicTest$TxService$$SpringCGLIB$$0
클래스에 CGLIB로 트랜잭션이 잘 적용되어 있는지 확인 할 수 있고.
TRACE 16601 --- [ Test worker] o.s.t.i.TransactionInterceptor : Getting transaction for [hello.springtx.apply.TxBasicTest$TxService.tx]
INFO 16601 --- [ Test worker] h.springtx.apply.TxBasicTest$TxService : transaction
INFO 16601 --- [ Test worker] h.springtx.apply.TxBasicTest$TxService : transactionActive=true
TRACE 16601 --- [ Test worker] o.s.t.i.TransactionInterceptor : Completing transaction for [hello.springtx.apply.TxBasicTest$TxService.tx]
INFO 16601 --- [ Test worker] h.springtx.apply.TxBasicTest$TxService : NonTransaction
INFO 16601 --- [ Test worker] h.springtx.apply.TxBasicTest$TxService : transactionActive=false
로그로 남긴 트랜잭션이 적용되어 있는지 없는지 확인할 수 있다.
이전에 선언적 트랜잭션에 대해 설명할 때 필자의 코드에서
@Transactional(readOnly = false),(readOnly = true)
readOnly를 false와 true를 사용할 수 있다.
스프링에서 우선순위는 더 구체적이고 자세한게 높은 우선순위를 가진다.
클래스에 적용하면 아래 메서드들은 자동으로 적용한다.
readOnly=false가 기본 옵션이다.
readOnly=true는 읽기 전용 트랜잭션인 것이고 false는 읽기 쓰기 트랜잭션이 적용된다.
인터페이스에도 @Transactional을 적용할 수 있는데 구체적인 게 우선순위를 가지니 우선순위는
클래스 메서드 - 클래스 타입 - 인터페이스 메서드 - 인터페이스 타입 순서가 된다.
가급적 구체 클래스에서 @Transactional을 사용하도록 하는게 좋다.
기본적인 AOP의 문제점과 조심해야할 점은 AOP 부분에서 다뤘으니 AOP 포스팅을 확인하자.
스프링 트랜잭션 AOP기능은 public 메서드만 트랜잭션을 적용하도록 기본 설정이 되어있다.
이유는 클래스 레벨에서 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 걸리게 되고 트랜잭션을 의도하지 않은 메서드도 걸릴 수 있기 때문에 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 외부에서 열어준 곳을 시작점으로 하기에 public에만 적용하도록 한 것이다.
스프링 3.0부턴 protected, default에도 트랜잭션이 적용된다.
스프링 트랜잭션에서 초기화 시점에서 AOP가 적용되지 않을 수 있다.
@PostConstruct
@Transactional
public void initUser() {
Address address = new Address("1","1","1");
User user = new User("1","1",1,"1@1","1",address, Tier.NORMAL);
userRepository.save(user);
}
@PostConstruct와 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다.
이유는 초기화 코드가 먼저 호출되고 다음에 AOP가 적용되기 때문이다.
해당 메서드에서는 트랜잭션이 적용되지 않는다.
@EventListener(value = ApplicationReadyEvent.class)
@Transactional
public void initUser() {
Address address = new Address("1","1","1");
User user = new User("1","1",1,"1@1","1",address, Tier.NORMAL);
userRepository.save(user);
}
대안으로는 @EventListener(value = ApplicationReadyEvent.class)를 사용하는 것이다.
이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 후 이벤트가 붙은 메서드를 호출한다.
@Transactional 옵션
@Transactional도 어떤 트랜잭션 매니저를 사용할지 지정해주어야 하는데 value, transactionManager 둘 중 하나에 트랜잭션 매니저의 스프링 빈의 이름을 적어주면 된다.
값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용하기 때문에 대부분 생략하고 사용하는 트랜잭션 매니저가 둘 이상이라면 지정해서 구분하면 된다.
rollvackFor 옵션은 어떤 예외가 발생했을 때 롤백할지 지정할 수 있다.
propagation 트랜잭션 전파에 관한 부분이다 전파 부분은 더 뒤에 다루도록 하겠다.
isolation 트랜잭션 격리 수준을 지정할 수 있는데 보통 default를 사용한다. 격리에 대한 설명은 database에서 다루었다.
readOnly 옵션을 true로 사용하면 읽기 전용 트랜잭션이 생성되고 등록, 수정, 삭제가 안 되고 읽기만 된다.
다음으로 트랜잭션에서 트랜잭션 범위 밖으로 예외를 던지는 부분에 대해 알아보자.
예외 발생 시 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
언체크 예외와 하위 예외가 발생하면 트랜잭션을 롤백한다.
체크 에이와 그 하위 예외가 발생하면 트랜잭션을 커밋한다.
@SpringBootTest
public class RollbackTest {
@Autowired
RollbackService rollbackService;
@Test
void runtimeException() {
Assertions.assertThatThrownBy(()-> rollbackService.runtimeException())
.isInstanceOf(RuntimeException.class);
}
@Test
void checkedException() {
Assertions.assertThatThrownBy(()-> rollbackService.checkedException())
.isInstanceOf(MyException.class);
}
@Test
void rollbackException() {
Assertions.assertThatThrownBy(()-> rollbackService.rollbackFor())
.isInstanceOf(MyException.class);
}
@TestConfiguration
static class RollbackTestConfig {
@Bean
RollbackService rollbackService() {
return new RollbackService();
}
}
@Slf4j
static class RollbackService {
//런타임 예외 발생=롤백
@Transactional
public void runtimeException() {
log.info("call runtimeException");
throw new RuntimeException();
}
//체크 예외 발생= 커밋
@Transactional
public void checkedException() throws MyException {
log.info("call checkedException");
throw new MyException();
}
//치크 예외 rollbackfor 지정= 롤백
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException {
log.info("call rollbackFor");
throw new MyException();
}
}
static class MyException extends Exception {
}
}
rollbackException
Creating new transaction with name [hello.springtx.exception.RollbackTest$RollbackService.rollbackFor]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-hello.springtx.exception.RollbackTest$MyException
Opened new EntityManager [SessionImpl(580904875<open>)] for JPA transaction
Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@cc91fe3]
Getting transaction for [hello.springtx.exception.RollbackTest$RollbackService.rollbackFor]
call rollbackFor
Completing transaction for [hello.springtx.exception.RollbackTest$RollbackService.rollbackFor] after exception: hello.springtx.exception.RollbackTest$MyException
Initiating transaction rollback
Rolling back JPA transaction on EntityManager [SessionImpl(580904875<open>)]
Closing JPA EntityManager [SessionImpl(580904875<open>)] after transaction
checkedException
PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Opened new EntityManager [SessionImpl(2046139918<open>)] for JPA transaction
Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@279ebd94]
Getting transaction for [hello.springtx.exception.RollbackTest$RollbackService.checkedException]
call checkedException
Completing transaction for [hello.springtx.exception.RollbackTest$RollbackService.checkedException] after exception: hello.springtx.exception.RollbackTest$MyException
Initiating transaction commit
Committing JPA transaction on EntityManager [SessionImpl(2046139918<open>)]
Closing JPA EntityManager [SessionImpl(2046139918<open>)] after transaction
runtimeException
PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Opened new EntityManager [SessionImpl(1280043480<open>)] for JPA transaction
Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@21dd91da]
Getting transaction for [hello.springtx.exception.RollbackTest$RollbackService.runtimeException]
call runtimeException
Completing transaction for [hello.springtx.exception.RollbackTest$RollbackService.runtimeException] after exception: java.lang.RuntimeException
Initiating transaction rollback
Rolling back JPA transaction on EntityManager [SessionImpl(1280043480<open>)]
Closing JPA EntityManager [SessionImpl(1280043480<open>)] after transaction
각 예외에 따른 커밋과 롤백에 대해 보았다.
체크 예외는 예외가 발생해도 커밋되는 것을 볼 수 있다.
런타임 예외는 롤백되고, rollbackFor은 체크 예외를 강제로 롤백하였다.
스프링에서 체크예외는 왜 커밋하고 언체크 예외는 롤백하는지 알아보자.
체크 예외는 비즈니스 의미가 있을 때 사용하고 언체크 예외는 복구 불가능한 예외이다.
비즈니스 의미가 있는 비즈니스 예외란?
시스템은 정상 작동하지만 비즈니스 상황에서의 예외인 것이다. 시스템적 예외가 아니기 때문에 예외를 반드시 처리해야 하는 경우가 많으므로 체크 예외를 고려할 수 있는 것이다.
비즈니스 로직에서 발생한 예외에 따라 예외의 정보를 사용자에게 제공하고 예외를 리턴 값처럼 사용하게 하고 커밋되는 부분은 예외에 따른 커밋이 될 데이터를 설정해줘야 한다.
체크 예외의 경우도 롤백하고 싶다면 rollback을 사용하면 된다.
이번엔 트랜잭션 전파에 대해 알아보자.
트랜잭션이 둘 이상 있을 때 동작과 스프링에서 제공하는 트랜젝션 전파에 대해 보자.
@Slf4j
@SpringBootTest
public class TxPropagationTest {
@Autowired
PlatformTransactionManager txManager;
@TestConfiguration
static class Config{
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Test
void commit() {
log.info("트랜잭션 시작");
TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 커밋 전");
txManager.commit(status);
log.info("트랜잭션 커밋 후");
}
@Test
void rollback() {
log.info("트랜잭션 시작");
TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 롤백 전");
txManager.rollback(status);
log.info("트랜잭션 롤백 후");
}
}
트랜잭션 매니저를 통해 트랜잭션을 시작하고 커밋과 롤백을 하는 코드이다.
어려운 부분은 없을 것이다 다음으로 트랜잭션 1이 끝난 후 트랜잭션 2를 수행하는 경우를 보자
@Slf4j
@SpringBootTest
public class TxPropagationTest {
@Autowired
PlatformTransactionManager txManager;
@TestConfiguration
static class Config{
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Test
void commit() {
log.info("트랜잭션 시작");
TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 커밋 전");
txManager.commit(status);
log.info("트랜잭션 커밋 후");
}
@Test
void rollback() {
log.info("트랜잭션 시작");
TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 롤백 전");
txManager.rollback(status);
log.info("트랜잭션 롤백 후");
}
@Test
void transaction2() {
log.info("트랜잭션1 시작");
TransactionStatus status1 = txManager.getTransaction(new DefaultTransactionAttribute());
txManager.commit(status1);
log.info("트랜잭션 커밋1 후");
log.info("트랜잭션2 시작");
TransactionStatus status2 = txManager.getTransaction(new DefaultTransactionAttribute());
txManager.commit(status2);
log.info("트랜잭션 커밋2 후");
}
}
2024-04-20T21:43:46.982+09:00 INFO 17487 --- [ Test worker] h.s.propagation.TxPropagationTest : 트랜잭션1 시작
2024-04-20T21:43:46.986+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-04-20T21:43:47.002+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1828372669 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA] for JDBC transaction
2024-04-20T21:43:47.005+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1828372669 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA] to manual commit
2024-04-20T21:43:47.007+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2024-04-20T21:43:47.008+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@1828372669 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA]
2024-04-20T21:43:47.010+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1828372669 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA] after transaction
2024-04-20T21:43:47.011+09:00 INFO 17487 --- [ Test worker] h.s.propagation.TxPropagationTest : 트랜잭션 커밋1 후
2024-04-20T21:43:47.012+09:00 INFO 17487 --- [ Test worker] h.s.propagation.TxPropagationTest : 트랜잭션2 시작
2024-04-20T21:43:47.013+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-04-20T21:43:47.014+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@389752004 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA] for JDBC transaction
2024-04-20T21:43:47.015+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@389752004 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA] to manual commit
2024-04-20T21:43:47.015+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2024-04-20T21:43:47.015+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@389752004 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA]
2024-04-20T21:43:47.015+09:00 DEBUG 17487 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@389752004 wrapping conn0: url=jdbc:h2:mem:6bf399d9-86e7-460e-a076-44bafc808aad user=SA] after transaction
2024-04-20T21:43:47.016+09:00 INFO 17487 --- [ Test worker] h.s.propagation.TxPropagationTest : 트랜잭션 커밋2 후
transaction2의 로그를 확인해 보면
conn 커넥션을 쓴 부분에 커넥션 풀에서 트랜잭션 1과 2가 같은 0 커넥션을 사용한 것을 확인할 수 있다.
트랜잭션 1이 conn0을 사용하고 커넥션풀에 반납한 것을 트랜잭션 2가 conn0을 획득해서 사용한 것은 다른 커넥션으로 인지해야 한다.
히카리 커넥션 풀에서 커넥션을 획득하면 실제 커넥션을 그대로 반환하는 것이 아니라 내부 관리를 위해 히카리 프록시 커넥션이라는 객체를 생성해서 반환한다.
결과적으로 conn0으로 커넥션이 재사용된 것을 알 수 있고, 히카리프록시커넥션을 통해 각 커넥션 풀에서 커넥션을 조회한 것을 볼 수 있다.
트랜잭션이 각각 수행되면서 사용되는 db 커넥션도 각각 다르다.
트랜잭션을 각자 관리하기 때문에 전체 트랜잭션을 묶을 수 없다.
@Test
void transaction3() {
log.info("트랜잭션1 시작");
TransactionStatus status1 = txManager.getTransaction(new DefaultTransactionAttribute());
txManager.commit(status1);
log.info("트랜잭션1 커밋 후");
log.info("트랜잭션2 시작");
TransactionStatus status2 = txManager.getTransaction(new DefaultTransactionAttribute());
txManager.rollback(status2);
log.info("트랜잭션2 롤백 후");
}
전체 트랜잭션을 묶지 않고 각각 관리했기 때문에 1은 커밋되고 2는 롤백된다.
이제 트랜잭션 전파에 대해 알아보자
트랜잭션을 각각 사용하는 것이 아니라 이미 트랜잭션을 실행 중인데 여기에 추가로 트랜잭션을 수행하는 것이다.
이런 경우 어떻게 동작할지 정하는 것을 트랜잭션 전파라 한다.
트랜잭션 전파(propagation)의 옵션 중에서 REQUIRED를 기준으로 설명하도록 하겠다.
나머지 전파 옵션은 뒤에 설명하도록 하겠다.
로직 1을 가진 트랜잭션 1이 먼저 실행중이고 로직2를 가진 트랜잭션2가 실행중이라 할 때 트랜잭션1이 상대적으로 외부이기 때문에 외부 트랜잭션이라 하고 트랜잭션 2는 내부 트랜잭션이라 할 때 내부 트랜잭션은 외부 트랜잭션이 수행되는 도중에 호출되기 때문에 마치 내부에 있는 것처럼 보인다. 스프링에선 이 경우 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다. 내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다. 이 방식은 기본 동작이고 옵션을 통해 다른 동작방식도 선택할 수 있다.
스프링은 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눈다.
논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다.
물리 트랜잭션은 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다.
논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이다.
논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타난다. 단순히 트랜잭션이 하나인 경우 둘을 구분하지 않는다.
논리 트랜잭션의 원칙이 있다.
모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
@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);
}
2024-04-20T23:22:08.562+09:00 INFO 17933 --- [ Test worker] h.s.propagation.TxPropagationTest : 외부 트랜잭션 시작
2024-04-20T23:22:08.567+09:00 DEBUG 17933 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-04-20T23:22:08.584+09:00 DEBUG 17933 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1720303364 wrapping conn0: url=jdbc:h2:mem:071e7eed-473f-4d88-8c25-ec24eb491023 user=SA] for JDBC transaction
2024-04-20T23:22:08.588+09:00 DEBUG 17933 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1720303364 wrapping conn0: url=jdbc:h2:mem:071e7eed-473f-4d88-8c25-ec24eb491023 user=SA] to manual commit
2024-04-20T23:22:08.590+09:00 INFO 17933 --- [ Test worker] h.s.propagation.TxPropagationTest : outer.isNewTransaction()=true
2024-04-20T23:22:08.591+09:00 INFO 17933 --- [ Test worker] h.s.propagation.TxPropagationTest : 내부 트랜잭션 시작
2024-04-20T23:22:08.592+09:00 DEBUG 17933 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Participating in existing transaction
2024-04-20T23:22:08.593+09:00 INFO 17933 --- [ Test worker] h.s.propagation.TxPropagationTest : inner.isNewTransaction()=false
2024-04-20T23:22:08.593+09:00 INFO 17933 --- [ Test worker] h.s.propagation.TxPropagationTest : 내부 트랜잭션 커밋
2024-04-20T23:22:08.594+09:00 INFO 17933 --- [ Test worker] h.s.propagation.TxPropagationTest : 외부 트랜잭션 커밋
2024-04-20T23:22:08.595+09:00 DEBUG 17933 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2024-04-20T23:22:08.599+09:00 DEBUG 17933 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@1720303364 wrapping conn0: url=jdbc:h2:mem:071e7eed-473f-4d88-8c25-ec24eb491023 user=SA]
2024-04-20T23:22:08.601+09:00 DEBUG 17933 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1720303364 wrapping conn0: url=jdbc:h2:mem:071e7eed-473f-4d88-8c25-ec24eb491023 user=SA] after transaction
외부 트랜잭션과 내부 트랜잭션을 추가하고 새로운 트랜잭션인지 확인하는 로그를 추가했다.
isNewTransaction을 확인해 보면 outer는 true, inner는 false로 새로운 트랜잭션인지 아닌지 확인할 수 있다.
내부 트랜잭션을 시작하는 시점에 이미 외부 트랜잭션이 실행 중이고 이 경우 내부 트랜잭션이 외부 트랜잭션에 참여한다.
이 경우 외부 트랜잭션을 내부 트랜잭션이 참여한다는 의미이다.
Participating in existing transaction이 기존 존재하는 외부 트랜잭션을 참여한다는 뜻이다.
이렇게 여러 트랜잭션이 함께 사용되는 경우 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다.
트랜잭션 매니저에 커밋하는 것이 논리 커밋이고 실제 커넥션에 커밋하는 것을 물리 커밋이다.
핵심은 트랜잭션 매니저에 커밋한다고 실제 커넥션에 물리 커밋이 발생하지 않는다.
트랜잭션이 내부에서 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.
@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);
}
외부 트랜잭션이 롤백된 경우에 대해 보자.
이전과 로그는 비슷하고
Initiating transaction commit
Initiating transaction rollback
Initiating transaction commit이 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("외부 트랜잭션 커밋");
assertThatThrownBy(() -> txManager.commit(outer))
.isInstanceOf(UnexpectedRollbackException.class);
}
검증문을 사용해 외부 트랜잭션을 커밋할 때 발생한 것을 잡았다.
2024-04-20T23:53:09.282+09:00 INFO 18223 --- [ Test worker] h.s.propagation.TxPropagationTest : 외부 트랜잭션 시작
2024-04-20T23:53:09.285+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-04-20T23:53:09.304+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1464233911 wrapping conn0: url=jdbc:h2:mem:51facbac-1025-4cac-948b-042955b1224f user=SA] for JDBC transaction
2024-04-20T23:53:09.308+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1464233911 wrapping conn0: url=jdbc:h2:mem:51facbac-1025-4cac-948b-042955b1224f user=SA] to manual commit
2024-04-20T23:53:09.309+09:00 INFO 18223 --- [ Test worker] h.s.propagation.TxPropagationTest : 내부 트랜잭션 시작
2024-04-20T23:53:09.310+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Participating in existing transaction
2024-04-20T23:53:09.311+09:00 INFO 18223 --- [ Test worker] h.s.propagation.TxPropagationTest : 내부 트랜잭션 롤백
2024-04-20T23:53:09.311+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Participating transaction failed - marking existing transaction as rollback-only
2024-04-20T23:53:09.312+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Setting JDBC transaction [HikariProxyConnection@1464233911 wrapping conn0: url=jdbc:h2:mem:51facbac-1025-4cac-948b-042955b1224f user=SA] rollback-only
2024-04-20T23:53:09.314+09:00 INFO 18223 --- [ Test worker] h.s.propagation.TxPropagationTest : 외부 트랜잭션 커밋
2024-04-20T23:53:09.346+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Global transaction is marked as rollback-only but transactional code requested commit
2024-04-20T23:53:09.346+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
2024-04-20T23:53:09.350+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1464233911 wrapping conn0: url=jdbc:h2:mem:51facbac-1025-4cac-948b-042955b1224f user=SA]
2024-04-20T23:53:09.352+09:00 DEBUG 18223 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1464233911 wrapping conn0: url=jdbc:h2:mem:51facbac-1025-4cac-948b-042955b1224f user=SA] after transaction
실행 로그이다.
기존과 다른 부분만 살펴보자.
Participating transaction failed - marking existing transaction as rollback-only
내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지 않는다. 대신 기존 트랜잭션을 롤백 전용으로 표시한다.
외부 트랜잭션을 커밋하지만
Global transaction is marked as rollback-only but transactional code requested commit
커밋을 호출했지만 전체 트랜잭션이 롤백 전용으로 표시되어 있다. 따라서 물리 트랜잭션을 롤백한다.
Initiating transaction rollback으로 롤백된 것을 확인할 수 있다.
트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 이 경우 신규 트랜잭션이 아니기 때문에 실제 롤백을 호출하지 않는다. 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다. 아직 트랜잭션이 끝난 것이 아니기 때문에 실제 롤백을 호출하면 안 된다. 물리 트랜잭션은 외부 트랜잭션을 종료할 때까지 이어져야 한다.
내부 트랜잭션은 트랜잭션 동기화 매니저에 rollbackOnly-true라는 표시를 해둔다.
외부 트랜잭션에서는 커밋을 호출했는데 실제로는 트랜잭션 매니저에 롤백 표시가 있어서 커밋과 롤백의 결정권자인 외부 트랜잭션에서는 커밋을 기대했지만 내부에서 롤백을 표시했기 때문에 UnexpectedRollbackException 런타임 예외를 던져줘서 기대하지 않는 롤백이 발생했다고 명확히 알려준다.
다음은 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 사용하는 방법이다.
이 방법은 내부 트랜잭션에서 문제가 생겨 롤백이 일어나도 외부 트랜잭션에 영향을 주지 않고 반대의 경우에도 영향을 주지 않는다.
물리 트랜잭션을 분리하려면 내부 트랜잭션을 시작할 때 REQUIRES_NEW 옵션을 사용하면 된다.
외부 트랜잭션과 내부 트랜잭션이 각각 별도의 물리 트랜잭션을 가진다.
별도의 트랜잭션을 가지는 것은 DB 커넥션을 따로 사용한다는 의미이다.
각각 물리 트랜잭션은 서로 데이터에 영향을 주지 않는다.
@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);
}
REQUIRES_NEW 옵션을 사용한 예제이다. 이전과 다를 건 별로 없이 옵션만 추가됐다.
2024-04-21T00:24:05.103+09:00 INFO 18288 --- [ Test worker] h.s.propagation.TxPropagationTest : 외부 트랜잭션 시작
2024-04-21T00:24:05.107+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-04-21T00:24:05.124+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@328046478 wrapping conn0: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA] for JDBC transaction
2024-04-21T00:24:05.128+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@328046478 wrapping conn0: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA] to manual commit
2024-04-21T00:24:05.129+09:00 INFO 18288 --- [ Test worker] h.s.propagation.TxPropagationTest : outer.isNewTransaction()=true
2024-04-21T00:24:05.130+09:00 INFO 18288 --- [ Test worker] h.s.propagation.TxPropagationTest : 내부 트랜잭션 시작
2024-04-21T00:24:05.132+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Suspending current transaction, creating new transaction with name [null]
2024-04-21T00:24:05.133+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1852194933 wrapping conn1: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA] for JDBC transaction
2024-04-21T00:24:05.133+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1852194933 wrapping conn1: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA] to manual commit
2024-04-21T00:24:05.133+09:00 INFO 18288 --- [ Test worker] h.s.propagation.TxPropagationTest : inner.isNewTransaction()=true
2024-04-21T00:24:05.133+09:00 INFO 18288 --- [ Test worker] h.s.propagation.TxPropagationTest : 내부 트랜잭션 롤백
2024-04-21T00:24:05.134+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
2024-04-21T00:24:05.136+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1852194933 wrapping conn1: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA]
2024-04-21T00:24:05.138+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1852194933 wrapping conn1: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA] after transaction
2024-04-21T00:24:05.138+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Resuming suspended transaction after completion of inner transaction
2024-04-21T00:24:05.139+09:00 INFO 18288 --- [ Test worker] h.s.propagation.TxPropagationTest : 외부 트랜잭션 커밋
2024-04-21T00:24:05.139+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2024-04-21T00:24:05.140+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@328046478 wrapping conn0: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA]
2024-04-21T00:24:05.141+09:00 DEBUG 18288 --- [ Test worker] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@328046478 wrapping conn0: url=jdbc:h2:mem:687d3a93-8afe-49ba-be7d-c85945a6ee0b user=SA] after transaction
로그를 보면 내부와 외부가 conn을 다른 것을 사용하는 것을 확인할 수 있으며 가각 별도의 물리 트랜잭션으로 롤백과 커밋이 작동하게 된다.
주의점으론 이럴 땐 커넥션이 동시에 2개가 사용된다는 점이다.
이외에 전파 옵션에 대해서도 알아보자.
설명할 때 Default 옵션인 REQUIRED를 사용했고 REQUIRED_NEW도 설명했다.
대부분 기본 옵션을 사용하기 때문에 간략히 설명하고 넘어가도록 하겠다.
SUPPORT
트랜잭션을 지원한다는 뜻으로 기존 트랜잭션이 없으면 그대로 진행하고 기존 트랜잭션이 있을 시 참여한다.
NOT_SUPPORT
트랜잭션을 지원하지 않는다는 의미이다.
기존 트랜잭션이 없으면 없는 대로 진행하고 기존 트랜잭션이 있으면 기존 트랜잭션은 보류하고 그대로 진행한다.
MANDATORY
의무사항으로 트랜잭션이 반드시 있어야 한다. 기존 트랜잭션이 없을 시 예외가 발생하고 기존 트랜잭션이 있을 시 참여한다.
NEVER
트랜잭션을 사용하지 않는다는 의미이다. 기존 트랜잭션이 있으면 예외가 발생한다.
기존 트랜잭션이 없을 시 트랜잭션 없이 진행하고 있다면 예외가 발생한다.
NESTED
기존 트랜잭션이 없으면 새로운 트랜잭션을 생성한다.
기존 트랜잭션이 있으면 중첩 트랜잭션을 만든다.
중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만 중첩 트랜잭션은 외부에 영향을 주지 않는다.
중첩 트랜잭션이 롤백되어도 외부 트랜잭션은 커밋할 수 있다.
외부 트랜잭션이 롤백되면 중첩 트랜잭션도 함께 롤백된다.
JPA에서는 사용할 수 없다.
isolation, timeout, readOnly는 트랜잭션이 처음 시작될 때만 적용된다. 트랜잭션에 참여하는 경우에는 적용되지 않는다.
여기까지 스프링 트랜잭션 AOP와 트랜잭션 전파를 알아보았다.
본 포스팅은 인프런의 김영한 강사님의 스프링 스프링 DB 2편의 코드, 내용을 일부 발췌하여 사용했습니다.
스프링 DB 2편 - 데이터 접근 활용 기술 | 김영한 - 인프런
김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드
www.inflearn.com
'database' 카테고리의 다른 글
CDC (Change Data Capture) 변경 데이터 캡처 (0) | 2025.05.11 |
---|---|
PostgreSQL에 대해서 (0) | 2024.06.25 |
데이터베이스 데이터 접근 기술 (1) | 2024.04.20 |
Transaction에 대해서 (1) | 2024.04.19 |
데이터 베이스와 JDBC, ORM (1) | 2024.04.18 |