[스프링 DB 2편] #10. 스프링 트랜잭션 전파 #637
Develop-KIM
started this conversation in
동환
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
스프링 트랜잭션 전파1 - 커밋, 롤백
트랜잭션이 둘 이상 있을 때 어떻게 동작하는지 자세히 알아보고, 스프링이 제공하는 트랜잭션 전파(propagation)라는 개념 이해
BasicTxTest
DataSourceTransactionManager를 스프링 빈으로 등록했다. 이후 트랜잭션 매니저인PlatformTransactionManager를 주입 받으면 방금 등록한DataSourceTransactionManager가 주입된다.commit()
txManager.getTransaction(new DefaultTransactionAttribute())트랜잭션 매니저를 통해 트랜잭션을 시작(획득)한다.
txManager.commit(status)트랜잭션을 커밋한다.
commit() - 실행 로그
rollback()
txManager.getTransaction(new DefaultTransactionAttribute())트랜잭션 매니저를 통해 트랜잭션을 시작(획득)한다.
txManager.rollback(status)트랜잭션을 롤백한다.
rollback() - 실행 로그
스프링 트랜잭션 전파2 - 트랜잭션 두 번 사용
트랜잭션이 각각 따로 사용되는 경우
double_commit() - BasicTxTest 추가
double_commit() - 실행 로그
트랜잭션1
Acquired Connection [HikariProxyConnection@1064414847 wrapping conn0] for JDBC transactionconn0커넥션을 획득했다.Releasing JDBC Connection [HikariProxyConnection@1064414847 wrapping conn0]after transactionconn0커넥션을 반납했다.주의!
로그를 보면 트랜잭션1과 트랜잭션2가 같은
conn0커넥션을 사용중이다. 이것은 중간에 커넥션 풀 때문에 그런 것이다.트랜잭션1은
conn0커넥션을 모두 사용하고 커넥션 풀에 반납까지 완료했다. 이후에 트랜잭션2가conn0를 커넥션 풀에서 획득한 것이다.따라서 둘은 완전히 다른 커넥션으로 인지하는 것이 맞다.
그렇다면 둘을 구분할 수 있는 다른 방법은 없을까?
히카리 커넥션 풀에서 커넥션을 획득하면 실제 커넥션을 그대로 반환하는 것이 아니라 내부 관리를 위해
히카리 프록시 커넥션이라는 객체를 생성해서 반환한다.
물론 내부에는 실제 커넥션이 포함되어 있다. 이 객체의 주소를 확인하면 커넥션 풀에서 획득한 커넥션을 구분할 수 있다.
Acquired Connection [HikariProxyConnection@1000000 wrapping conn0]Acquired Connection [HikariProxyConnection@2000000 wrapping conn0]히카리 커넥션풀이 반환해주는 커넥션을 다루는 프록시 객체의 주소가 트랜잭션1은

HikariProxyConnection@1000000이고,트랜잭션2는
HikariProxyConnection@2000000으로 서로 다른 것을 확인할 수 있다.결과적으로
conn0을 통해 커넥션이 재사용 된 것을 확인할 수 있고,HikariProxyConnection@1000000,HikariProxyConnection@2000000을 통해 각각 커넥션 풀에서 커넥션을 조회한 것을 확인할 수 있다.트랜잭션1에서 저장한 데이터는 커밋되고, 트랜잭션2에서 저장한 데이터는 롤백된다.
double_commit_rollback() - BasicTxTest 추가
double_commit_rollback() - 실행 로그
스프링 트랜잭션 전파3 - 전파 기본
트랜잭션을 각각 사용하는 것이 아니라, 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행하면 어떻게 될까?
기존 트랜잭션과 별도의 트랜잭션을 진행해야 할까? 아니면 기존 트랜잭션을 그대로 이어 받아서 트랜잭션을 수행해야 할까?
이런 경우 어떻게 동작할지 결정하는 것을 **트랜잭션 전파(propagation)**라 한다.
참고로 스프링은 다양한 트랜잭션 전파 옵션을 제공한다.
참고
외부 트랜잭션이 수행중인데, 내부 트랜잭션이 추가로 수행됨

