You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@Slf4j@Repository@RequiredArgsConstructorpublicclassMemberRepository {
privatefinalEntityManagerem;
@Transactionalpublicvoidsave(Membermember) {
log.info("member 저장");
em.persist(member);
}
publicOptional<Member> find(Stringusername) {
returnem.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList().stream().findAny(); // getSingleResult() 도 있는데 조회 값이 없으면 예외가 터짐
}
}
여기서는 각각의 테스트가 완료된 시점에 데이터를 삭제하지 않는다. 따라서 username 은 테스트별로 각각 다르게 설정해야 한다. 그렇지 않으면 다음 테스트에 영향을 준다. (모든 테스트가 완료되어야 DB가 사라진다.)
트랜잭션 전파 활용2- 커밋, 롤백
서비스 계층에 트랜잭션이 없을 때 - 커밋
상황
서비스 계층에 트랜잭션이 없다.
회원, 로그 리포지토리가 각각 트랜잭션을 가지고 있다.
회원, 로그 리포지토리 둘다 커밋에 성공한다.
MemberService 에서 MemberRepository의 save() 호출, 리포지토리엔 @Transactioal이 있으므로 트랜잭션 AOP 가 작동함. 즉 트랜잭션매니저를 통해 트랜잭션 시작함. 이를 트랜잭션 B 라고 하자.
트랜잭션 B 는 커넥션을 획득하고 이를 수동 커밋으로 변경해 트랜잭션 시작, 트랜잭션 동기화 매니저에 보관, 이후 호출 결과로 status 반환. 신규 트랜잭션은 True 임.
MemberRepository는 con1 을 사용해 회원을 저장함 (entityManager.persist() 를 하면 내부에서 해당 커넥션을 쓰게된다)
정상 상황이기 때문에 트랜잭션 매니저에 커밋을 요청하고, 신규 트랜잭션이기 때문에
트랜잭션 매니저는 물리 트랜잭션을 커밋 처리함.
이떄 rollbackOnly 여부 체크한다.
이렇게 해서 MemberRepository 와 관련된 모든 데이터는 정상 커밋되고, 트랜잭션B는 완전히 종료된다.
이후에 LogRepository 를 통해 트랜잭션C를 시작하고, 정상 커밋한다.
결과적으로 둘다 커밋되었으므로 Member , Log 모두 안전하게 저장된다.
@transactional과 REQUIRED
트랜잭션 전파의 기본 값은 REQUIRED 이다. 따라서 다음 둘은 같다. @Transactional(propagation = Propagation.REQUIRED) @Transactional REQUIRED 는 기존 트랜잭션이 없으면 새로운 트랜잭션을 만들고, 기존 트랜잭션이 있으면 참여한다.
서비스 계층에 트랜잭션이 없을 때 - 롤백
상황
서비스 계층에 트랜잭션이 없다.
회원, 로그 리포지토리가 각각 트랜잭션을 가지고 있다.
회원 리포지토리는 정상 동작하지만 로그 리포지토리에서 예외가 발생한다.
/** * memberService @Transactional : OFF * memberRepository @Transactional : ON * logRepository @Transactional : ON Exception */@TestvoidouterTxOff_fail() {
//givenStringusername = "로그예외_outerTxOff_fail"; // 이 로직때문에 예외 발생 if (logMessage.getMessage().contains("로그예외")) {//whenassertThatThrownBy(() -> memberService.joinV1(username))
.isInstanceOf(RuntimeException.class);
//thenassertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isEmpty());
}
}
MemberService 에서 MemberRepository 를 호출하는 부분은 앞서 설명한 내용과 같다. 트랜잭션이 정상 커밋되고, 회원 데이터도 DB에 정상 반영된다.
MemberService 에서 LogRepository 를 호출하는데, 로그예외 라는 이름을 전달한다. 이 과정에서 새로운 트랜잭션 C가 만들어진다.
LogRepository 응답 로직
LogRepository 는 트랜잭션C와 관련된 con2 를 사용한다.
로그예외 라는 이름을 전달해서 LogRepository 에 런타임 예외가 발생한다.
LogRepository 는 해당 예외를 밖으로 던진다. 이 경우 트랜잭션 AOP가 예외를 받게된다.
런타임 예외가 발생해서 트랜잭션 AOP는 트랜잭션 매니저에 롤백을 호출한다.
트랜잭션 매니저는 신규 트랜잭션이므로 물리 롤백을 호출한다.
참고
트랜잭션 AOP도 결국 내부에서는 트랜잭션 매니저를 사용하게 된다.
이 경우 회원은 저장되지만, 회원 이력 로그는 롤백된다. 따라서 데이터 정합성에 문제가 발생할 수 있다.
요구사항 : 회원과 로그는 무조건 커밋, 롤백이 일치해야한다!
둘을 하나의 트랜잭션으로 묶어서 처리해보자.
트랜잭션 전파 활용3- 단일 트랜잭션
트랜잭션 하나만 사용하기
회원 리포지토리와 로그 리포지토리를 하나의 트랜잭션으로 묶는 가장 간단한 방법은 이 둘을 호출하는 회원 서비스에만 트랜잭션을 사용하는 것이다.
/** * memberService @Transactional : ON * memberRepository @Transactional : OFF * logRepository @Transactional : OFF */@TestvoidsingleTX() {
//givenStringusername = "outerTxOff_success";
//whenmemberService.joinV1(username);
//thenassertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isPresent());
}
MemberService의 joinV1() 에 @transactional 추가하고,
MemberRepository, LogRepository 에 @transactional 에 주석넣어 작동하지 않도록 하기.
이렇게 하면 트랜잭션은 하나만 생기고, 마지막 트랜잭션 커밋 이후 물리 트랜잭션이 커밋된다.
이렇게 하면 MemberService 를 시작할 때 부터 종료할 때 까지의 모든 로직을 하나의 트랜잭션으로 묶을 수 있다.
물론 MemberService 가 MemberRepository , LogRepository 를 호출하므로 이 로직들은 같은 트랜잭션을 사용한다.
이 상황에선 내부, 외부 트랜잭션 없이 MemberService 만 트랜잭션을 처리하기 때문에 트랜잭션 전파 상황이 아니다. 아주 단순하고 깔끔하게 트랜잭션을 묶을 수 있다.
@Transactional 이 MemberService 에만 붙어있기 때문에 이 클래스만 AOP 가 적용된다.
MemberRepository , LogRepository 는 트랜잭션 AOP가 적용되지 않는다.
MemberService 의 시작부터 끝까지, 관련 로직은 해당 트랜잭션이 생성한 커넥션을 사용하게 된다.
MemberService 가 호출하는 MemberRepository , LogRepository 자연스럽게 트랜잭션 범위에 포함된다.
참고
같은 쓰레드를 사용하면 트랜잭션 동기화 매니저는 같은 커넥션을 반환한다.
로그를 확인해보면
처음 트랜잭션 시작 후 마지막 로직인 logRepository 호출 종료 후 커밋되는것 확인 가능
인서트쿼리가 뒤에 호출되는것은 JPA의 특징.
플러시 라고 부르는데, JPA 는 JPA 인서트쿼리나 업데이트쿼리를 디비에 날리고 그다음에 디비트랜잭션이 커밋된다.
서비스에만 트랜잭션걸고 나머지 다 빼면 깔끔하게 묶임.
근데 만약에 각각 트랜잭션이 필요한 경우는 어떻게 해야할까?
예를들어
클라이언트 A 는 서비스를 호출하지만
클라이언트 B 는 MemberRepository 에서 직접 호출하고
클라이언트 C 는 LogRepository를 바로 호출하고 다 트랜잭션이 필요한 경우
클라이언트 A는 MemberService 부터 MemberRepository , LogRepository 를 모두 하나의 트랜잭션으로 묶고 싶다.
클라이언트 B는 MemberRepository 만 호출하고 여기에만 트랜잭션을 사용하고 싶다.
클라이언트 C는 LogRepository 만 호출하고 여기에만 트랜잭션을 사용하고 싶다.
클라이언트 A 만 생각하면 위에한 것 처럼 MemberService에만 트랜잭션 코드 있고 나머진 삭제하면 되는데,
하지만 이렇게 되면 클라이언트 B, C가 호출하는 MemberRepository , LogRepository 에는 트랜잭션을 적용할 수 없다.
이런 문제를 해결하기 위해 트랜잭션전파가 있음.
만약 트랜잭션 전파가 없었다면, 트랜잭션 있는 메서드 없는 메서드 각각 만들어야했을 것임.
이러런경우도 있을 수 있음.
계층으로 많이 엮어있는데 트랜잭션을 사용해야하는 경우
트랜잭션 전파 활용4- 전파 커밋
스프링은 @Transactional이 적용되어있으면 기본으로 REQUIRED 전파옵션을 사용한다.
이 옵션은 기존 트랜잭션이 없으면 새로 생성하고, 있으면 참여하는 옵션.
참여한다 = 해당 트랜잭션을 그대로 따르며 같은 동기화 커넥션을 사용한다는 뜻.
이렇게 둘 이상이 트랜잭션이 하나의 물리 트랜잭션에 묶이면
논리트랜잭션 / 물리트랜잭션 으로 구분할 수 있다.
서비스, 리포지토리 전부에 @Transactional 이 있는경우
이 경우 외부에 있는 신규 트랜잭션만 실제 물리 트랜잭션을 시작하고 커밋한다.
내부에 있는 트랜잭션은 물리 트랜잭션 시작하거나 커밋하지 않는다.
모든 트랜잭션이 커밋되어야 물리 트랜잭션도 커밋된다.
@TestvoidouterTXOn_success() {
//givenStringusername = "outerTXOn_success"; // 이 테스트는 @Transactional 을 걸 수 없어 테스트 롤백 불가능.//whenmemberService.joinV1(username);
//thenassertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isPresent());
}
클라이언트A (테스트코드) 가 MemberService 호출해 트랜잭션 AOP 호출
신규 트랜잭션 생성, 물리 트랜잭션 시작
MemberService 가 MemberRepository 호출하면서 트랜잭션 AOP 호출
이미 트랜잭션이 있어 기존 트랜잭션에 참여
MemberRepository 호출이 정상적이므로 AOP 에서 트랜잭션 매니저에 커밋 요청.
신규 트랜잭션이 아니므로 물리 트랜잭션에 커밋하지 않음.
MemberService 가 LogRepository 호출하면서 트랜잭션 AOP 호출
이미 트랜잭션이 있어 기존 트랜잭션에 참여
LogRepository 호출이 정상적이므로 AOP 에서 트랜잭션 매니저에 커밋 요청.
신규 트랜잭션이 아니므로 물리 트랜잭션에 커밋하지 않음.
MemberService 로직 호출이 끝나고 정상적이므로 AOP 에서 트랜잭션 매니저에 커밋 요청.
신규 트랜잭션이므로 물리 트랜잭션에 커밋해 실제 DB 에 반영.
트랜잭션 전파 활용5- 전파 롤백
이번엔 로그리포지토리에서 예외가 발생해 전체 트랜잭션이 롤백되는 경우를 알아보자.
MemberRepository 는 커밋했는데, LogRepository 에서 롤백 발생.
이러면 전체적으로 롤백 되어야함.
그런데 LogRepository 에서 던진 익셉션을 MemberRepository 에서 잡지 못하므로 여기서도 롤백이 발생함.
/** * memberService @Transactional : ON * memberRepository @Transactional : ON * logRepository @Transactional : ON Exception */@TestvoidouterTxOn_fail() {
//givenStringusername = "로그예외_outerTxOn_fail"; // 이 로직때문에 예외 발생 if (logMessage.getMessage().contains("로그예외")) {//whenassertThatThrownBy(() -> memberService.joinV1(username))
.isInstanceOf(RuntimeException.class);
//then 모든 데이터 롤백assertTrue(memberRepository.find(username).isEmpty());
assertTrue(logRepository.find(username).isEmpty());
}
로그예외로 넘겼기 때문에 LogRepository 에서 런타임 예외 발생.
처음 MemberService AOP 호출하면서 신규 트랜잭션 생성 및 물리 트랜잭션 시작
MemberRepository 호출하면서 트랜잭션에 참여하고 커밋요청 하지만 실제 커밋은 하지 않음
LogRepository 호출하면서 트랜잭션 참여, 런타임 예외 발생해 트랜잭션 매니저에 롤백 요청, 실제 롤백을 하진 않음. 트랜잭션에 rollbackOnly=true 를 설정함 + 예외를 밖으로 던짐
MemberService 에까지 런타임 예외가 오고, 마찬가지로 트랜잭션 매니저에 롤백 요청, 신규 트랜잭션 이므로 실제 롤백을 하게됨.
어짜피 롤백이기 때문에 rollbackOnly 설정 참고하지 않음.
결국 클라이언트A 는 LogRepository 부터 날라온 런타임 익셉션을 받게됨.
정리
회원과 회원 이력 로그를 처리하는 부분을 하나의 트랜잭션으로 묶은 덕분에 문제가 발생했을 때 회원과 회원 이력 로그가 모두 함께 롤백된다. 따라서 데이터 정합성에 문제가 발생하지 않는다.
여기까진 쉬움.
그런데 MemberService 에서 예외를 잡아버리면 어떻게될까?
트랜잭션 전파 활용6- 복구 REQUIRED
앞서 회원고 로그를 하나의 트랜잭션으로 묶어 데이터 정합성 문제를 해결했음.
그런데 로그를 DB 에 남기는 작업에 문제가 있다고 해서 회원가입에 실패해 사용자 이탈 발생
로그는 복구할 수 있으므로
비즈니스 요구사항이 회원 가입 시도 로그 남기는데 실패해도 회원가입은 유지되어야 한다.
회원가입 성공, 로그는 실패해도 회원가입 커밋은 진행되도록!
단순하게 생각하면, LogRepository 에서 발생한 런타임 에러를 MemberService 에서 잡아 처리하면 될 것 같음.
이렇게하면 MemberService 에서 정상이기 때문에 해당 트랜잭션에서 커밋을 수행할 수 있게된다.
그런데 지금까지 학습한 내용과 다르다! rollbackOnly 때문에.
이 방법이 왜 실패하는지 예제를 통해서 알아보자. 참고로 실무에서 많은 개발자가 이 방법을 사용해서 실패한다.
/** * memberService @Transactional : ON * memberRepository @Transactional : ON * logRepository @Transactional : ON Exception */@TestvoidrecoverException_fail() {
//givenStringusername = "로그예외_recoverException_fail"; // 이 로직때문에 예외 발생 if (logMessage.getMessage().contains("로그예외")) {//whenassertThatThrownBy(() -> memberService.joinV2(username)) // v2 는 예외를 잡아서 정상 흐름으로 변환
.isInstanceOf(UnexpectedRollbackException.class);
//then 모든 데이터 롤백assertTrue(memberRepository.find(username).isEmpty());
assertTrue(logRepository.find(username).isEmpty());
}
내부 트랜잭션에서 rollbackOnly 설정 하기 때문에 외부 트랜잭션에서 커밋해도 전체 물리 트랜잭션은 롤백된다.
그리고 UnexpectedRollbackException 이 던저진다.
전체 흐름을 알아보자
LogRepository에서 예외 발생. 예외를 던지면 LogRepository 의 트랜잭션 AOP 가 예외를 받음
신규 트랜잭션이 아니므로 트랜잭션 동기화 매니저에 rollbackOnly 표시함.
이후 예외를 밖으로 던짐
MemberService 가 예외를 받아 복구하고 정상 리턴하며 AOP 가 커밋 호출
그러나 rollbackOnly를 체크하고 true기 때문에 물리 트랜잭션은 롤백된다
트랜잭션 매니저는 UnexpectedRollbackException 예외 던짐.
AOP 가 이를 받고 클라이언트까지 던진다.
정리
논리 트랜잭션 중 하나라도 롤백되면 전체 트랜잭션은 롤백된다.
내부 트랜잭션이 롤백 되었는데, 외부 트랜잭션이 커밋되면 UnexpectedRollbackException 예외가 발생한다.
rollbackOnly 상황에서 커밋이 발생하면 UnexpectedRollbackException 예외가 발생한다.
그렇다면 어떻게 해야 다음 요구사항을 만족할 수 있을까? 회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
여러 옵션이 있지만 REQUIRES_NEW 옵션으로 해결해보자.
트랜잭션 전파 활용7- 복구 REQUIRES_NEW
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
이 요구사항을 만족하기 위해서 로그와 관련된 물리 트랜잭션을 별도로 분리해보자. 바로 REQUIRES_NEW 를 사용하는 것이다.
/** * memberService @Transactional : ON * memberRepository @Transactional : ON * logRepository @Transactional : ON (REQUIRES_NEW) Exception */@TestvoidrecoverException_success() {
//givenStringusername = "로그예외_recoverException_success"; // 이 로직때문에 예외 발생 if (logMessage.getMessage().contains("로그예외")) {//whenmemberService.joinV2(username);
//then member 저장, log 롤백assertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isEmpty());
}
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
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 - 예제 프로젝트 시작
지금까지 배운 트랜잭션 전파를 실제 예제로 이해해보자
비즈니스 요구사항
Member 도메인
Member 관리하는 리포지토리
데이터베이스에 로그를 남기기 위한 LOG 엔티티
로그를 위한 리포지토리
MemberService
회원을 등록하면서 동시에 회원 등록에 대한 DB 로그도 함께 남긴다.
joinV1()joinV2()joinV1()과 같은 기능을 수행한다.테스트 성공
현재는 서비스에서 트랜잭션 없이 리포지토리 트랜잭션에서 저장로직 수행함.
상황들을 하나씩 늘려가면서 트랜잭션 전파에 대해 알아보자.
참고
username은 테스트별로 각각 다르게 설정해야 한다. 그렇지 않으면 다음 테스트에 영향을 준다. (모든 테스트가 완료되어야 DB가 사라진다.)트랜잭션 전파 활용2- 커밋, 롤백
서비스 계층에 트랜잭션이 없을 때 - 커밋
상황
status반환. 신규 트랜잭션은 True 임.rollbackOnly여부 체크한다.이렇게 해서
MemberRepository와 관련된 모든 데이터는 정상 커밋되고, 트랜잭션B는 완전히 종료된다.이후에
LogRepository를 통해 트랜잭션C를 시작하고, 정상 커밋한다.결과적으로 둘다 커밋되었으므로
Member,Log모두 안전하게 저장된다.@transactional과 REQUIRED
트랜잭션 전파의 기본 값은
REQUIRED이다. 따라서 다음 둘은 같다.@Transactional(propagation = Propagation.REQUIRED)@TransactionalREQUIRED는 기존 트랜잭션이 없으면 새로운 트랜잭션을 만들고, 기존 트랜잭션이 있으면 참여한다.서비스 계층에 트랜잭션이 없을 때 - 롤백
상황
MemberService에서MemberRepository를 호출하는 부분은 앞서 설명한 내용과 같다. 트랜잭션이 정상 커밋되고, 회원 데이터도 DB에 정상 반영된다.MemberService에서LogRepository를 호출하는데,로그예외라는 이름을 전달한다. 이 과정에서 새로운 트랜잭션 C가 만들어진다.LogRepository 응답 로직
LogRepository는 트랜잭션C와 관련된con2를 사용한다.로그예외라는 이름을 전달해서LogRepository에 런타임 예외가 발생한다.LogRepository는 해당 예외를 밖으로 던진다. 이 경우 트랜잭션 AOP가 예외를 받게된다.참고
트랜잭션 AOP도 결국 내부에서는 트랜잭션 매니저를 사용하게 된다.
이 경우 회원은 저장되지만, 회원 이력 로그는 롤백된다. 따라서 데이터 정합성에 문제가 발생할 수 있다.
요구사항 : 회원과 로그는 무조건 커밋, 롤백이 일치해야한다!
둘을 하나의 트랜잭션으로 묶어서 처리해보자.
트랜잭션 전파 활용3- 단일 트랜잭션
트랜잭션 하나만 사용하기
회원 리포지토리와 로그 리포지토리를 하나의 트랜잭션으로 묶는 가장 간단한 방법은 이 둘을 호출하는 회원 서비스에만 트랜잭션을 사용하는 것이다.
MemberService의 joinV1() 에 @transactional 추가하고,
MemberRepository, LogRepository 에 @transactional 에 주석넣어 작동하지 않도록 하기.
이렇게 하면 트랜잭션은 하나만 생기고, 마지막 트랜잭션 커밋 이후 물리 트랜잭션이 커밋된다.

