1. 트랜잭션 전파
- 트랜잭션 전파란?
이전까지 스프링 트랜잭션에 관해 내부 원리와 함께 깊이 있는 학습을 했다.
이번에는 좀 더 나아가 *만약 하나의 트랜잭션 안에 또 다른 트랜잭션을 진행*한다면 어떻게 처리해야하는지, 즉 트랜잭션 전파가 어떻게 이뤄지는지 알아볼 것이다.
내용을 학습하기 전, 미리 짐작해봤을 때 다양한 상황이 존재할 것 같았다.
- 모든 트랜잭션이 성공해야만 커밋으로 인정할까?
- 성공한 트랜잭션은 커밋하고, 실패한 트랜잭션은 따로 롤백할까? (이 경우가 쓰일까..?)
- 스프링의 기본 설정
스프링 트랜잭션에서 따로 전파(propagation)설정을 하지 않으면 `REQUIRED`모드로 작동한다. 이는 모든 트랜잭션이 커밋되어야 최종 트랜잭션이 커밋되고, 하나의 트랜잭션이라도 롤백된다면 최종 트랜잭션은 롤백된다는 의미이다. *미리 짐작했던 상황에서 1번에 부합했다.*
스프링이 추상화해서 설명하는 다이어그램을 보며 좀 더 자세히 살펴보자.
편의상 최초 시작한 트랜잭션, 즉 상대적으로 밖에 위치한 트랜잭션을 외부 트랜잭션이라 칭한다.
이 경우 외부 트랜잭션에서 내부 트랜잭션을 호출하는 로직이 포함되어있는 것이다.
스프링은 위 다이어그램을 아래와 같이 추상화하여 설명한다.
물리 트랜잭션은 우리가 이해하는 *실제 데이터베이스에 적용되는 트랜잭션*을 의미한다. 실제 DB 커넥션을 통해 트랜잭션을 시작하고, 커밋 및 롤백하는 단위이다.
논리 트랜잭션은 *트랜잭션 매니저*를 통해 트랜잭션을 사용하는 단위이다.
응? 기존에 하나의 트랜잭션을 사용할 때에도 트랜잭션 매니저를 통해 트랜잭션을 사용했는데, 이 경우는 물리 트랜잭션일까 논리 트랜잭션일까?
트랜잭션 전파가 발생하지 않고 하나의 독립적인 트랜잭션이 진행되는 경우 *물리와 논리 트랜잭션을 구분하지 않는다.*
실제로 외부 트랜잭션을 시작해 status를 얻어온 뒤 해당 트랜잭션이 최초 트랜잭션인지 확인할 수 있는 메소드가 있는데, 여기서 true를 받아야지만 실제 데이터베이스 커넥션도 커밋이 발생하게 되어 *자연스럽게 하나의 과정으로 연결되는 것이다.*
트랜잭션 전파가 발생하는 경우는 내부 트랜잭션에 커밋 및 롤백이 발생하면, 최초 트랜잭션인지 판단 후 false이므로 데이터베이스 커넥션에 커밋이나 롤백을 하지 않고 트랜잭션이 종료되는 것이다.
자, 기본 설정인 `REQUIRED`모드에 대해서 정리하고 다양한 상황들에서 어떻게 적용되는지 알아보자.
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
2. 예제
- 외부, 내부 트랜잭션 모두 커밋
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
innerTx();
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
외부, 내부 트랜잭션이 모두 커밋되는 상황이다.
`status.isNewTransaction()`을 통해 해당 트랜잭션이 외부 트랜잭션인지 확인할 수 있다.
로그를 보면, 외부 트랜잭션이 시작될 때 실제 DB 커넥션을 커넥션 풀에서 얻어오는 모습을 확인할 수 있다.
하지만, 내부 트랜잭션이 시작할 때는 `Participating in existing transaction`이라는 로그와 함께 별도의 DB 커넥션을 얻어오지 않고 아무 동작을 하지 않는 모습을 보인다.
이는 `REQUIRED`옵션에 의해 기존 *트랜잭션 동기화 매니저*에 커넥션이 존재하면 새로운 커넥션을 획득하지 않고 기존 트랜잭션에 참여하겠다는 의미이다.
따라서 커밋 후에도 별도의 동작이없고, *외부 트랜잭션이 커밋된 이후에야 실제 DB 커넥션이 풀에 반환되며 물리 트랜잭션이 종료된다.*
- 외부 롤백
@Test
void outer_rollback () {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
innerTx();
log.info("외부 트랜잭션 롤백");
txManager.rollback(outer);
}
위에서 언급했듯, 물리 트랜잭션의 커밋 및 롤백을 판단하는 주체는 외부 트랜잭션이다.
따라서 내부의 트랜잭션 결과가 어떻게 됐든, 외부에서 롤백한다면 물리 트랜잭션은 롤백된다.
로그 결과로 보이듯 물리 트랜잭션인 DB 커넥션의 트랜잭션 결과가 롤백되는 모습을 확인할 수 있다.
- 내부 롤백
@Test
void inner_rollback () {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
innerTx_rollback();
log.info("외부 트랜잭션 커밋");
Assertions.assertThatThrownBy(() -> txManager.commit(outer))
.isInstanceOf(UnexpectedRollbackException.class);
}
내부 롤백은 상황이 조금 다르다. 현재 상황은 내부 롤백 후 외부 트랜잭션은 커밋을 시도한다.
로그 결과를 확인하면, 내부 트랜잭션이 롤백될 때 기존 트랜잭션에 `rollback-only`라는 것을 마킹한다고 로그가 남아있다.
이후 외부 트랜잭션의 커밋을 시도하면 Global transaction이 `rollback-only`로 마킹되어있지만 커밋을 요청했다는 로그가 남는다.
결과적으로는 *롤백을 수행하는 모습이다.*
추가로 테스트 코드에서 볼 수 있듯, 이 경우는 `UnexpectedRollbackException`예외를 발생시킨다. *커밋을 시도했지만, 기대하지 않는 롤백이 발생했다는 것을 명확히 알려주는 것이다.*
3. 다양한 전파 옵션
스프링은 다양한 전파 옵션을 제공한다. 사실 실무에서 가장 많이 사용되는 것은 기본 옵션인 `REQUIRED`이다.
아주 가끔은 외부 트랜잭션과 내부 트랜잭션을 별도의 커넥션을 통해 각기 다른 트랜잭션으로 처리하고 싶을 수 있다.
내부에서 롤백되어도 외부가 커밋이면 외부는 커밋하거나, 내부만 커밋하고 외부는 롤백하거나 등등 ...
이 때 사용되는 옵션이 `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());
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
TransactionDefinition을 통해 전파 옵션을 설정할 수 있다.
로그를 살펴봐도 각각의 트랜잭션에서 다른 커넥션을 사용하고 있다는 점을 확인할 수 있고,
내부 트랜잭션의 성공 여부에 관계없이 외부 트랜잭션이 동작하는 모습을 확인할 수 있다.
하지만 하나의 요청에서 DB 커넥션을 2개를 사용하기 때문에, 성능적인 측면에서 주의해야한다. 가령 커넥션 풀의 커넥션 수가 부족해진다던지...
이외에도 다양한 전파 옵션이 있지만 대체로 사용하지 않는 편이다. 필요한 경우 공식 문서를 참고하면 될 것 같다!
마지막으로 트랜잭션 옵션인 `isolation`, `timeout`, `readOnly` 설정은 트랜잭션이 처음 시작되는 시점에만 적용된다는 사실을 주의하자. `REQUIRED`, `REQUIRES_NEW`를 통해 최초 시작된 트랜잭션에만 해당 옵션이 적용되고, 전파에 참여한 내부 트랜잭션에는 적용되지 않는다.
출처 : 인프런, 김영한의 스프링 DB 2편 - 데이터 접근 핵심 원리
'Spring' 카테고리의 다른 글
[Spring] 스프링의 템플릿 콜백 패턴 (0) | 2024.03.03 |
---|---|
[Spring] 필드 동기화와 ThreadLocal (0) | 2024.03.01 |
[Spring] @Transactional의 다양한 기능과 주의사항 (1) | 2024.02.27 |
[Spring] 스프링의 RuntimeException 추상화 (1) | 2023.12.18 |
[Spring] 자바 예외 계층 이해 (0) | 2023.12.16 |