트랜잭션 관련 포스팅
- [Spring] 트랜잭션 추상화 정리
- [Spring] 트랜잭션 동기화 정리
- [Spring] 트랜잭션 AOP 동작 흐름
- [Spring] 트랜잭션 AOP 사용 시 주의점 (ft. Spring AOP self-invocation)
- [Spring] 스프링 트랜잭션 전파와 롤백
- [Spring] 트랜잭션 전파 옵션
트랜잭션 전파란?
- 트랜잭션을 각각 사용하는 것이 아니라 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행하면 어떻게 되는가?
- 기존 트랜잭션과 별도의 트랜잭션을 따로 진행해야 하는가? 혹은 기존 트랜잭션을 그대로 이어 받아서 트랜잭션을 수행해야 하는가?
- 이렇게 복수의 트랜잭션을 사용할 경우 동작 방식을 결정하는 것을 트랜잭션 전파(propagation)라고 한다
- 참고로 스프링은 다양한 트랜잭션 전파 옵션을 제공하며, 본문은 트랜잭션 전파의 기본 옵션인 REQUIRED를 기준으로 설명하고자 한다
복수의 트랜잭션 예제
외부 트랜잭션이 수행중인데, 내부 트랜잭션이 추가로 수행되는 경우
- 외부 트랜잭션이 수행중이고 끝나지 않은 상태에서 내부 트랜잭션이 수행된다
- 외부 트랜잭션
- 두 트랜잭션 중 상대적으로 밖에 있으므로 외부 트랜잭션이라 명칭한다
- 외부 트랜잭션 = 처음 시작한 트랜잭션
- 내부 트랜잭션
- 외부에 트랜잭션이 수행되고 있는 도중에 호출되기 때문에 마치 내부에 있는 것처럼 보여서 내부 트랜잭션이라 명칭한다
- 내부 트랜잭션 = 나중에 시작한 트랜잭션
- 스프링은 이 경우 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다
- 즉, 내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다
- 이것이 기본 동작이며 옵션을 통해 다른 동작방식도 선택할 수 있다
물리 트랜잭션과 논리 트랜잭션
- 스프링은 이해를 돕기 위해 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눈다
- 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이며, 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다
- 물리 트랜잭션은 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션을 뜻하며, 실제 커넥션을 통해서 트랜잭션을 시작하고 실제 커넥션을 통해서 커밋/롤백하는 단위이다
- 논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타난다(REQUIRED 옵션 기준)
- 단순히 트랜잭션이 하나인 경우 둘을 구분하지는 않음
- 트랜잭션이 사용중일 때 또 다른 트랜잭션이 내부에 사용되면 여러가지 복잡한 상황이 발생하게 되는데, 이때 논리 트랜잭션 개념을 도입하면 다음과 같은 단순한 원칙을 만들 수 있다
- (중요!!) 모든 논리 트랜잭션이 커밋돼야 물리 트랜잭션이 커밋된다
- (중요!!) 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다
트랜잭션 전파 예제
외부 트랜잭션이 수행중인데, 내부 트랜잭션을 추가로 수행한 경우
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // outer.isNewTransaction()=true
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // inner.isNewTransaction()=false
log.info("내부 트랜잭션 커밋");
txManager.commit(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
- 외부 트랜잭션은 처음 수행된 트랜잭션이므로 신규 트랜잭션이 된다
- 내부 트랜잭션을 시작하는 시점에는 이미 외부 트랜잭션이 진행중인 상태이다. 이 경우 내부 트랜잭션은 외부 트랜잭션에 참여한다
- 트랜잭션 참여
- 내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻
- = 외부 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
- = 외부에서 시작된 물리적인 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
- 정리하면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것
- 내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻
- 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여했으므로 신규 트랜잭션이 아니다(isNewTransaction = false)
inner_commit() 실행결과
- 외부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통한 물리 트랜잭션을 시작(mannual commit)하고, DB 커넥션을 통해 커밋하는 것을 확인할 수 있다
- 반면 내부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통해 커밋하는 로그를 확인할 수 없다
정리
- 외부 트랜잭션만 물리 트랜잭션을 시작/커밋한다
- 만약 내부 트랜잭션이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리므로 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없다
- 따라서 내부 트랜잭션은 DB 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다
- 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다
- 이를 통해 트랜잭션 중복 커밋 문제를 해결한다
트랜잭션 동작 흐름
요청 흐름
- 기본적인 동작 흐름은 [Spring] 트랜잭션 AOP 동작 흐름을 참고하자
- 외부 트랜잭션
- 실제 물리 트랜잭션을 시작한다
- 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus(여기에 신규 트랜잭션의 여부가 담겨있음)에 담아서 반환하는데, 여기서는 트랜잭션을 처음 시작했으므로 신규 트랜잭션이다
- 내부 트랜잭션
- 기존 트랜잭션이 존재하므로 기존 트랜잭션에 참여한다(= 아무것도 하지 않는다)
- 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 반환하는데, 여기서는 기존 트랜잭션에 참여했으므로 신규 트랜잭션이 아니다
응답 흐름
- 내부 트랜잭션
- 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋한다
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작하는데, 이 경우 신규 트랜잭션이 아니므로 실제 커밋을 호출 X
- 외부 트랜잭션
- 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작하는데, 이 경우 신규 트랜잭션이므로 DB 커넥션에 실제 커밋 호출
- 트랜잭션 매니저에 커밋하는 것이 논리적인 커밋이라면 실제 커넥션에 커밋하는 것을 물리 커밋이라 할 수 있다
- 실제 데이터베이스에 커밋이 반영되고, 물리 트랜잭션도 끝난다
정리
- 트랜잭션 매니저에 커밋을 호출한다고 해서 항상 실제 커넥션에 물리 커밋이 발생하지는 않는다
- 신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행하며, 신규 트랜잭션이 아니면 실제 물리 커넥션을 사용하지 않는다
- 트랜잭션이 내부에서 추가로 사용되면 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다
트랜잭션 롤백
1) 외부 롤백
내부 트랜잭션은 커밋되는데 외부 트랜잭션이 롤백되는 경우
@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);
}
- 외부 롤백은 간단한 상황이므로 이해하기 쉽다
- 이 경우 내부 트랜잭션을 커밋했더라도 내부 트랜잭션 안에서 저장한 데이터도 모두 함께 롤백된다
- 논리 트랜잭션이 하나라도 롤백되면 전체 물리 트랜잭션이 롤백되기 때문이다
outer_rollback 실행결과
- 커밋이 롤백으로 바뀌었을 뿐 앞서 나온 내용과 거의 유사하다
응답흐름
2) 내부 롤백
내부 트랜잭션은 롤백되는데 외부 트랜잭션이 커밋되는 상황
@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); // 외부 트랜잭션 커밋 시에 UnexpectedRollbackException이 발생한다
}
- 내부 롤백은 외부 롤백보다 좀 더 복잡하다
- 내부 트랜잭션을 롤백했지만, 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않기 때문이다
- 이 경우도 외부 롤백과 마찬가지로 전체 트랜잭션이 롤백된다
- "하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다"라는 원칙을 기억하자
- 외부 트랜잭션을 커밋하는 순간 UnexpectedRollbackException 예외가 발생한다
inner_rollback() 실행결과
- 1) 외부 트랜잭션 시작
- 물리 트랜잭션을 시작한다
- 2) 내부 트랜잭션 시작
- Participating in exsiting transaction
- 기존 트랜잭션에 참여한다
- 3) 내부 트랜잭션 롤백
- Participating transaction failed - marking existing transaction as rollback-only
- 내부 트랜잭션을 롤백하더라도 실제 물리트랜잭션은 롤백 X
- 대신 기존 트랜잭션을 롤백 전용으로 표시한다
- 4) 외부 트랜잭션 커밋
- 외부 트랜잭션을 커밋한다
- Global transaction is marked as rollback-only
- 커밋을 호출했지만 전체 트랜잭션이 롤백 전용으로 표시되어있으므로 물리 트랜잭션을 롤백한다
응답흐름
- 내부 트랜잭션은 물리 트랜잭션을 롤백하지 않는 대신에 트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly=true) 표시를 해둔다
- 외부 트랜잭션을 커밋하는 시점에 트랜잭션 매니저는 트랜잭션 동기화 매니저에 롤백 전용 표시가 있는지 확인하고, 롤백 전용 표시가 있다면 물리 트랜잭션을 롤백한다
- 마지막으로 클라이언트에 UnexpectedRollbackException 예외를 던진다
- 트랜잭션 매니저에 커밋을 호출한 개발 입장에서는 분명 커밋을 기대했는데 롤백 전용 표시로 인해 실제로는 롤백이 되어버렸기 때문이다
- 스프링은 UnexpectedRollbackException 예외를 던짐으로써 "커밋을 시도했지만 기대하지 않은 롤백이 발생했다"라는 사실을 명확하게 알려준다
3) 정리
- 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다
- 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시한다
- 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인한다
- 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고 UnexpectedRollbackException 예외를 던진다
외부 트랜잭션과 내부 트랜잭션을 분리해서 사용하기 (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); // 커밋
}
- REQUIRES_NEW 옵션을 주면 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용할 수 있다
- 즉, 커밋과 롤백도 각각 별도로 이루어진다
- 내부 트랜잭션을 시작할 때 전파 옵션인 propagationBehavior에 PROPAGATION_REQUIRES_NEW 옵션을 주면 내부 트랜잭션을 시작할 때 기존 트랜잭션에 참여하는 것이 아니라 새로운 물리 트랜잭션을 만들어서 시작하게 된다
inner_rollback_requires_new() 실행결과
- 1) 외부 트랜잭션 시작
- 외부 트랜잭션을 시작하면서 conn0을 획득하고 manual commit으로 변경해서 물리 트랜잭션을 시작한다
- 외부 트랜잭션은 신규 트랜잭션이다
- 2) 내부 트랜잭션 시작
- 내부 트랜잭션을 시작하면서 conn1을 획득하고 manual commit으로 변경해서 물리 트랜잭션을 시작한다
- 내부 트랜잭션은 외부 트랜잭션에 참여하지 않고 새로운 신규 트랜잭션으로 생성된다
- 3) 내부 트랜잭션 롤백
- 내부 트랜잭션을 롤백한다
- 내부 트랜잭션은 신규 트랜잭션이므로 실제 물리 트랜잭션을 롤백한다
- 내부 트랜잭션은 conn1을 사용하므로 conn1에 물리 롤백을 수행한다
- 4) 외부 트랜잭션 커밋
- 외부 트랜잭션을 커밋한다
- 외부 트랜잭션은 신규 트랜잭션이므로 실제 물리 트랜잭션을 커밋한다
- 외부 트랜잭션은 conn0을 사용하므로 conn0에 물리 커밋을 수행한다
요청 흐름
- 내부 트랜잭션
- 트랜잭션 매니저는 REQUIRES_NEW 옵션을 확인하여 기존 트랜잭션에 참여하는 것이 아니라 새로운 트랜잭션을 시작한다
- 트랜잭션 매니저가 트랜잭션 동기화 매니저에 커넥션을 보관할 때 con1은 잠시 보류되고 con2가 사용된다
- 내부 트랜잭션이 완료될 때까지는 con2가 사용된다
응답 흐름
- 내부 트랜잭션
- 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션에 여부에 따라 다르게 동작하는데, 현재 내부 트랜잭션은 신규 트랜잭션이므로 실제 롤백을 호출한다
- 내부 트랜잭션이 con2의 물리 트랜잭션을 롤백한다
- 이후에 con1의 보류가 끝나고 다시 con1을 사용한다
- 외부 트랜잭션
- 외부 트랜잭션에 커밋을 요청하는데, 외부 트랜잭션은 신규 트랜잭션이므로 물리 트랜잭션을 커밋한다
- 이때 rollbackOnly 설정을 체크하는데, rollbackOnly 설정이 없으므로 커밋한다
- con1을 통해 물리 트랜잭션을 커밋한다
- 외부 트랜잭션에 커밋을 요청하는데, 외부 트랜잭션은 신규 트랜잭션이므로 물리 트랜잭션을 커밋한다
정리
- REQUIRES_NEW 옵션을 사용하면 물리 트랜잭션이 명확하게 분리된다
- REQUIRES_NEW 옵션을 사용하면 데이터베이스 커넥션이 동시에 2개 사용된다는 점을 주의해야 한다
참고
'Spring > Spring' 카테고리의 다른 글
[Spring] 빈 중복 시 해결방법(@Autowired, @Qualifier, @Primary) (0) | 2022.12.19 |
---|---|
[Spring] 트랜잭션 전파 옵션 (0) | 2022.09.07 |
[Spring] 트랜잭션 AOP 사용 시 주의점 (ft. Spring AOP self-invocation) (0) | 2022.09.01 |
[Spring] 테스트 코드를 작성할 때 @Transactional을 주의하라 (4) | 2022.08.17 |
[Spring] @Transactional과 메모리 DB를 활용한 데이터 접근 계층 테스트 (0) | 2022.08.13 |