이것이 기본 동작이고, 옵션을 통해 다른 동작방식도 선택할 수 있다.
물리 트랜잭션, 논리 트랜잭션

실제 커넥션을 통해서 트랜잭션을 시작(
setAutoCommit(false))하고, 실제 커넥션을 통해서 커밋, 롤백하는 단위이다.단순히 트랜잭션이 하나인 경우 둘을 구분하지는 않는다.
그럼 왜 이렇게 논리 트랜잭션과 물리 트랜잭션을 나누어 설명하는 것일까?
트랜잭션이 사용중일 때 또 다른 트랜잭션이 내부에 사용되면 여러가지 복잡한 상황이 발생한다.
이때 논리 트랜잭션 개념을 도입하면 다음과 같은 단순한 원칙을 만들 수 있다.
원칙
풀어서 설명하면 이렇게 된다. 모든 트랜잭션 매니저를 커밋해야 물리 트랜잭션이 커밋된다.

하나의 트랜잭션 매니저라도 롤백하면 물리 트랜잭션은 롤백된다.
스프링 트랜잭션 전파4 - 전파 예제
inner_commit() - BasicTxTest 추가
isNewTransaction=true)이 된다.정리하면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것이다.
isNewTransaction=false)이 예제에서는 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶인다고 설명했다. 그런데 코드를 잘 보면 커밋을 두 번 호출했다.
트랜잭션을 생각해보면 하나의 커넥션에 커밋은 한번만 호출할 수 있다. 커밋이나 롤백을 하면 해당 트랜잭션은 끝나버린다.
실행 결과 - inner_commit()
Participating in existing transaction이라는 메시지를 확인할 수 있다.이 메시지는 내부 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 뜻이다.
실행 결과를 보면 외부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통한 물리 트랜잭션을 시작(
manualcommit)하고,DB 커넥션을 통해 커밋 하는 것을 확인할 수 있다. 그런데 내부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통해 커밋하는 로그를 확인할 수 없다.
정리하면 외부 트랜잭션만 물리 트랜잭션을 시작하고, 커밋한다.
만약 내부 트랜잭션이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리기 때문에, 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없다.
따라서 내부 트랜잭션은 DB 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다. 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우
처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다. 이를 통해 트랜잭션 중복 커밋 문제를 해결한다.
요청 흐름 - 외부 트랜잭션
txManager.getTransaction()를 호출해서 외부 트랜잭션을 시작한다.setAutoCommit(false))로 설정한다. - 물리 트랜잭션 시작TransactionStatus에 담아서 반환하는데, 여기에 신규트랜잭션의 여부가 담겨 있다.isNewTransaction를 통해 신규 트랜잭션 여부를 확인할 수 있다. 트랜잭션을 처음 시작했으므로 신규 트랜잭션이다.(true)요청 흐름 - 내부 트랜잭션
txManager.getTransaction()를 호출해서 내부 트랜잭션을 시작한다.TransactionStatus에 담아서 반환하는데여기에서
isNewTransaction를 통해 신규 트랜잭션 여부를 확인할 수 있다.여기서는 기존 트랜잭션에 참여했기 때문에 신규 트랜잭션이 아니다. (
false)응답 흐름 - 내부 트랜잭션
이 부분이 중요한데, 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다.
아직 트랜잭션이 끝난 것이 아니기 때문에 실제 커밋을 호출하면 안된다. 물리 트랜잭션은 외부 트랜잭션을 종료할 때 까지 이어져야한다.
응답 흐름 - 외부 트랜잭션
실제 데이터베이스에 커밋이 반영되고, 물리 트랜잭션도 끝난다.
핵심 정리
그래서 이 경우 논리 트랜잭션과 물리 트랜잭션을 나누게 된다. 또는 외부 트랜잭션과 내부 트랜잭션으로 나누어 설명하기도 한다.
모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.
스프링 트랜잭션 전파5 - 외부 롤백
이번에는 내부 트랜잭션은 커밋되는데, 외부 트랜잭션이 롤백되는 상황을 알아보자.

