스프링 트랜잭션 이해
: 스프링은 PlatformTransactionManager 라는 인터페이스를 통해 트랜잭션을 추상화
: PlatformTransactionManager 를 사용하는 방법은 크게 2가지로 구분
1) 선언적 트랜잭션 관리
@Transactional 어노테이션 하나만 선언해서 매우 편리하게 트랜잭션을 적용
2)프로그래밍 방식 트랜잭션 관리
트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성
: AopUtils.isAopProxy() 로 Aop 확인
: TransactionSynchronizationManager.isActualTransactionActive()
현재 쓰레드에 트랜잭션이 적용되어 있는지 확인할 수 있는 기능
: 스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위.
: 스프링의 @Transactional 은 다음 두 가지 규칙이 존재
1) 우선순위 규칙
2)클래스에 적용하면 메서드는 자동 적용
: public이 아닌 곳에 @Transactional 사용 시 예외가 발생하지는 않고, 트랜잭션 적용만 무시
(스프링 부트 3.0 부터는 protected , package-visible (default 접근제한자)에도 트랜잭션이 적용)
: 스프링 초기화 시점에 트랜잭션 사용하기
- ApplicationReadyEvent 이벤트를 사용
- 이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출
트랜잭션 옵션 소개
rollbackFor
: 이 옵션을 사용하면 기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정
ex) @Transactional(rollbackFor = Exception.class)
이렇게 지정하면 체크 예외인 Exception이 발생해도 커밋 대신 롤백(자식 타입도 롤백됨)
noRollbackFor
: 기본 정책에 추가로 어떤 예외가 발생했을 때 롤백하면 안되는지 지정
isolation
: 트랜잭션 격리 수준 지정(일반적으로 많이 사용하는 READ COMMITTED-커밋된 읽기)
timeout
: 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정
readOnly=true 옵션을 사용하면 읽기 전용 트랜잭션이 생성
: 예외 발생 시 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백
- 언체크 예외인 RuntimeException , Error 와 그 하위 예외가 발생하면 트랜잭션을 롤백
- 체크 예외인 Exception 과 그 하위 예외가 발생하면 트랜잭션을 커밋
- 정상 응답(리턴)하면 트랜잭션 커밋
: 비즈니스 의미가 있는 비즈니스 예외라는 것은??
ex) 주문 시 결제 잔고가 부족하면 주문 데이터를 저장하고 결제 상태를 대기로 처리 이 경우 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내
: JPA 테이블 자동 생성
application.properties 에 spring.jpa.hibernate.ddl-auto 옵션 사용 (none이면 테이블 생성 x, create면 애플리케이션 시작 시점에 테이블 생성 )
스프링 트랜잭션 전파1 - 기본
Commit
TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 커밋 시작");
txManager.commit(status);
l-og.info("트랜잭션 커밋 완료");
// Commit() 실행 로그
main] hello.springtx.propagation.BasicTxTest : 트랜잭션 커밋 시작
o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
main] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@903028779 wrapping conn0: url=jdbc:h2:mem:81400691-b69c-440c-af57-422ffadf0607 user=SA]
main] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [HikariProxyConnection@903028779 wrapping conn0: url=jdbc:h2:mem:81400691-b69c-440c-af57-422ffadf0607 user=SA] after transaction
main] hello.springtx.propagation.BasicTxTest : 트랜잭션 커밋 완료
Rollback
TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("트랜잭션 롤백 시작");
txManager.rollback(status);
log.info("트랜잭션 롤백 완료");
// Rollback() 실행 로그
hello.springtx.propagation.BasicTxTest : 트랜잭션 롤백 시작
main] o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
main] o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@903028779 wrapping conn0: url=jdbc:h2:mem:5f0a68a1-c6da-47c0-94da-ad5e2bee39f6 user=SA]
main] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection
Double Commit Rollback
: 전체 트랜잭션을 묶지 않고 각각 관리했기 때문에,
트랜잭션1에서 저장한 데이터는 커밋되고 트랜잭션2에서 저장한 데이터는 롤백됨.
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);
}
// double_commit_rollback() 실행 로그
: Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
: Acquired Connection [HikariProxyConnection@1554473375 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA] for JDBC transaction
: Switching JDBC Connection [HikariProxyConnection@1554473375 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA] to manual commit
: 트랜잭션1 커밋
: Initiating transaction commit
: Committing JDBC transaction on Connection [HikariProxyConnection@1554473375 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA]
: Releasing JDBC Connection [HikariProxyConnection@1554473375 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA] after transaction
: 트랜잭션2 시작
: Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
: Acquired Connection [HikariProxyConnection@1426420939 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA] for JDBC transaction
: Switching JDBC Connection [HikariProxyConnection@1426420939 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA] to manual commit
: 트랜잭션2 롤백
: Initiating transaction rollback
: Rolling back JDBC transaction on Connection [HikariProxyConnection@1426420939 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA]
: Releasing JDBC Connection [HikariProxyConnection@1426420939 wrapping conn0: url=jdbc:h2:mem:6e1d9f3d-d663-4026-adf3-a54cea9ff5c2 user=SA]
Inner Commit
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // true
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // false
log.info("내부 트랜잭션 커밋");
txManager.commit(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
: 내부 트랜잭션을 시작하는 시점에는 이미 외부 트랜잭션이 진행중인 상태
: 이 경우 내부 트랜잭션은 외부 트랜잭션에 참여
: 내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻
: 다른 관점으로 보면 외부 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
: 외부에서 시작된 물리적인 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
: 정리하면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것
// inner_commit() 실행 로그
외부 트랜잭션 시작
Creating new transaction with name [null]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@1943867171 wrapping conn0] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@1943867171 wrapping conn0] to manual commit
outer.isNewTransaction()=true
내부 트랜잭션 시작
Participating in existing transaction
inner.isNewTransaction()=false
내부 트랜잭션 커밋
외부 트랜잭션 커밋
Initiating transaction commit
Committing JDBC transaction on Connection [HikariProxyConnection@1943867171wrapping conn0]
Releasing JDBC Connection [HikariProxyConnection@1943867171 wrapping conn0] after transaction
: 내부 트랜잭션을 시작할 때 Participating in existing transaction 이라는 메시지를 확인할 수 있는데,
이 메시지는 내부 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 뜻
: 만약 내부 트랜잭션이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리기 때문에 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없음
:따라서 내부 트랜잭션은 DB 커넥션을 통한 물리 트랜잭션을 커밋하면 안됨
:스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 함
Inner Commit
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);
}
:내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지는 않지만 기존 트랜잭션을 롤백 전용으로 표시
: 외부 트랜잭션 커밋을 호출했지만 내부 트랜잭션 롤백에서 기존 트랜잭션을 롤백 전용으로 표시했기 때문에 물리 트랜잭션을 롤백
: 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백
: 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시
: 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인해서 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고, UnexpectedRollbackException 예외를 던짐
Inner Rollback Requires New
void inner_rollback_requires_new() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // true
log.info("내부 트랜잭션 시작");
DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus inner = txManager.getTransaction(definition);
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // true
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
: 내부 트랜잭션을 시작 시 전파 옵션인 propagationBehavior 에PROPAGATION_REQUIRES_NEW 옵션 설정
: 이 전파 옵션을 사용하면 내부 트랜잭션을 시작할 때 기존 트랜잭션에 참여하는 것이 아니라 새로운 물리 트랜잭션을 만들어서 시작하게 됨
: 하지만 REQUIRES_NEW 를 사용하면 데이터베이스 커넥션이 동시에 2개 사용되어 성능 측면에서는 주의해야 함.
'Backend' 카테고리의 다른 글
코딩자율학습단 스프링부트_1장 (1) | 2024.08.03 |
---|---|
스프링 DB 데이터 접근 활용 기술_04 (0) | 2024.01.10 |
스프링 DB 데이터 접근 활용 기술_02 (0) | 2024.01.06 |
스프링 DB 데이터 접근 활용 기술_01 (1) | 2024.01.06 |
전자정부표준프레임워크_06 (0) | 2022.03.11 |