트랜잭션 두 번 사용
double_commit()
@Test
void double_commit() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션1 커밋");
txManager.commit(tx1);
log.info("트랜잭션2 시작");
TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션2 커밋");
txManager.commit(tx2);
}
트랜잭션1
- 트랜잭션1을 시작하고, 커넥션 풀에서 conn0 커넥션을 획득
- 트랜잭션1을 커밋하고, 커넥션 풀에 conn0 커넥션을 반납
트랜잭션2
- 트랜잭션2을 시작하고, 커넥션 풀에서 conn0 커넥션을 획득했다.
- 트랜잭션2을 커밋하고, 커넥션 풀에 conn0 커넥션을 반납했다.
※ 주의! : 로그를 보면 트랜잭션1과 트랜잭션2가 같은 conn0 커넥션을 사용중이다. 중간에 커넥션 풀 때문에 그런 것이다


double_commit_rollback()
@Test
void double_commit_rollback() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션1 커밋");
txManager.commit(tx1);
log.info("트랜잭션2 시작");
TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션2 롤백");
txManager.rollback(tx2);
}
- 전체 트랜잭션을 묶지 않고 각각 관리했기 때문에, 트랜잭션1은 커밋, 트랜잭션2는 롤백된다.

전파 기본
트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행할 경우 : 어떻게 동작 할지 결정 → 전파 (propagation)



- 물리 트랜잭션은 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다.
- 실제 커넥션을 통해서 트랜잭션을 시작( setAutoCommit(false)) 하고, 실제 커넥션을 통해서 커밋, 롤백하는 단위이다.
논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이다.
트랜잭션 중요 원칙
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
즉, 모든 트랜잭션 매니저를 커밋해야 물리 트랜잭션이 커밋된다. 하나의 트랜잭션 매니저라도 롤백하면 물리 트랜잭션은 롤백된다.
전파 예제

- 외부 트랜잭션 = 신규 트랜잭션 O (isNewTransaction = true)
- 내부 트랜잭션 = 신규 트랜잭션 X (isNewTransaction = false)
트랜잭션 참여
- 외부에서 시작된 물리적인 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻이다.
- 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것이다.
스프링이 중복 커밋 문제 해결하는 방법
- 내부 트랜잭션을 시작할 때 Participating in existing transaction 이라는 메시지를 확인
- 이 메시지는 내부 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 뜻
- 즉, 외부 트랜잭션만 물리 트랜잭션을 시작하고, 커밋한다.
- 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다. 이를 통해 트랜잭션 중복 커밋 문제를 해결한다.


핵심 정리
- 트랜잭션 매니저에 커밋을 호출한다고 해서 항상 실제 커넥션에 물리 커밋이 발생하지는 않는다는 점이다.
신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행한다. - 이렇게 트랜잭션이 내부에서 추가로 사용되면 트랜잭션 매니저에 커밋하는 것이 항상 물리 커밋으로 이어지지 않는다.
- 그래서 이 경우 논리 트랜잭션과 물리 트랜잭션 (외부, 내부)을 나누게 된다.
- 트랜잭션이 내부에서 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.
외부 롤백


내부 롤백

- 외부 트랜잭션 시작
물리 트랜잭션을 시작한다. - 내부 트랜잭션 시작 기존 트랜잭션에 참여한다.
Participating in existing transaction - 내부 트랜잭션 롤백
Participating transaction failed - marking existing transaction as rollback-only - 내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지 않는다. 대신에 기존 트랜잭션을 롤백 전용으로 표시한다.
- 외부 트랜잭션 커밋
외부 트랜잭션을 커밋한다.
Global transaction is marked as rollback-only
커밋을 호출했지만, 전체 트랜잭션이 롤백 전용으로 표시되어 있다. 따라서 물리 트랜잭션을 롤백한다.

★ 정리
- 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다.
- 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시한다.
외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인한다. 롤백 전용 마크가 표시 있으면 물리 트랜잭션을 롤백하고, UnexpectedRollbackException 예외를 던진다.
REQUIRES_NEW