논리 트랜잭션이 하나라도 롤백되면 전체 물리 트랜잭션은 롤백된다.
따라서 이 경우 내부 트랜잭션이 커밋했어도, 내부 트랜잭션 안에서 저장한 데이터도 모두 함께 롤백된다.
outer_rollback() - BasicTxTest 추가
실행 결과 - outer_rollback()
외부 트랜잭션 롤백
Initiating transaction rollback
Rolling back JDBC transaction on Connection [HikariProxyConnection@461376017
wrapping conn0]
Releasing JDBC Connection [HikariProxyConnection@461376017 wrapping conn0] after
transaction
UnexpectedRollbackException.class이 발생하는 것을 확인할 수 있다.실행 결과 - inner_rollback()
Participating in existing transactionParticipating transaction failed - marking existing transaction as rollback-onlyGlobal transaction is marked as rollback-only응답 흐름

응답 흐름 - 내부 트랜잭션
이 부분이 중요한데, 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다.
아직 트랜잭션이 끝난 것이 아니기 때문에 실제 롤백을 호출하면 안된다. 물리 트랜잭션은 외부 트랜잭션을 종료할 때 까지 이어져야한다.
rollbackOnly=true라는 표시를 해둔다.응답 흐름 - 외부 트랜잭션
따라서 DB 커넥션에 실제 커밋을 호출해야 한다. 이때 먼저 트랜잭션 동기화 매니저에 롤백 전용 (
rollbackOnly=true) 표시가 있는지 확인한다.롤백 전용 표시가 있으면 물리 트랜잭션을 커밋하는 것이 아니라 롤백한다.
UnexpectedRollbackException런타임 예외를 던진다. 그래서 커밋을 시도했지만 롤백이 발생했다는 것을 명확하게 알려준다.정리
UnexpectedRollbackException예외를 던진다.참고
스프링 트랜잭션 전파7 - REQUIRES_NEW
이번에는 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 사용하는 방법에 대해서 알아보자.
외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법이다. 그래서 커밋과 롤백도 각각 별도로 이루어지게 된다.
이 방법은 내부 트랜잭션에 문제가 발생해서 롤백해도, 외부 트랜잭션에는 영향을 주지 않는다.
반대로 외부 트랜잭션에 문제가 발생해도 내부 트랜잭션에 영향을 주지 않는다.
REQUIRES_NEW

REQUIRES_NEW옵션을 사용하면 된다.inner_rollback_requires_new() - BasicTxTest 추가
propagationBehavior에PROPAGATION_REQUIRES_NEW옵션을 주었다.실행 결과 - inner_rollback_requires_new()
외부 트랜잭션 시작
conn0를 획득하고manual commit으로 변경해서 물리 트랜잭션을 시작한다.outer.isNewTransaction()=true)내부 트랜잭션 시작
conn1를 획득하고manual commit으로 변경해서 물리 트랜잭션을 시작한다.PROPAGATION_REQUIRES_NEW옵션을 사용했기 때문에 완전히 새로운 신규 트랜잭션으로 생성된다.(inner.isNewTransaction()=true)내부 트랜잭션 롤백
conn1을 사용하므로conn1에 물리 롤백을 수행한다.외부 트랜잭션 커밋
conn0를 사용하므로conn0에 물리 커밋을 수행한다.요청 흐름 - REQUIRES_NEW

