스프링 트랜잭션 이해

: 스프링은 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개 사용되어 성능 측면에서는 주의해야 함.