- 이렇게 물리 트랜잭션을 분리하려면 내부 트랜잭션을 시작할 때 REQUIRES_NEW 옵션을 사용
- 외부 트랜잭션과 내부 트랜잭션이 각각 별도의 물리 트랜잭션을 가진다.
- 별도의 물리 트랜잭션을 가진다는 뜻은 DB 커넥션을 따로 사용한다는 뜻이다.
- 이 경우 내부 트랜잭션이 롤백되면서 로직 2가 롤백되어도 로직 1에서 저장한 데이터에는 영향을 주지 않는다.
- 즉, 로직2는 롤백되고, 로직1은 커밋된다.
외부 트랜잭션 시작
- 외부 트랜잭션을 시작하면서 conn0 를 획득하고 물리 트랜잭션을 시작한다.
- 외부 트랜잭션은 신규 트랜잭션이다.( outer.isNewTransaction()=true)
내부 트랜잭션 시작
- 내부 트랜잭션을 시작하면서 conn1 를 획득하고 물리 트랜잭션을 시작한다.
- PROPAGATION_REQUIRES_NEW 옵션으로 완전히 새로운 신규 트랜잭션으로 생성된다.(inner.isNewTransaction()=true )


★ 정리
- REQUIRES_NEW 옵션을 사용하면 물리 트랜잭션이 명확하게 분리된다.
- REQUIRES_NEW 를 사용하면 데이터베이스 커넥션이 동시에 2개 사용된다는 점을 주의해야 한다.
다양한 전파 옵션
REQUIRED
가장 많이 사용하는 기본 설정이다.
트랜잭션이 필수라는 의미로 이해하면 된다.
- 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.
REQUIRES_NEW
항상 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 있음: 새로운 트랜잭션을 생성한다.
SUPPORT
트랜잭션을 지원한다는 뜻이다. 기존 트랜잭션이 없으면, 없는대로 진행하고, 있으면 참여한다.
- 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.
NOT_SUPPORT
트랜잭션을 지원하지 않는다는 의미이다.
- 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
기존 트랜잭션 있음: 트랜잭션 없이 진행한다. (기존 트랜잭션 : 보류)
MANDATORY
의무사항이다. 트랜잭션이 반드시 있어야 한다. 기존 트랜잭션이 없으면 예외가 발생한다.
- 기존 트랜잭션 없음: IllegalTransactionStateException 예외 발생
기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.
NEVER
트랜잭션을 사용하지 않는다는 의미이다. 기존 트랜잭션이 있으면 예외가 발생한다.
- 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
- 기존 트랜잭션 있음: IllegalTransactionStateException 예외 발생
NESTED
- 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 있음: 중첩 트랜잭션을 만든다.
- 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만, 중첩 트랜잭션은 외부에 영향을 주지 않는다.
중첩 트랜잭션이 롤백 되어도 외부 트랜잭션은 커밋할 수 있다.
외부 트랜잭션이 롤백 되면 중첩 트랜잭션도 함께 롤백된다.
◎ 참고
JDBC savepoint 기능을 사용한다. DB 드라이버에서 해당 기능을 지원하는지 확인이 필요하다.
중첩 트랜잭션은 JPA에서는 사용할 수 없다.
트랜잭션 전파와 옵션
isolation , timeout , readOnly 는 트랜잭션이 처음 시작될 때만 적용된다. 트랜잭션에 참여하는 경우에는 적용되지 않는다.
ex) REQUIRED 를 통한 트랜잭션 시작, REQUIRES_NEW 를 통한 트랜잭션 시작 시점에만 적용된다.
'Spring > DB 2편' 카테고리의 다른 글
스프링 트랜잭션 전파2 - 활용 (0) | 2023.08.12 |
---|---|
스프링 트랜잭션 이해 (0) | 2023.08.11 |
데이터 접근 기술 - 활용 방안 (0) | 2023.08.11 |
데이터 접근 기술 - Querydsl (0) | 2023.08.11 |
데이터 접근 기술 - 스프링 데이터 JPA (0) | 2023.08.11 |