1. 트랜잭션 기본
* 트랜잭션 관련 로그
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
#JPA log
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG
#JPA SQL
logging.level.org.hibernate.SQL=DEBUG
1) 기본 커밋, 롤백
package hello.springtx.propagation;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import javax.sql.DataSource;
@Slf4j
@SpringBootTest
public class BasicTxTest {
@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("트랜잭션 커밋 완료");
}
- 원래 기본으로 트랜잭션 매니저 등록해주지만, 내가 작성하면 작성한 트랜잭션 매니저 적용
- DataSourceTransactionManager를 스프링 빈으로 등록
2) 트랜잭션 각각 두번 사용
@Test
void double_commit() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션1 커밋");
txManager.commit(tx1);
log.info("트랜잭션 커밋 완료");
log.info("트랜잭션2 시작");
TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션2 커밋");
txManager.commit(tx2);
log.info("트랜잭션 커밋 완료");
}
- 트랜잭션 1
- 트랜잭션 1 시작 -> 히카리 커넥션 풀에서 conn0 커넥션 획득
- 트랜잭션 1 커밋 -> 히카리 커넥션 풀에 conn0 커넥션 반납
- 트랜잭션 2
- 트랜잭션 2 시작 -> 히카리 커넥션 풀에서 conn0 커넥션 획득
- 트랜잭션 2 커밋 -> 히카리 커넥션 풀에 conn0 커넥션 반납
- 트랜잭션 각각 수행 -> 각각 다른 커넥션 사용
2. 스프링 트랜잭션 전파 (Propagation)
2-1. 전파 예제
- 외부 트랜잭션 (처음 트랜잭션)이 수행중인데, 내부 트랜잭션이 수행되는 경우
- 외부와 내부를 묶어서 하나의 트랜잭션으로 만듦
- 내부 트랜잭션이 외부 트랜잭션에 "참여"
- 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶임
- 물리 트랜잭션 : 실제 데이터베이스에 적용되는 트랜잭션 (커넥션을 통해 커밋, 롤백하는 단위)
- 논리 트랜잭션 : 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위
(트랜잭션이 진행되는 중에 추가로 트랜잭션을 사용하는 경우에만 쓰이는 개념)
📌 원칙
* 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됨
* 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됨
1) 모두 커밋되는 경우
@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);
}
- 외부 트랜잭션이 수행중인데, 내부 트랜잭션 추가 -> 내부 트랜잭션이 외부 트랜잭션에 참여
- 내부 트랜잭션이 외부 트랜잭션을 그대로 이어받아서 따른다는 뜻 !
- -> 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶임
- isNewTransaction()
- 외부 트랜잭션은 처음 수행된 트랜잭션 -> isNewTransaction = true
- 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여 -> isNewTransaction = false
- 커밋을 2번 호출
- 외부 트랜잭션만 물리 트랜잭션을 시작, 커밋 가능
- 내부 트랜잭션이 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버림 (커밋/롤백 -> 트랜잭션 종료)
- 내부 트랜잭션은 사실상 아무것도 하지 않음
* 흐름 순서
- txManager.getTransaction() : 외부 트랜잭션 시작
- 트랜잭션 매니저는 데이터소스를 통해 커넥션 생성
- 생성한 커넥션을 수동 커밋 모드(setAutoCommit(false))로 설정 -> 물리 트랜잭션 시작
- 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션 보관
- 트랜잭션 매니저는 트랜잭션 생성 결과를 TransactionStatus에 담아서 반환하는데, 여기에 신규 트랜잭션 여부가 담겨있음 (isNewTransaction = true)
- 로직1 사용 ( + 커넥션 필요한 경우, 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션 획득해서 사용 )
- txManager.getTransaction() : 내부 트랜잭션 시작
- 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해 기존 트랜잭션이 존재하는지 확인
- 기존 트랜잭션 존재 -> 기존 트랜잭션에 참여 -> 물리 트랜잭션을 건들지 않음
- 트랜잭션 매니저는 트랜잭션 생성 결과를 TransactionStatus에 담아서 반환하는데, 여기에서 신규 트랜잭션 여부 확인 (isNewTransaction = false)
- 로직2 사용 ( + 커넥션 필요한 경우, 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션 획득해서 사용 )
- 로직2 끝난 뒤 트랜잭션 매니저를 통해 내부 트랜잭션 커밋
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
신규 트랜잭션 X -> 실제 커밋 호출 X (논리 커밋) - 로직1 끝난 뒤 트랜잭션 매니저를 통해 외부 트랜잭션 커밋
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
신규 트랜잭션 O -> 실제 커밋 호출 O (물리 커밋)
📌 정리
* 트랜잭션 매니저에 커밋 호출 -> 항상 실제 커넥션에 물리 커밋이 발생하는 것 X
* 신규 트랜잭션인 경우에만 실제 커넥션 사용 -> 물리 커밋, 롤백 수행
2) 외부 롤백 -> 전체 롤백되는 경우
@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);
}
- 내부 트랜잭션은 새로운 트랜잭션 X -> 실제 물리 트랜잭션에 커밋 X
- 외부 트랜잭션 롤백 -> 물리 롤백
3) 내부 롤백 -> 전체 롤백
- 내부 트랜잭션은 새로운 트랜잭션 X -> 물리 트랜잭션에 영향 X -> 근데 왜 롤백되냐?
@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);
}
* 응답 흐름
- 로직2에 문제 발생 -> 내부 트랜잭션 롤백
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
신규 트랜잭션 X -> 실제 롤백 호출 X - 대신, 트랜잭션 동기화 매니저에 롤백 전용 마크 (rollbackOnly = true) 표시 !
- 로직1 수행 -> 외부 트랜잭션 커밋
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
신규 트랜잭션 O -> rollbackOnly 표시 확인 -> rollbackOnly = true 표시 有 -> 롤백 요청 - 물리 롤백
- 커밋을 호출했지만 롤백됨 -> UnexpectedRollbackException 런타임 예외를 던져서 명확하게 알려줌
📌 정리
* 논리 트랜잭션이 하나라도 롤백 -> 물리 트랜잭션 롤백
* 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크 표시
* 외부 트랜잭션을 커밋할 때 롤백 전용 마크 확인 -> 있으면 물리 트랜잭션 롤백 -> UnexpectedRollbackException 예외 던짐
4) 외부 트랜잭션과 내부 트랜잭션 분리 (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);
}
- setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW)
- 새로운 물리 트랜잭션 시작
- txManager.getTransaction() : 외부 트랜잭션 시작
- 트랜잭션 매니저는 데이터소스를 통해 커넥션 생성
- 생성한 커넥션을 수동 커밋 모드(setAutoCommit(false))로 설정 -> 물리 트랜잭션 시작
- 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션 보관
- 트랜잭션 매니저는 트랜잭션 생성 결과를 TransactionStatus에 담아서 반환하는데, 여기에 신규 트랜잭션 여부가 담겨있음 (isNewTransaction = true)
- 로직1 사용 ( + 커넥션 필요한 경우, 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션 획득해서 사용 )
- txManager.getTransaction() + REQUIRES_NEW 옵션 : 새로운 내부 트랜잭션 시작
- 트랜잭션 매니저는 데이터소스를 통해 커넥션 생성
- 생성한 커넥션을 수동 커밋 모드(setAutoCommit(false))로 설정 -> 물리 트랜잭션 시작
- 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션 보관
- 이때 con1은 잠시 보류, con2 사용
- 트랜잭션 매니저는 신규 트랜잭션 생성 결과 반환 (isNewTransaction = true)
- 로직 2 사용 ( + 커넥션 필요한 경우, 트랜잭션 동기화 매니저에 있는 con2 커넥션 획득해서 사용 )
- 로직 2 문제 발생 -> 롤백 요청
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
신규 트랜잭션 O -> 실제 롤백 호출 O - con2 물리 트랜잭션 롤백 (트랜잭션 종료 -> con2 종료되거나 커넥션 풀에 반납)
- 로직 1 수행 -> 커밋 요청
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
신규 트랜잭션 O -> 실제 커밋 호출 O - 이때 rollbackOnly 설정 확인 -> 없음 -> 커밋
- con1 물리 트랜잭션 커밋 (트랜잭션 종료 -> con1 종료되거나 커넥션 풀에 반납)
2-2. 전파 옵션
(1) REQUIRED (기본) - 트랜잭션 필수
- 기존 트랜잭션 無 : 새로운 트랜잭션 생성
- 기존 트랜잭션 有 : 기존 트랜잭션에 참여
(2) REQUIRES_NEW - 항상 새로운 트랜잭션 생성
- 기존 트랜잭션 無 : 새로운 트랜잭션 생성
- 기존 트랜잭션 有 : 새로운 트랜잭션 생성
(3) SUPPORT - 트랜잭션 지원
- 기존 트랜잭션 無 : 트랜잭션 없이 진행
- 기존 트랜잭션 有 : 기존 트랜잭션에 참여
(4) NOT_SUPPORT - 트랜잭션 지원 X
- 기존 트랜잭션 無 : 트랜잭션 없이 진행
- 기존 트랜잭션 有 : 트랜잭션 없이 진행 (기존 트랜잭션은 보류)
(5) MANDATORY - 트랜잭션 의무
- 기존 트랜잭션 無 : IllegalTransactionStateException 예외 발생
- 기존 트랜잭션 有 : 기존 트랜잭션에 참여
(6) NEVER - 트랜잭션 사용 X
- 기존 트랜잭션 無 : 트랜잭션 없이 진행
- 기존 트랜잭션 有 : IllegalTransactionStateException 예외 발생
(7) NESTED- 트랜잭션 중첩 (JPA에서 사용 X)
- 기존 트랜잭션 無 : 새로운 트랜잭션 생성
- 기존 트랜잭션 有 : 중첩 트랜잭션 생성
isolation, timeout, readOnly : 트랜잭션이 처음 시작될 때만 적용 (참여하는 경우 X)
'Spring' 카테고리의 다른 글
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 1. 인텔리제이로 스프링 부트 시작하기 (0) | 2023.07.31 |
---|---|
[인프런/스프링DB 2편] 6. 스프링 트랜잭션 전파 (2) (0) | 2023.07.28 |
[인프런/스프링 DB 2편] 5. 스프링 트랜잭션 (0) | 2023.07.20 |
[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (6) SpringData JPA + Query DSL (0) | 2023.07.18 |
[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (5) Query DSL (0) | 2023.07.17 |