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
여기서는 각각의 테스트가 완료된 시점에 데이터를 삭제하지 않는다. 따라서 username은 테스트별로 각각 다르게 설정해야 한다. 그렇지 않으면 다음 테스트에 영향을 준다.
JPA와 데이터 변경
JPA를 통한 모든 데이터 변경(등록, 수정, 삭제)에는 트랜잭션이 필요하다.( 조회는 트랜잭션 없이 가능하다)
현재 코드에서 서비스 계층에 트랜잭션이 없기 때문에 리포지토리에 트랜잭션이 있다.
2. 커밋, 롤백
서비스 계층에 트랜잭션이 없을때 - 커밋
예제를 통해 서비스 계층에 트랜잭션이 없을때 트랜잭션이 각각 어떻게 작동하는지 확인해보자
“상황”
서비스 계층에 트랜잭션이 없다.
회원, 로그 리포지토리가 각각 트랜잭션을 가지고 있다.
회원, 로그 리포지토리 둘다 커밋에 성공한다.
Test
/** * memberService @Transactional:OFF * memberRepository @Transactional:ON * logRepository @Transactional:ON Exception */@TestvoidouterTxOff_fail() {
//givenStringusername = "로그예외_outerTxOff_fail";
//whenmemberService.joinV1(username);
//when : 모든 데이터가 정상 저장된다.assertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isPresent());
}
결과
정리
MemberService에서 MemberRepository 를 호출한다. MemberRepository에는 @transactional 애노테이션이 있으므로 트랜잭션 AOP가 작동한다. 여기서 트랜잭션 매니저를 통해 트랜잭션을 시작한다. 이렇게 시작한 트랜잭션을 트랜잭션 B라 하자.
그림에서는 생략했지만 트랜잭션 매니저에 트랜잭션을 요청하면 데이터 소스를 통해 커넥션 con1을 획득하고 해당 커넥션을 수동 커밋모드로 변경해서 트랜잭션을 시작한다.
그리고 트랜잭션 동기화 매니저를 통해 트랜잭션을 시작한 커넥션을 보관한다.
트랜잭션 매니저의 호출 결과로 status 를 반환한다. 여기서는 신규 트랜잭션 여부가 참이 된다.
MemberRepository 는 JPA를 통해 회원을 저장하는데 이때 JPA는 트랜잭션이 시작된 con1 을 사용해서 회원을 저장한다.
MemberRepository가 정상 응답을 반환했기 때문에 트랜잭션 AOP는 트랜잭션 매니저에 커밋을 요청한다.
트랜잭션 매니저는 con1 을 통해 물리 트랜잭션을 커밋한다.
물론 이 시점에 앞서 설명한 신규 트랜잭션 여부, rollbackOnly 여부를 모두 체크한다.
이렇게 해셔 MemberRepository와 관련된 모든 데이터는 정상 커밋되고 트랜잭션 B는 완전히 종료된다. 이후에 LogRepository를 통해 트랜잭션C를 시작하고 정상 커밋한다.
이렇게 하면 MemberService를 시작할때부터 종료할때까지의 모든 로직을 하나의 트랜잭션으로 묶을 수 잇다.
물론 MemberService가 MemberRepository, LogRepository 를 호출하므로 이 로직들은 같은 트랜잭션을 사용한다.
MemberService 만 트랜잭션을 처리하기 때문에 앞서 배운 논리 트랜잭션, 물리 트랜잭션, 외부 트랜잭션, 내부 트랜잭션, rollbackOnly, 신규 트랜잭션, 트랜잭션 전파와 같은 복잡한 것을 고민할 필요가 없다. 아주 단순하고 깔끔하게 트랜잭션을 묶을 수 있다.
@ Transactional 이 MemberService 에만 붙어있기 때문에 여기에만 트랜잭션 AOP가 적용된다.
MemberRepository, LogRepository 는 트랜잭션 AOP가 적용되지 않는다.
MemberService 의 시작부터 끝까지 관련 로직은 해당 트랜잭션이 생성한 커넥션을 사욯하게 된다.
MemberService가 호출하는 MemberRepository, LogRepository 도 같은 커넥션을 사용하면서 자연스럽게 트랜잭션 범위에 포함된다.
“참고”
같은 스레드를 사용하면 트랜잭션 동기화 매니저는 같은 커넥션을 반환한다. → 스레드 로컬 때문
“직접 실행, 로그와 그림을 비교해보기”
결과
각각 트랜잭션이 필요한 상황
트랜잭션 적용 범위
클라이언트 A 는 MemberService 부터 MemberReporitory 를 모두 하나의 트랜잭션으로 묶고 싶다.
클라이언트 B는 MemberRepository 만 호출하고 여기에만 트랜잭션을 사용허고 싶다.
클라이언트 C는 LogRepository 만 호출하고 여기에만 트랜잭션을 사용하고 싶다.
클라이언트 A 만 생각하면 MemberService에 트랜잭션 코드를 남기고 MemberRepository, LogRepository의 트랜잭션코드를 제거하면 앞서 배운것처럼 깔끔하게 하나의 트랜잭션을 적용할 수 있다.
하지만 이렇게 되면 클라이언트 B,C가 호출하는 MemberRepository, LogRepository의 트랜잭션 코드를 제거하면 앞서 배운 것처럼 깔끔하게 하나의 트랜잭션을 적용할 수 있다.
트랜잭션 전파없이 이런 문제를 해결하려면 아마도 트랜잭션이 있는 메서드와 없는 메서드를 각각 만들어야 한다.
클라이언트 Z가 호출하는 OrderService에서도 트랜잭션을 시작할 수 있어야 하고 클라이언트 A가 호출하는 MemberService에서도 트랜잭션을 시작할 수 있어야한다.
이런 문제를 해결하기 위해 트랜잭션 전파가 필요한다.
4. 전파 커밋
스프링은 @transactional 이 적용되어 있으면 기본으로 REQUIRED 라는 전파옵션을 사용한다.
이 옵션은 기존 트랜잭션이 없으면 트랜잭션을 생성하고 기존 트랜잭션이 있으면 기존 트랜잭션에 참여한다.
참여한다는 뜻은 해당 트랜잭션을 그대로 따른다는 뜻이고 동시에 같은 동기화 커넥셔은 사용한다는 뜻이다.
이렇게 둘 이상의 트랜잭션이 하나의 물리 트랜잭션에 묶이게 되면 둘을 구분하기 위해 논리 트랜잭션과 물리 트랜잭션으로 구분한다.
외부 트랜잭션, 내부 트랜잭션
신규 트랜잭션
이 경우에 외부에 있는 신규 트랜잭션만 실제 물리 트랜잭션을 시작하고 커밋한다
내부에 있는 트랜잭션은 물리 트랜잭션 시작하거나 커밋하지 않는다.
모든 논리 트랜잭션 커밋
모든 논리 트랜잭션을 커밋해야 물리 트랜잭션도 커밋된다. 하나라도 롤백되면 물리 트랜잭션은 롤백된다.
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 클래스 만들어준다.
MemberRepository
Log 객체 만들고
LogRepository
MemberService
회원을 등록하면서 동시에 회원등록에 대한 DB 로그도 함께 남긴다.
joinV1()joinV2()joinV1()과 같은 기능을 수행한다Ctrl+Shift+T : Test
Test 완료 결과