MemberService를 시작할 때 부터 종료할 때 까지의 모든 로직을 하나의 트랜잭션으로 묶을 수 있다.MemberService가MemberRepository,LogRepository를 호출하므로 이 로직들은 같은 트랜잭션을 사용한다.MemberService만 트랜잭션을 처리하기 때문에 트랜잭션 전파 상황이 아니다. 아주 단순하고 깔끔하게 트랜잭션을 묶을 수 있다.@Transactional이MemberService에만 붙어있기 때문에 이 클래스만 AOP 가 적용된다.MemberRepository,LogRepository는 트랜잭션 AOP가 적용되지 않는다.MemberService의 시작부터 끝까지, 관련 로직은 해당 트랜잭션이 생성한 커넥션을 사용하게 된다.MemberService가 호출하는MemberRepository,LogRepository자연스럽게 트랜잭션 범위에 포함된다.참고
같은 쓰레드를 사용하면 트랜잭션 동기화 매니저는 같은 커넥션을 반환한다.
로그를 확인해보면

처음 트랜잭션 시작 후 마지막 로직인 logRepository 호출 종료 후 커밋되는것 확인 가능
인서트쿼리가 뒤에 호출되는것은 JPA의 특징.
플러시 라고 부르는데, JPA 는 JPA 인서트쿼리나 업데이트쿼리를 디비에 날리고 그다음에 디비트랜잭션이 커밋된다.
서비스에만 트랜잭션걸고 나머지 다 빼면 깔끔하게 묶임.

