Spring/DB

스프링 DB 2편 - 11장. 스프링 트랜잭션 전파2 - 활용

광터틀 2022. 10. 10. 15:05

트랜잭션 전파 활용1 - 예제 프로젝트 시작 

 

비즈니스 요구사항 

- 회원 등록 및 조회,

- 변경 이력 추적위해 변경 이력을 DB LOG 테이블에 남기기 

 

멤버 엔티티, 회원 리포지토리, 로그 엔티티, 로그 리포지토리, 멤버 서비스 등을 만들며 JPA를 사용한다.

JPA의 구현체인 하이버네이트가 테이블을 자동으로 생성.

메모리 DB이기 때문에 모든 테스트가 완료된 이후에 DB는 사라진다. 

JPA를 통한 모든 데이터 변경에는 트랜잭션이 필요하다. 현재 코드에서는 서비스 계층에 트랜잭션이 없기 때문에 리포지토리에 트랜잭션이 있다. 

 


 

트랜잭션 전파 활용2 - 커밋, 롤백 

 

서비스 계층에 트랜잭션이 없을 때 - 커밋 

서비스 계층에 트랜잭션이 없고 회원, 로그 리포지토리가 각각 트랜잭션을 가지고 있으며 이때 회원, 로그 리포지토리가 둘다 커밋에 성공한다면 

 

 

서비스 계층에 트랜잭션이 없을 때 - 롤백

서비스 계층에 트랜잭션이 없고 회원, 로그 리포지토리가 각각 트랜잭션을 가지고 있으며 이때 회원 리포지토리는 정상 동작하지만 로그 리포지토리에서 예외가 발생한다면 

 

위 경우 회원은 저장되지만 회원 이력 로그는 롤백된다. 따라서 데이터 정합성에 문제가 발생할 수 있다. 둘을 하나의 트랜잭션으로 묶어서 처리해보자. 

 

 


 

트랜잭션 전파 활용3 - 단일 트랜잭션 

트랜잭션 하나만 사용하기

- 회원 리포지토리오 로그 리포지토리를 하나의 트랜잭션으로 묶는 가장 간단한 방법은 이 둘을 호출하는 회원 서비스에만 트랜잭션을 사용하는 것이다. 

 

MemberRepository, LogRepository 의 @Transactional 코드를 제거.

MemberService 에만 @Transactional 코드를 추가하자. 

 

-> 이렇게 하면 MemberService를 시작할 때부터 종료할 때까지의 모든 로직을 하나의 트랜잭션으로 묶을 수 있다. 

MemberService 만 트랜잭션을 처리하기 때문에 앞서 배운 논리 트랜잭션, 물리 트랜잭션, 외부 트랜잭션, 내부 트랜잭션, rollbackOnly, 신규 트랜잭션, 트랜잭션 전파와 같은 복잡한 것은 고민할 필요가 없고 단순하고 깔끔하게 트랜잭션을 묶을 수 있다. 

 

@Transactional이 MemberService에만 붙어있으므로 MemberRepository, LogRepository는 트랜잭션 AOP가 적용되지 않는다. MemberService의 시작부터 끝까지 관련 로직은 해당 트랜잭션이 생성한 커넥션을 사용하게 된다. 

 

 

하지만 클라이언트마다 트랜잭션을 사용하고 싶은 부분이 다르다면?

-> 트랜잭션이 있는 메서드와 없는 메서드를 각각 만들어야한다. 이런 문제를 해결하기 위하여 트랜잭션 전파가 필요하다. 

 


 

트랜잭션 전파 활용4 - 전파 커밋 

스프링은 @Transactional이 적용되어 있으면 기본으로 REQUIRED 라는 전파 옵션을 사용한다. 기존 트랜잭션이 없으면 트랜잭션을 생성하고, 기존 트랜잭션이 있으면 기존 트랜잭션을 그대로 따르며 같은 동기화 커넥션을 사용한다. 

 

 

 

 


 

트랜잭션 전파 활용5 - 전파 롤백 

로그 리포지토리에서 예외가 발생하여 전체 트랜잭션이 롤백되는 경우 

즉, 회원과 회원 이력 로그를 처리하는 부분을 하나의 트랜잭션으로 묶은 덕분에 문제가 발생했을 때 회원과 회원 이력 로그가 모두 함께 롤백된다. 

 

 


 

트랜잭션 전파 활용6 - 복구 REQUIRED 

앞에서 회원과 로그를 하나의 트랜잭션으로 묶어서 데이터 정합성 문제를 깔끔히 해결했다. 

 

그런데 회원 이력 로그를 DB에 남기는 작업에 가끔 문제가 발생해서 회원 가입 자체가 안 되는 경우가 가끔 발생하게 되었다. 그래서 사용자들이 회원가입에 실패해서 이탈하는 문제가 발생한다. 

 

비즈니스 요구사항을 다음과 같이 변경하자. 

- 회원가입을 시도한 로그를 남기는데 실패하더라도 회원가입은 유지되어야 한다. 

이때 LogRepository에서 예외가 발생하면 MemberService에서 예외를 잡아서 처리하면 될것 같지만 이렇게 간단히 생각하면 안된다. 

논리 트랜잭션 중 하나라도 롤백되면 전체 트랜잭션은 롤백된다. 내부 트랜잭션이 롤백 되었는데, 외부 트랜잭션이 커밋되면 UnexpectedRollbackException 예외가 발생한다. 

rollbackOnly 상황에서 커밋이 발생하면 UnexpectedRollbackException 예외가 발생한다. 

 


 

트랜잭션 전파 활용7 - 복구 REQUIRES_NEW 

회원가입을 시도한 로그를 남기는데 실패하더라도 회원가입은 유지되어야 한다. 

이를 위해 로그와 관련된 물리 트랜잭션을 REQUIRES_NEW를 이용하여 별도로 분리해보자. 

기존 트랜잭션에 참여하는 REQUIRED 대신에 

@Transactional(propagation = Propagation.REQUIRES_NEW)

을 적용하자. 

 

결과적으로 회원 데이터는 저장되고, 로그 데이터만 롤백 되는 것을 확인할 수 있다. 

 

논리 트랜잭션은 하나라도 롤백되면 관련된 물리 트랜잭션은 롤백되어 버린다. 

이 문제를 해결하기 위하여 REQUIRES_NEW를 사용해서 트랜잭션을 분리해야 한다. 

예제를 단순화하기 위해 MemberService가 MemberRepository, LogRepository만 호출하지만 실제로는 더 많은 리포지토리들을 호출하고 그 중에 LogRepository만 트랜잭션을 분리한다고 생각해보면 이해하는데 도움이 될 것이다. 

 

 

즉, REQUIRES_NEW를 쓰면 그냥 트랜잭션을 새로 만드므로 회원데이터는 저장되고 로그데이터만 롤백된다.