참고
JPA와 데이터 변경
2. 커밋, 롤백
서비스 계층에 트랜잭션이 없을때 - 커밋
예제를 통해 서비스 계층에 트랜잭션이 없을때 트랜잭션이 각각 어떻게 작동하는지 확인해보자
“상황”
Test결과

정리
이렇게 해셔 MemberRepository와 관련된 모든 데이터는 정상 커밋되고 트랜잭션 B는 완전히 종료된다. 이후에 LogRepository를 통해 트랜잭션C를 시작하고 정상 커밋한다.
결과적으로 둘다 커밋되었으므로 Member, Log 모두 안전하게 저장된다.
@transactional과 REQUIRED
서비스 계층에 트랜잭션이 없을때 - 롤백
상황
수정
결과

“로그예외 로직”
MemberService에서 MemberRepository를 호출하는 부분은 앞서 설명한 내용과 같다. 트랜잭션이 정상 커밋되고 회원데이터도 DB에 정상 반영된다.MemberService에서LogRepository를 호출하는데, 로그 예외라는 이름을 전달한다. 이 과정에서 새로운 트랜잭션 C가 만들어진다.“LogRepository 응답로직”
참고
트랜잭션 AOP도 결국 내부에서는 트랜잭션 매니저를 사용하게 된다.
이 경우 회원은 저장되지만 회원 로그는 롤백된다. 따라서 데이터 정합성에 문제가 발생할 수 있다. 둘을 하나의 트랜잭션으로 묶어서 처리해보자.
3. 단일 트랜잭션
트랜잭션 하나만 사용하기
회원 리포지토리와 로그 리포지토리를 하나의 트랜잭션으로 묶는 간단한 방법은 이 둟을 호출하는 회원 서비스에만 트랜잭션을 사용하는 것이다.
“참고”
같은 스레드를 사용하면 트랜잭션 동기화 매니저는 같은 커넥션을 반환한다. → 스레드 로컬 때문
“직접 실행, 로그와 그림을 비교해보기”
결과
각각 트랜잭션이 필요한 상황
트랜잭션 적용 범위
클라이언트 A 는 MemberService 부터 MemberReporitory 를 모두 하나의 트랜잭션으로 묶고 싶다.
클라이언트 B는 MemberRepository 만 호출하고 여기에만 트랜잭션을 사용허고 싶다.
클라이언트 C는 LogRepository 만 호출하고 여기에만 트랜잭션을 사용하고 싶다.
클라이언트 A 만 생각하면 MemberService에 트랜잭션 코드를 남기고 MemberRepository, LogRepository의 트랜잭션코드를 제거하면 앞서 배운것처럼 깔끔하게 하나의 트랜잭션을 적용할 수 있다.
하지만 이렇게 되면 클라이언트 B,C가 호출하는 MemberRepository, LogRepository의 트랜잭션 코드를 제거하면 앞서 배운 것처럼 깔끔하게 하나의 트랜잭션을 적용할 수 있다.
트랜잭션 전파없이 이런 문제를 해결하려면 아마도 트랜잭션이 있는 메서드와 없는 메서드를 각각 만들어야 한다.