근데 만약에 각각 트랜잭션이 필요한 경우는 어떻게 해야할까?
예를들어
클라이언트 A 는 서비스를 호출하지만
클라이언트 B 는 MemberRepository 에서 직접 호출하고
클라이언트 C 는 LogRepository를 바로 호출하고 다 트랜잭션이 필요한 경우
클라이언트 A는
MemberService부터MemberRepository,LogRepository를 모두 하나의 트랜잭션으로 묶고 싶다.클라이언트 B는
MemberRepository만 호출하고 여기에만 트랜잭션을 사용하고 싶다.클라이언트 C는
LogRepository만 호출하고 여기에만 트랜잭션을 사용하고 싶다.클라이언트 A 만 생각하면 위에한 것 처럼
MemberService에만 트랜잭션 코드 있고 나머진 삭제하면 되는데,하지만 이렇게 되면 클라이언트 B, C가 호출하는
MemberRepository,LogRepository에는 트랜잭션을 적용할 수 없다.이런 문제를 해결하기 위해 트랜잭션전파가 있음.
만약 트랜잭션 전파가 없었다면, 트랜잭션 있는 메서드 없는 메서드 각각 만들어야했을 것임.
이러런경우도 있을 수 있음.
계층으로 많이 엮어있는데 트랜잭션을 사용해야하는 경우
트랜잭션 전파 활용4- 전파 커밋
스프링은
@Transactional이 적용되어있으면 기본으로REQUIRED전파옵션을 사용한다.이 옵션은 기존 트랜잭션이 없으면 새로 생성하고, 있으면 참여하는 옵션.
참여한다 = 해당 트랜잭션을 그대로 따르며 같은 동기화 커넥션을 사용한다는 뜻.
이렇게 둘 이상이 트랜잭션이 하나의 물리 트랜잭션에 묶이면
논리트랜잭션 / 물리트랜잭션 으로 구분할 수 있다.
서비스, 리포지토리 전부에
@Transactional이 있는경우모든 트랜잭션이 커밋되어야 물리 트랜잭션도 커밋된다.
MemberService호출해 트랜잭션 AOP 호출MemberService가MemberRepository호출하면서 트랜잭션 AOP 호출MemberRepository호출이 정상적이므로 AOP 에서 트랜잭션 매니저에 커밋 요청.MemberService가LogRepository호출하면서 트랜잭션 AOP 호출LogRepository호출이 정상적이므로 AOP 에서 트랜잭션 매니저에 커밋 요청.MemberService로직 호출이 끝나고 정상적이므로 AOP 에서 트랜잭션 매니저에 커밋 요청.트랜잭션 전파 활용5- 전파 롤백
이번엔 로그리포지토리에서 예외가 발생해 전체 트랜잭션이 롤백되는 경우를 알아보자.
MemberRepository는 커밋했는데,LogRepository에서 롤백 발생.이러면 전체적으로 롤백 되어야함.
그런데
LogRepository에서 던진 익셉션을MemberRepository에서 잡지 못하므로 여기서도 롤백이 발생함.로그예외로 넘겼기 때문에
LogRepository에서 런타임 예외 발생.MemberServiceAOP 호출하면서 신규 트랜잭션 생성 및 물리 트랜잭션 시작MemberRepository호출하면서 트랜잭션에 참여하고 커밋요청 하지만 실제 커밋은 하지 않음LogRepository호출하면서 트랜잭션 참여, 런타임 예외 발생해 트랜잭션 매니저에 롤백 요청, 실제 롤백을 하진 않음. 트랜잭션에rollbackOnly=true를 설정함 + 예외를 밖으로 던짐MemberService에까지 런타임 예외가 오고, 마찬가지로 트랜잭션 매니저에 롤백 요청, 신규 트랜잭션 이므로 실제 롤백을 하게됨.rollbackOnly설정 참고하지 않음.LogRepository부터 날라온 런타임 익셉션을 받게됨.정리
회원과 회원 이력 로그를 처리하는 부분을 하나의 트랜잭션으로 묶은 덕분에 문제가 발생했을 때 회원과 회원 이력 로그가 모두 함께 롤백된다. 따라서 데이터 정합성에 문제가 발생하지 않는다.
여기까진 쉬움.
그런데
MemberService에서 예외를 잡아버리면 어떻게될까?트랜잭션 전파 활용6- 복구 REQUIRED
앞서 회원고 로그를 하나의 트랜잭션으로 묶어 데이터 정합성 문제를 해결했음.
그런데 로그를 DB 에 남기는 작업에 문제가 있다고 해서 회원가입에 실패해 사용자 이탈 발생
로그는 복구할 수 있으므로
비즈니스 요구사항이 회원 가입 시도 로그 남기는데 실패해도 회원가입은 유지되어야 한다.
회원가입 성공, 로그는 실패해도 회원가입 커밋은 진행되도록!
LogRepository에서 발생한 런타임 에러를MemberService에서 잡아 처리하면 될 것 같음.MemberService에서 정상이기 때문에 해당 트랜잭션에서 커밋을 수행할 수 있게된다.rollbackOnly때문에.이 방법이 왜 실패하는지 예제를 통해서 알아보자. 참고로 실무에서 많은 개발자가 이 방법을 사용해서 실패한다.
rollbackOnly설정 하기 때문에 외부 트랜잭션에서 커밋해도 전체 물리 트랜잭션은 롤백된다.UnexpectedRollbackException이 던저진다.전체 흐름을 알아보자
LogRepository에서 예외 발생. 예외를 던지면LogRepository의 트랜잭션 AOP 가 예외를 받음rollbackOnly표시함.MemberService가 예외를 받아 복구하고 정상 리턴하며 AOP 가 커밋 호출rollbackOnly를 체크하고 true기 때문에 물리 트랜잭션은 롤백된다UnexpectedRollbackException예외 던짐.정리
UnexpectedRollbackException예외가 발생한다.rollbackOnly상황에서 커밋이 발생하면UnexpectedRollbackException예외가 발생한다.그렇다면 어떻게 해야 다음 요구사항을 만족할 수 있을까?
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
여러 옵션이 있지만 REQUIRES_NEW 옵션으로 해결해보자.
트랜잭션 전파 활용7- 복구 REQUIRES_NEW
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
이 요구사항을 만족하기 위해서 로그와 관련된 물리 트랜잭션을 별도로 분리해보자. 바로
REQUIRES_NEW를 사용하는 것이다.예외를 복구하는 memberService.joinV2()를 사용한다는 점도 주의하자
LogRepository 에 옵션 추가
이렇게 하면 LogRepository 는 별도의 커넥션을 사용하는 트랜잭션이 되어서 안에서만 롤백됨.
그러나 익셉션은 던저지는데, 이를
MemberService에서 잡았으니 괜찮음MemberRepository는REQUIRED옵션을 사용한다. 따라서 기존 트랜잭션에 참여한다.LogRepository의 트랜잭션 옵션에REQUIRES_NEW를 사용했다.REQUIRES_NEW는 항상 새로운 트랜잭션을 만든다. 따라서 해당 트랜잭션 안에서는 DB 커넥션도 별도로 사용하게 된다.REQUIRES_NEW를 사용하게 되면 물리 트랜잭션 자체가 완전히 분리되어 버린다.REQUIRES_NEW는 신규 트랜잭션이므로rollbackOnly표시가 되지 않는다. 그냥 해당 트랜잭션이 물리 롤백되고 끝난다.LogRepository를 시작하는데 옵션이REQUIRES_NEW이므로 여기 시작할떈 별도의 커넥션2 을 사용하고, 원래 쓰던 커넥션 1은 잠깐 미뤄둠LogRepository에서 예외가 발생한다. 예외를 던지면LogRepository의 트랜잭션 AOP가 해당 예외를 받는다.REQUIRES_NEW를 사용한 신규 트랜잭션이므로 물리 트랜잭션을 롤백한다. 물리 트랜잭션을 롤백했으므로rollbackOnly를 표시하지 않는다. 여기서REQUIRES_NEW를 사용한 물리 트랜잭션은 롤백되고 완전히 끝이 나버린다.MemberService에 던져지고,MemberService는 해당 예외를 복구한다. 그리고 정상적으로 리턴한다.MemberService트랜잭션 AOP 는 커밋 호출, rollbackOnly 없으므로 물리트랜잭션 커밋.결과적으로 회원 데이터는 저장되고, 로그 데이터만 롤백 되는 것을 확인할 수 있다.
정리
REQUIRES_NEW를 사용해서 트랜잭션을 분리해야 한다.MemberService에서 2개의 리포지토리만 사용하도록 했지만, 실제론 더 많이 호출하고 그중에LogRepository만 분리한다고 생각하면 좋다.주의
REQUIRES_NEW를 사용하면 하나의 HTTP 요청에 동시에 2개의 데이터베이스 커넥션을 사용하게 된다. 따라서 성능이 중요한 곳에서는 이런 부분을 주의해서 사용해야 한다.REQUIRES_NEW를 사용하지 않고 문제를 해결할 수 있는 단순한 방법이 있다면, 그 방법을 선택하는 것이 더 좋다.예를 들면 다음과 같이
REQUIRES_NEW를 사용하지 않고 구조를 변경하는 것이다.MemberService앞단에MemberFacade를 두고 여기서하도록 함.
애초에 트랜잭션을 분리해버림.
이렇게 하면 HTTP 요청에 동시에 2개의 커넥션을 사용하지는 않는다. 순차적으로 사용하고 반환하게 된다.
물론 구조상
REQUIRES_NEW를 사용하는 것이 더 깔끔한 경우도 있으므로 각각의 장단점을 이해하고 적절하게 선택해서 사용하면 된다..All reactions