요청 흐름 - 외부 트랜잭션
txManager.getTransaction()를 호출해서 외부 트랜잭션을 시작한다.setAutoCommit(false))로 설정한다. - 물리 트랜잭션 시작TransactionStatus에 담아서 반환하는데, 여기에 신규트랜잭션의 여부가 담겨 있다.isNewTransaction를 통해 신규 트랜잭션 여부를 확인할 수 있다. 트랜잭션을 처음 시작했으므로 신규 트랜잭션이다.(true)요청 흐름 - 내부 트랜잭션
txManager.getTransaction()를 호출해서 내부 트랜잭션을 시작한다.REQUIRES_NEW옵션을 확인하고, 기존 트랜잭션에 참여하는 것이 아니라 새로운 트랜잭션을 시작한다.setAutoCommit(false))로 설정한다. - 물리 트랜잭션 시작con1은 잠시 보류되고, 지금부터는con2가 사용된다. (내부 트랜잭션을 완료할 때 까지con2가 사용된다.)isNewTransaction == truecon2커넥션을 획득해서 사용한다.응답 흐름 - REQUIRES_NEW

응답 흐름 - 내부 트랜잭션
con2물리 트랜잭션을 롤백한다.con2는 종료되거나, 커넥션 풀에 반납된다.con1의 보류가 끝나고, 다시con1을 사용한다.rollbackOnly설정을 체크한다.rollbackOnly설정이 없으므로 커밋한다.con1커넥션을 통해 물리 트랜잭션을 커밋한다.con1은 종료되거나, 커넥션 풀에 반납된다.정리
REQUIRES_NEW옵션을 사용하면 물리 트랜잭션이 명확하게 분리된다.REQUIRES_NEW를 사용하면 데이터베이스 커넥션이 동시에 2개 사용된다는 점을 주의해야 한다.스프링 트랜잭션 전파8 - 다양한 전파 옵션
스프링은 다양한 트랜잭션 전파 옵션을 제공한다. 전파 옵션에 별도의 설정을 하지 않으면
REQUIRED가 기본으로 사용된다.참고로 실무에서는 대부분
REQUIRED옵션을 사용한다. 그리고 아주 가끔REQUIRES_NEW을 사용하고, 나머지는 거의 사용하지 않는다.REQUIRED
가장 많이 사용하는 기본 설정이다. 기존 트랜잭션이 없으면 생성하고, 있으면 참여한다.
트랜잭션이 필수라는 의미로 이해하면 된다. (필수이기 때문에 없으면 만들고, 있으면 참여한다.)
REQUIRES_NEW
항상 새로운 트랜잭션을 생성한다.
SUPPORT
트랜잭션을 지원한다는 뜻이다. 기존 트랜잭션이 없으면, 없는대로 진행하고, 있으면 참여한다.
NOT_SUPPORT
트랜잭션을 지원하지 않는다는 의미이다.
MANDATORY
의무사항이다. 트랜잭션이 반드시 있어야 한다. 기존 트랜잭션이 없으면 예외가 발생한다.
IllegalTransactionStateException예외 발생NEVER
트랜잭션을 사용하지 않는다는 의미이다. 기존 트랜잭션이 있으면 예외가 발생한다. 기존 트랜잭션도 허용하지 않는 강한 부정의 의미로 이해하면 된다.
IllegalTransactionStateException예외 발생NESTED
기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
기존 트랜잭션 있음: 중첩 트랜잭션을 만든다.
참고
트랜잭션 전파와 옵션
isolation,timeout,readOnly는 트랜잭션이 처음 시작될 때만 적용된다. 트랜잭션에 참여하는 경우에는 적용되지 않는다.예시)
REQUIRED를 통한 트랜잭션 시작,REQUIRES_NEW를 통한 트랜잭션 시작 시점에만 적용된다.트랜잭션 전파 활용1 - 예제 프로젝트 시작
비즈니스 요구사항
Member
MemberRepository
Log
LogRepository
로그예외라고 입력하는 경우 예외를 발생시킨다.MemberService
joinV1()joinV2()joinV1()과 같은 기능을 수행한다.MemberServiceTest
정상 동작하는지 테스트 코드를 만들어서 수행해보자.
우선 테스트가 정상 실행되는 것만 확인하자.
참고
username은 테스트별로 각각 다르게 설정해야 한다.그렇지 않으면 다음 테스트에 영향을 준다. (모든 테스트가 완료되어야 DB가 사라진다.)
JPA와 데이터 변경
트랜잭션 전파 활용2 - 커밋, 롤백
서비스 계층에 트랜잭션이 없을 때 - 커밋
예제를 통해 서비스 계층에 트랜잭션이 없을 때 트랜잭션이 각각 어떻게 작동하는지 확인해보자.
상황
outerTxOff_success
MemberService에서MemberRepository를 호출한다.MemberRepository에는@Transactional애노테이션이 있으므로트랜잭션 AOP가 작동한다. 여기서 트랜잭션 매니저를 통해 트랜잭션을 시작한다. 이렇게 시작한 트랜잭션을 트랜잭션B라 하자.
- 그림에서는 생략했지만, 트랜잭션 매니저에 트랜잭션을 요청하면 데이터소스를 통해 커넥션
con1을 획득하고해당 커넥션을 수동 커밋 모드로 변경해서 트랜잭션을 시작한다.
- 그리고 트랜잭션 동기화 매니저를 통해 트랜잭션을 시작한 커넥션을 보관한다.
- 트랜잭션 매니저의 호출 결과로
status를 반환한다. 여기서는 신규 트랜잭션 여부가 참이 된다.MemberRepository는 JPA를 통해 회원을 저장하는데, 이때 JPA는 트랜잭션이 시작된con1을 사용해서 회원을 저장한다.MemberRepository가 정상 응답을 반환했기 때문에 트랜잭션 AOP는 트랜잭션 매니저에 커밋을 요청한다.con1을 통해 물리 트랜잭션을 커밋한다.rollbackOnly여부를 모두 체크한다.이렇게 해서
MemberRepository와 관련된 모든 데이터는 정상 커밋되고, 트랜잭션B는 완전히 종료된다.이후에
LogRepository를 통해 트랜잭션C를 시작하고, 정상 커밋한다. 결과적으로 둘다 커밋되었으므로Member,Log모두 안전하게 저장된다.@transactional과 REQUIRED
REQUIRED이다. 따라서 다음 둘은 같다.@Transactional(propagation = Propagation.REQUIRED)@TransactionalREQUIRED는 기존 트랜잭션이 없으면 새로운 트랜잭션을 만들고, 기존 트랜잭션이 있으면 참여한다.서비스 계층에 트랜잭션이 없을 때 - 롤백
상황
outerTxOff_fail
로그예외라는 단어가 포함되어 있으면LogRepository에서 런타임 예외가 발생한다.로그예외 로직
에서MemberRepository` 를 호출하는 부분은 앞서 설명한 내용과 같다. 트랜잭션이 정상 커밋되고회원 데이터도 DB에 정상 반영된다.
MemberService에서LogRepository를 호출하는데,로그예외라는 이름을 전달한다. 이 과정에서 새로운 트랜잭션 C가 만들어진다.LogRepository 응답 로직
LogRepository는 트랜잭션C와 관련된con2를 사용한다.로그예외라는 이름을 전달해서LogRepository에 런타임 예외가 발생한다.LogRepository는 해당 예외를 밖으로 던진다. 이 경우 트랜잭션 AOP가 예외를 받게된다.참고
트랜잭션 AOP도 결국 내부에서는 트랜잭션 매니저를 사용하게 된다.
이 경우 회원은 저장되지만, 회원 이력 로그는 롤백된다. 따라서 데이터 정합성에 문제가 발생할 수 있다. 둘을 하나의 트랜잭션으로 묶어서 처리해보자.
트랜잭션 전파 활용3 - 단일 트랜잭션
###트랜잭션 하나만 사용하기
회원 리포지토리와 로그 리포지토리를 하나의 트랜잭션으로 묶는 가장 간단한 방법은 이 둘을 호출하는 회원 서비스에만 트랜잭션을 사용하는 것이다.
singleTx
MemberService - joinV1()
MemberRepository - save()
LogRepository - save()
MemberRepository,LogRepository의@Transactional코드를 제거하자.MemberService에만@Transactional코드를 추가하자.MemberService를 시작할 때 부터 종료할 때 까지의 모든 로직을 하나의 트랜잭션으로 묶을 수 있다.MemberService가MemberRepository,LogRepository를 호출하므로 이 로직들은 같은 트랜잭션을 사용한다.MemberService만 트랜잭션을 처리하기 때문에 앞서 배운 논리 트랜잭션, 물리 트랜잭션, 외부 트랜잭션, 내부트랜잭션,rollbackOnly, 신규 트랜잭션, 트랜잭션 전파와 같은 복잡한 것을 고민할 필요가 없다. 아주 단순하고 깔끔하게 트랜잭션을 묶을 수 있다.@Transactional이MemberService에만 붙어있기 때문에 여기에만 트랜잭션 AOP가 적용된다.MemberRepository,LogRepository는 트랜잭션 AOP가 적용되지 않는다.MemberService의 시작부터 끝까지, 관련 로직은 해당 트랜잭션이 생성한 커넥션을 사용하게 된다.MemberService가 호출하는MemberRepository,LogRepository자연스럽게 트랜잭션 범위에 포함된다.참고
같은 쓰레드를 사용하면 트랜잭션 동기화 매니저는 같은 커넥션을 반환한다.
각각 트랜잭션이 필요한 상황
하지만 다음과 같이 각각 트랜잭션이 필요하면 어떻게 해야할까?

트랜잭션 적용 범위

MemberService부터MemberRepository,LogRepository를 모두 하나의 트랜잭션으로 묶고 싶다.MemberRepository만 호출하고 여기에만 트랜잭션을 사용하고 싶다.LogRepository만 호출하고 여기에만 트랜잭션을 사용하고 싶다.MemberService에 트랜잭션 코드를 남기고MemberRepository,LogRepository의 트랜잭션 코드를 제거하면 앞서 배운 것 처럼 깔끔하게 하나의 트랜잭션을 적용할 수 있다.MemberRepository,LogRepository에는 트랜잭션을 적용할 수 없다.트랜잭션 전파 없이 이런 문제를 해결하려면 아마도 트랜잭션이 있는 메서드와 트랜잭션이 없는 메서드를 각각 만들어야 할 것이다.

클라이언트 Z가 호출하는
OrderService에서도 트랜잭션을 시작할 수 있어야 하고클라이언트 A가 호출하는
MemberService에서도 트랜잭션을 시작할 수 있어야 한다.이런 문제를 해결하기 위해 트랜잭션 전파가 필요한 것이다.
트랜잭션 전파 활용4 - 전파 커밋
스프링은

@Transactional이 적용되어 있으면 기본으로REQUIRED라는 전파 옵션을 사용한다.이 옵션은 기존 트랜잭션이 없으면 트랜잭션을 생성하고, 기존 트랜잭션이 있으면 기존 트랜잭션에 참여한다.
참여한다는 뜻은 해당 트랜잭션을 그대로 따른다는 뜻이고, 동시에 같은 동기화 커넥션을 사용한다는 뜻이다.
이렇게 둘 이상의 트랜잭션이 하나의 물리 트랜잭션에 묶이게 되면 둘을 구분하기 위해 논리 트랜잭션과 물리 트랜잭션으로 구분한다.
신규 트랜잭션

모든 논리 트랜잭션 커밋

모든 논리 트랜잭션이 정상 커밋되는 경우
outerTxOn_success
클래스 위의 주석을 확인해서 모든 곳에 트랜잭션을 적용하자.
MemberService @Transactional:ONMemberRepository @Transactional:ONLogRepository @Transactional:ONMemberService를 호출하면서 트랜잭션 AOP가 호출된다.MemberRepository를 호출하면서 트랜잭션 AOP가 호출된다.MemberRepository의 로직 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출된다.LogRepository를 호출하면서 트랜잭션 AOP가 호출된다.LogRepository의 로직 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출된다.MemberService의 로직 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출된다.트랜잭션 전파 활용5 - 전파 롤백
이번에는 로그 리포지토리에서 예외가 발생해서 전체 트랜잭션이 롤백되는 경우를 알아보자.

outerTxOn_fail
클래스 위의 주석을 확인해서 모든 곳에 트랜잭션을 적용하자.
MemberService @Transactional:ONMemberRepository @Transactional:ONLogRepository @Transactional:ON여기서는
로그예외라고 넘겼기 때문에LogRepository에서 런타임 예외가 발생한다.흐름

MemberService를 호출하면서 트랜잭션 AOP가 호출된다.MemberRepository를 호출하면서 트랜잭션 AOP가 호출된다.MemberRepository의 로직 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출된다.LogRepository를 호출하면서 트랜잭션 AOP가 호출된다.LogRepository로직에서 런타임 예외가 발생한다. 예외를 던지면 트랜잭션 AOP가 해당 예외를 받게 된다.대신에
rollbackOnly를 설정한다.LogRepository가 예외를 던졌기 때문에 트랜잭션 AOP도 해당 예외를 그대로 밖으로 던진다.MemberService에서도 런타임 예외를 받게 되는데, 여기 로직에서는 해당 런타임 예외를 처리하지 않고 밖으로 던진다.rollbackOnly설정은 참고하지 않는다.MemberService가 예외를 던졌기 때문에 트랜잭션 AOP도 해당 예외를 그대로 밖으로 던진다.LogRepository부터 넘어온 런타임 예외를 받게 된다.정리
회원과 회원 이력 로그를 처리하는 부분을 하나의 트랜잭션으로 묶은 덕분에 문제가 발생했을 때 회원과 회원 이력 로그가 모두 함께 롤백된다.
따라서 데이터 정합성에 문제가 발생하지 않는다.
트랜잭션 전파 활용6 - 복구 REQUIRED
앞서 회원과 로그를 하나의 트랜잭션으로 묶어서 데이터 정합성 문제를 깔끔하게 해결했다.

그런데 회원 이력 로그를 DB에 남기는 작업에 가끔 문제가 발생해서 회원 가입 자체가 안되는 경우가 가끔 발생하게 되었다.
그래서 사용자들이 회원 가입에 실패해서 이탈하는 문제가 발생하기 시작했다.
회원 이력 로그의 경우 여러가지 방법으로 추후에 복구가 가능할 것으로 보인다. 그래서 비즈니스 요구사항이 변경되었다.
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
LogRepository에서 예외가 발생하면 그것을MemberService에서 예외를 잡아서 처리하면 될 것 같다.MemberService에서 정상 흐름으로 바꿀 수 있기 때문에MemberService의 트랜잭션 AOP에서 커밋을 수행할 수 있다.recoverException_fail
모든 트랜잭션을 켜자
MemberService @Transactional:ONMemberRepository @Transactional:ONLogRepository @Transactional:ON여기서 memberService.joinV2()를 호출하는 부분을 주의해야 한다. joinV2()에는 예외를 잡아서 정상 흐름으로 변환하는 로직이 추가되어 있다.
rollbackOnly를 설정하기 때문에 결과적으로 정상 흐름 처리를 해서 외부 트랜잭션에서 커밋을 호출해도 물리 트랜잭션은 롤백된다.UnexpectedRollbackException이 던져진다.LogRepository에서 예외가 발생한다. 예외를 던지면LogRepository의 트랜잭션 AOP가 해당 예외를 받는다.rollbackOnly를 표시한다.MemberService에 던져지고,MemberService는 해당 예외를 복구한다. 그리고 정상적으로 리턴한다.MemberService의 트랜잭션 AOP는 커밋을 호출한다.rollbackOnly를 체크한다.rollbackOnly가 체크 되어 있으므로 물리 트랜잭션을 롤백한다.UnexpectedRollbackException예외를 던진다.UnexpectedRollbackException을 클라이언트에 던진다.정리
UnexpectedRollbackException예외가 발생한다.rollbackOnly상황에서 커밋이 발생하면UnexpectedRollbackException예외가 발생한다.트랜잭션 전파 활용7 - 복구 REQUIRES_NEW
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
이 요구사항을 만족하기 위해서 로그와 관련된 물리 트랜잭션을 별도로 분리해보자. 바로
REQUIRES_NEW를 사용하는 것이다.recoverException_success
MemberService @Transactional:ONMemberRepository @Transactional:ONLogRepository @Transactional(REQUIRES_NEW)LogRepository - save()
이렇게 해서 기존 트랜잭션에 참여하는
REQUIRED대신에, 항상 신규 트랜잭션을 생성하는REQUIRES_NEW를 적용하자.예외를 복구하는 memberService.joinV2()를 사용한다는 점도 주의하자
REQUIRES_NEW - 물리 트랜잭션 분리

MemberRepository는REQUIRED옵션을 사용한다. 따라서 기존 트랜잭션에 참여한다.LogRepository의 트랜잭션 옵션에REQUIRES_NEW를 사용했다.REQUIRES_NEW는 항상 새로운 트랜잭션을 만든다. 따라서 해당 트랜잭션 안에서는 DB 커넥션도 별도로 사용하게 된다.REQUIRES_NEW - 복구

REQUIRES_NEW를 사용하게 되면 물리 트랜잭션 자체가 완전히 분리되어 버린다.REQUIRES_NEW는 신규 트랜잭션이므로rollbackOnly표시가 되지 않는다. 그냥 해당 트랜잭션이 물리 롤백되고 끝난다.REQUIRES_NEW - 자세히

LogRepository에서 예외가 발생한다. 예외를 던지면LogRepository의 트랜잭션 AOP가 해당 예외를 받는다.REQUIRES_NEW를 사용한 신규 트랜잭션이므로 물리 트랜잭션을 롤백한다. 물리 트랜잭션을 롤백했으므로rollbackOnly를 표시하지 않는다.여기서
REQUIRES_NEW를 사용한 물리 트랜잭션은 롤백되고 완전히 끝이 나버린다.MemberService에 던져지고,MemberService는 해당 예외를 복구한다. 그리고 정상적으로 리턴한다.MemberService의 트랜잭션 AOP는 커밋을 호출한다.rollbackOnly를 체크한다.rollbackOnly가 없으므로 물리 트랜잭션을 커밋한다.결과적으로 회원 데이터는 저장되고, 로그 데이터만 롤백 되는 것을 확인할 수 있다.
정리
REQUIRES_NEW를 사용해서 트랜잭션을 분리해야 한다.MemberService가MemberRepository,LogRepository만 호출하지만실제로는 더 많은 리포지토리들을 호출하고 그 중에
LogRepository만 트랜잭션을 분리한다고 생각해보면 이해하는데 도움이 될 것이다.주의
REQUIRES_NEW를 사용하면 하나의 HTTP 요청에 동시에 2개의 데이터베이스 커넥션을 사용하게 된다.따라서 성능이 중요한 곳에서는 이런 부분을 주의해서 사용해야 한다.
REQUIRES_NEW를 사용하지 않고 문제를 해결할 수 있는 단순한 방법이 있다면, 그 방법을 선택하는 것이 더 좋다.예를 들면 다음과 같이

REQUIRES_NEW를 사용하지 않고 구조를 변경하는 것이다.이렇게 하면 HTTP 요청에 동시에 2개의 커넥션을 사용하지는 않는다. 순차적으로 사용하고 반환하게 된다.
물론 구조상
REQUIRES_NEW를 사용하는 것이 더 깔끔한 경우도 있으므로 각각의 장단점을 이해하고 적절하게 선택해서 사용하면 된다.All reactions