클라이언트 Z가 호출하는 OrderService에서도 트랜잭션을 시작할 수 있어야 하고 클라이언트 A가 호출하는 MemberService에서도 트랜잭션을 시작할 수 있어야한다.
이런 문제를 해결하기 위해 트랜잭션 전파가 필요한다.
4. 전파 커밋
스프링은 @transactional 이 적용되어 있으면 기본으로 REQUIRED 라는 전파옵션을 사용한다.
이 옵션은 기존 트랜잭션이 없으면 트랜잭션을 생성하고 기존 트랜잭션이 있으면 기존 트랜잭션에 참여한다.
참여한다는 뜻은 해당 트랜잭션을 그대로 따른다는 뜻이고 동시에 같은 동기화 커넥셔은 사용한다는 뜻이다.

이렇게 둘 이상의 트랜잭션이 하나의 물리 트랜잭션에 묶이게 되면 둘을 구분하기 위해 논리 트랜잭션과 물리 트랜잭션으로 구분한다.
외부 트랜잭션, 내부 트랜잭션
신규 트랜잭션
모든 논리 트랜잭션 커밋
먼저 모든 트랜잭션이 커밋되는 상황을 보자.
모든 곳에 트랜잭션을 적용하자 - 주석풀기
Test 코드
5. 전파 롤백
이번에는 로그 리포지토리에서 예외가 발생해서 전체 트랜잭션이 롤백이 되는 경우를 알아보자.

Test 코드
결과

흐름
정리
6. 복구 REQUIRED
회원 이력 로그를 DB에 남기는 작업에 가끔 문제가 발생해서 회원가입 자체가 안되는 경우가 가끔 발생하게 되었다.
그래서 사용자들이 회원 가입에 실패해서 이탈하는 문제가 발생하기 시작했다.
회원 이력 로그의 경우 여러가지 방법으로 추후에 복구가 가능할 것으로 보인다.
그래서 비즈니스 요구사항이 변경되었다.
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
예상
Test 코드
joinV2
memberRepository.find(username).isPresent()일때 결과
UnexceptedRollbackException이 던져진다.전체 흐름
정리
그렇다면 어떻게 해야 다음 요구사항을 만족할 수 있을까?
회원가입을 시도한 로그를 남기는데 실패하더라도 회원가입은 유지되어야 한다.
7. 복구 REQUIRES_NEW
1단계_메소드 작성
결과

결과

결과

결과적으로 회원 데이터가 저장되고 로그 데이터만 롤백되는 것을 확인할 수 있다
정리
주의
예를 들면 다음과 같이 REQUIRES_NEW를 사용하지 않고 구조를 변경하는 것이다.

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