김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
Member / Log : 각각 username
, message
로 객체 생성
MemberRepository / LogRepository : 객체를 DB 저장( save()
) 및 조회( find()
)
LogRepository.save()
: "로그예외"라는 메세지이면 런타임 예외 발생MemberService : String을 받아 Member, Log 객체 생성 및 각 Repository 를 통해 저장
- MemberService 의
joinV1()
에 @Transactional 이 없는 상황- 각 Repository의
save()
메서드에 @Transactional 이 있는 상황
➜ MemberRepository 에 @Transactional
이 있으므로 트랜잭션 AOP가 작동
➜ 트랜잭션 매니저에 트랜잭션을 요청하고 데이터 소스를 통해 커넥션 획득
➜ 해당 커넥션을 수동 커밋 모드로 변경하고 트랜잭션 시작
➜ 트랜잭션 동기화 매니저를 통해 트랜잭션을 시작한 커넥션을 보관
➜ 트랜잭션 매니저의 호출 결과로 status 를 반환
MemberRepository 가 회원을 저장할 때 트랜잭션이 시작된 con1 을 사용
MemberRepository 가 정상 응답을 반환 ➜ 트랜잭션 AOP는 트랜잭션 매니저에 커밋 요청
트랜잭션 매니저는 신규 트랜잭션 여부, rollbackOnly 여부를 모두 체크한 후 con1 을 통해 물리 트랜잭션을 커밋
LogRepository 도 커밋하기 때문에 동일한 과정을 수행
LogRepository 는 다른 트랜잭션과 관련된 con2 를 사용한다
로그예외 라는 이름을 전달해서 LogRepository 에 런타임 예외가 발생한다
LogRepository 는 해당 예외를 밖으로 던진다. 이 경우 트랜잭션 AOP가 예외를 받게된다
런타임 예외가 발생해서 트랜잭션 AOP는 트랜잭션 매니저에 롤백을 호출한다
트랜잭션 매니저는 신규 트랜잭션이므로 물리 롤백을 호출한다
- MemberService 의
joinV1()
에 @Transactional 이 있는 상황- 각 Repository의
save()
메서드에 @Transactional 이 없는 상황
@Transactinal
이 MemberService 에만 있기 때문에 여기에만 트랜잭션 AOP가 적용
MemberService 의 시작부터 종료까지 모든 로직을 하나의 트랜잭션으로 묶을 수 있다
MemberService 의 시작 이후 모든 관련 로직은 해당 트랜잭션이 생성한 커넥션 사용
MemberRepository, LogRepository를 호출하기 때문에 같은 트랜잭션 사용
MemberService 부터 MemberRepository , LogRepository 를 모두 하나의 트랜잭션으로 묶고 싶은 클라이언트
MemberRepository 만 호출하고 여기에만 트랜잭션을 사용하고 싶은 클라이언트
LogRepository 만 호출하고 여기에만 트랜잭션을 사용하고 싶은 클라이언트가 존재할 때
MemberService 에만 트랜잭션을 적용하면 다른 클라이언트가 호출하는 Repository 에는 트랜잭션을 적용할 수 없는 문제가 발생
이런 문제를 해결하기 위해 트랜잭션 전파가 필요
@Transactional
이 적용되어 있으면 기본으로 REQUIRED
전파 옵션을 사용
이 옵션은 기존 트랜잭션이 없으면 새로 생성하고, 있으면 기존 트랜잭션에 참여한다
참여한다는 의미는 같은 동기화 커넥션을 사용한다는 것이다
즉, MemberService, MemberRepository, LogRepository 모두 @Transactional
을 사용하면 해결할 수 있다
@Transactional
이 붙어 있기 때문에 트랜잭션 AOP가 적용MemberService
를 호출하면서 트랜잭션 AOP가 호출➜ 신규 트랜잭션 생성, 물리 트랜잭션 시작
MemberRepository
를 호출하면서 트랜잭션 AOP가 호출➜ 기존 트랜잭션이 있으므로 참여한다
MemberRepository
의 로직 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출➜ 트랜잭션 AOP는 정상 응답이므로 트랜잭션 매니저에 커밋을 요청
➜ 신규 트랜잭션이 아니므로 실제 커밋을 호출하지 않는다
LogRepository
도 MemberRepository
와 동일하게 수행된다
MemberService
의 로직 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출
➜ 트랜잭션 AOP는 정상 응답이므로 트랜잭션 매니저에 커밋을 요청
➜ 신규 트랜잭션이므로 물리 커밋을 호출
MemberService
를 호출하면서 트랜잭션 AOP가 호출➜ 신규 트랜잭션 생성, 물리 트랜잭션 시작
MemberRepository
를 호출하면서 트랜잭션 AOP가 호출➜ 기존 트랜잭션이 있으므로 참여한다
MemberRepository
의 로직 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출➜ 트랜잭션 AOP는 정상 응답이므로 트랜잭션 매니저에 커밋을 요청
➜ 신규 트랜잭션이 아니므로 실제 커밋을 호출하지 않는다
LogRepository
를 호출하면서 트랜잭션 AOP가 호출➜ 기존 트랜잭션이 있으므로 참여한다
LogRepository
로직에서 런타임 예외가 발생, 트랜잭션 AOP가 예외를 받는다➜ 트랜잭션 AOP는 런타임 예외가 발생했으므로 트랜잭션 매니저에 롤백을 요청
➜ 신규 트랜잭션이 아니므로 물리 롤백을 호출하지 않고 rollbackOnly 를 설정
➜ LogRepository
가 예외를 던졌기 때문에 트랜잭션 AOP도 해당 예외를 밖으로 던진다
MemberService
에서도 런타임 예외를 받게 되는데, 처리하지 않고 밖으로 던진다➜ 트랜잭션 AOP는 런타임 예외가 발생했으므로 트랜잭션 매니저에 롤백을 요청
➜ 신규 트랜잭션이므로 물리 롤백을 호출
➜ MemberService
가 예외를 던졌기 때문에 트랜잭션 AOP도 해당 예외를 밖으로 던진다
➜ 클라이언트가 LogRepository
부터 넘어온 런타임 예외를 받게 된다
로그를 DB에 저장할 때 문제가 발생하면 전체 트랜잭션이 롤백되기 때문에 회원 가입 실패
but> 로그를 DB에 저장하는 것은 실패해도 회원 가입은 정상적으로 진행되도록 하고 싶다
MemberService 에서 예외를 잡아서 정상 흐름을 반환하는 joinV2()
를 사용하면 회원 가입은 정상적으로 진행될 것인가?
동작을 확인해보면 회원 가입이 정상적으로 이루어지지 않고, MemberService 에서 클라이언트로 UnexpectedRollbackException
을 던지게 된다
내부 트랜잭션에서 rollbackOnly
를 설정하기 때문에 정상 흐름 처리를 해서 외부 트랜잭션에서 커밋을 해도 물리 트랜잭션이 롤백되기 때문이다
LogRepository
에서 예외가 발생하고 예외를 던진다 ➜ LogRepository
의 트랜잭션 AOP가 해당 예외를 받는다
➜ 신규 트랜잭션이 아니므로 물리 트랜잭션을 롤백하지는 않는다
➜ 트랜잭션 동기화 매니저에 rollbackOnly
를 표시한다
➜ 그 후 트랜잭션 AOP는 예외를 밖으로 던진다
MemberService
는 던져진 예외를 복구하고 정상 흐름을 반환 ➜ 정상 흐름이 되었으므로 MemberService
의 트랜잭션 AOP는 커밋을 호출
➜ 신규 트랜잭션이므로 물리 트랜잭션을 커밋해야하는데 이 때 rollbackOnly
를 체크
➜ rollbackOnly
가 체크 되어 있으므로 물리 트랜잭션을 롤백한다
➜ 트랜잭션 매니저는 UnexpectedRollbackException
예외를 던진다
➜ 트랜잭션 AOP도 전달받은 UnexpectedRollbackException
을 클라이언트에 던진다
논리 트랜잭션 중 하나라도 롤백되면 전체 트랜잭션은 롤백된다
내부 트랜잭션이 롤백 되었는데, 외부 트랜잭션이 커밋되면 UnexpectedRollbackException
예외가 발생
rollbackOnly
상황에서 커밋이 발생하면 UnexpectedRollbackException
예외가 발생
MemberService
에서 정상 흐름을 반환하는 joinV2()
를 사용해도 로그를 남길 때 예외가 발생하면 전체 트랜잭션은 롤백되어 버렸다
joinV2()
를 사용하면서 LogRepository.save()
의 @Transacional
의 옵션에 REQUIRES_NEW
를 사용하면 로그를 남기는데 실패해도 회원 가입은 정상적으로 저장된다
REQUIRES_NEW
는 항상 새로운 트랜잭션을 생성
➜ 물리 트랜잭션 자체가 분리되어 기존 트랜잭션에 영향을 주지 않는다
➜ 롤백해도 rollbackOnly
표시가 되지 않고 해당 트랜잭션이 롤백되고 종료
➜ 즉, 로그 남기는 것과 관계 없이 회원 가입이 정상적으로 처리되었으면 회원이 저장된다
LogRepository
에서 예외가 발생하고 예외를 던진다 ➜ LogRepository
의 트랜잭션 AOP가 해당 예외를 받는다
➜ 신규 트랜잭션이므로 물리 트랜잭션을 롤백한다
➜ 물리 트랜잭션을 롤백했으니 rollbackOnly
표시 없이 종료된다
➜ 그 후 트랜잭션 AOP는 전달받은 예외를 밖으로 던진다
MemberService
는 던져진 예외를 복구하고 정상 흐름을 반환 ➜ 정상 흐름이 되었으므로 MemberService
의 트랜잭션 AOP는 커밋을 호출
➜ 신규 트랜잭션이므로 물리 트랜잭션을 커밋해야하는데 이 때 rollbackOnly
를 체크
➜ rollbackOnly
가 없으므로 물리 트랜잭션을 커밋하고 정상 흐름이 반환
REQUIRES_NEW
를 사용하면 하나의 HTTP 요청에 동시에 2개의 데이터베이스 커넥션을 사용하게 된다
새로운 커넥션이 사용될 때 이전의 커넥션은 잠시 중단된 상태로 유지된다