개발자로 후회없는 삶 살기

[문법] 스프링 트랜잭션 전파 활용 본문

[백엔드]/[spring | 학습기록]

[문법] 스프링 트랜잭션 전파 활용

몽이장쥰 2024. 8. 8. 17:54

서론

※ 과거에 기록한 내용에서 중요한 부분만 발췌하여 모두가 이해하기 쉽게 다시 서술한다.

 

본론

- 트랜잭션 등록 과정

@트랜잭션이 일어날때 어떤 일이 일어나나 깊이있게 알아보자

 

@트랜잭션이 클래스나 메서드에 하나라도 있으면, 트랜잭션 AOP가 프록시를 만들어서 컨테이너에 등록한다. 실제 객체 대신에 프록시 컨테이너에 들어가고 AutoWired하면 프록시가 주입된다.

 

@트랜잭션을 메서드에 붙여도 프록시가 생성되니 nonTx 메서드도 트랜잭션이 적용되어야 하지 않나 싶다. nonTx도 호출은 프록시의 메서드가 호출되지만, @트랜잭션이 없어서 트랜잭션이 안 된다.

 

- 트랜잭션 적용 위치

트랜잭션을 클래스, 메서드 단위에 붙였을 때 적용 범위를 보자. 스프링은 구체적인 것이 우선순위를 가진다.

 

write와 read 메서드를 호출해보면

 

write는 false라서 readonly가 안되고, read는 클래스 단위에 붙은 트랜잭션이 적용된다.

 

🚨 트랜잭션 AOP 프록시 내부 호출

실무에서 많이 하는 실수로 트랜잭션을 적용했는데 적용이 안되는 경우가 있다.

 

만약에 프록시를 거치지 않고 직접 대상 객체를 호출하면 프록시에만 트랜잭션이 있으니 적용되지 않을 것이다. AOP를 적용하면 프록시를 빈으로 등록해서 대상 객체를 직접 호출하는 문제는 잘 발생하지 않는다. 하지만, Target 객체 내부에서 다른 메서드를 내부 호출하면 프록시를 거치치 않고 대상 객체를 직접 호출하는 문제가 발생한다. 프록시를 거쳐서 target 메서드를 호출해야 하는데, 안 그러면 대상 객체를 직접 호출해서 트랜잭션이 적용되지 않는다.

 

- 테스트

하지만, 내부에서 메서드를 호출하면 @트랜잭션을 붙여도 트랜잭션이 적용되지 않을 수 있다. ex는 트랜잭션을 적용하지 않는 코드이고 in은 트랜잭션을 적용하는데 ex에서 in을 내부 호출하는 코드이다.

 

서비스 클래스를 등록하고 주입받으면 인터널에 트랜잭션이 있어서 CallService가 프록시로 주입되고

 

in을 외부에서 호출할 때는 트랜잭션이 적용되지만,

 

설명

 ex를 통해서 내부에서 호출하면 적용되지 않는다. 트랜잭션이 메서드에 있어도 프록시가 등록되고 호출할 때 @가 붙어있어서 트랜잭션이 적용되어야 정상인데, 이는 클라이언트에서 트랜잭션이 붙은 메서드를 직접 호출한 상황이다.

 

실제 호출되는 흐름을 보자, 클라이언트에서 서비스의 external을 호출할 땐 프록시의 external을 호출한다. ex에는 트랜잭션이 없어서 적용하지 않고 넘긴다. 이때 문제는 ex 내부에서 in을 호출해서 프록시를 거치지 않고 호출한다.

 

 

컨트롤러 : 주입 받은 프록시 사용
서비스 : 자기 자신 사용(주입 x)

자바는 앞이 생략 되어있으면 this를 생략해서 자기 자신의 메서드를 호출한다.

 

해결 방법

실무에서는 내부 호출 로직인 internal 메서드를 별도의 클래스로 분리한다. 즉, 무조건 트랜잭션이 적용되어야 하는 메서드는 분리해서 사용해야 한다.

 

트랜잭션이 붙은 메서드를 별도의 클래스로 분리하고 트랜잭션이 반드시 일어나도록 외부 호출로 만들어버린다.

 

그러면 원래 서비스에서 in 서비스를 주입 받아서 사용해서 외부 호출로 바꿔버린다. 따라서 무조건 AOP가 호출이 된다.

 

- 트랜잭션 옵션

1. rollbackFor

언체크 : 스프링에서 런타임 예외는 롤백
체크 : 커밋

예외가 발생하면 기본 정책은 위와 같다. rollbackFor를 사용하면 체크 예외에서 커밋할 때도 롤백하게 할 수 있다. noRollBackFor은 그 반대이다.

 

2. isolation

트랜잭션 격리 수준을 지정한다.

 

3. readOnly

1) 프레임워크 : jpa는 읽기 전용이면, 스냅샷과 변경 감지를 안함
2) jdbc 드라이버 : 읽기 전용에서 변경이 발생하면 아예 예외를 던짐
3) db : db 내부적으로 수정, 등록에 관련된 준비를 안함

트랜잭션은 기본적으로 읽기 쓰기에 다 가능하다. true로 하면 읽기 전용 트랜잭션이 생성되고 등록, 수정, 삭제는 안된다.

 

- 예외와 트랜잭션 커밋, 롤백

1) 런타임 예외 롤백

런타임 예외를 던져서 롤백할 것이다. 그래서 사용자 지정 예외(ZzimkongException)를 런타임으로 하는 것이다.

 

2) 체크 예외 커밋

트랜잭션에서 체크 예외가 발생하면 커밋을 해야한다. 예외가 일어났는데 왜 커밋하나 알아본다.

 

3) rollbackFor

체크 예외지만 커밋하는 경우를 보자

 

- 결과

1) 런타임 예외 호출

런타임 예외는 롤백된다.

 

2) 체크 예외 호출

체크 예외에서는 커밋한다.

 

3) rollbackFor

어떤 체크 예외는 롤백하고 싶다.

 

- 예외, 트랜잭션 커밋 롤백 활용

스프링의 가정이지 꼭 따를 필요는 없다.

 

→ 일반적인 자바 예외 활용

체크 : 비즈니스 적으로 중요해서 개발자에게 명시적으로 알리는 예외, 소프트웨어 적으로 처리할 수 없는 OOM, 네트워크 문제(SQL 예외)
언체크 : 소프트웨어적으로 해결할 수 있는 예외

 

-> 스프링 예외

언체크 : OOM, 네트워크 오류 등 복구 불가능한 예외로 공통 처리부에서 처리
체크 : 비즈니스 적으로 중요해서 커밋되는 예외

자바 예외와 다르게 언체크가 복구 불가능한 예외로 된다.

 

→ 비즈니스 요구사항

트랜잭션에서 예외가 발생하면 무조건 롤백인줄 알았는데 아니다.

 

1) 정상인 경우

고객이 결제를 성공하면 완료 화면을 보인다.

 

2) 시스템 예외

시스템 예외가 터져셔 복구 불가능하다면 전체 데이터를 롤백해야 한다. 시스템 복구가 안되는 예외가 터지면 절대로 해결할 수 없으니 런타임으로 처리한다.

 

3) 비즈니스 예외

결제 부족 시 결제 상태를 대기로 저장하고 커밋한다. 고객에게 잔고 부족을 알리는 로직을 controlladvice에 공통 처리한다.

 

→ 정리

잔고가 부족하면 비즈니스 상황에서 문제가 된다. 이는 체크로 잡아야 하고 커밋해야 하는 예를 이해하기 위한 가정이다.

비즈니스 예외는 개발자에게 알리고 서비스에서 잡는 것이 동일하다.

 

 

예외 : 시스템 예외는 복구 불가능하여 롤백
잔고 부족 : 비즈니스 예외는 상태를 대기로 바꾸고 커밋 후 고객에게 잔고 부족 알림

 

→ 테스트

1) 시스템 오류

서비스 이름 예외라면,

 

롤백이 일어난다.

 

2) 비즈니스 오류

잔고 부족으로 체크 예외가 터져야 하고 이를 고객에게 알리는 것을 controlladvice에 작성한다.

 

커밋이된다. 따라서 jpa를 사용하기 때문에 insert, update도 일어난다.

 

- 정리

체크 예외를 통해 비즈 문제 상황을 알려준 것이라고 할 수 있다. 예외를 던져서 서비스를 호출한 측에서 잡으라고 예외를 알려준거다.

 

- 스프링 트랜잭션 전파

스프링에서 트랜잭션이 작동하고 있는 중에 새로운 트랜잭션이 발생하는 상황을 알아보자

 

커밋 : 커밋하고 커낵션 반납
롤백 : 롤백하고 커낵션 반납

기본은 커밋이나 롤백을 하면 커낵션을 반납하고 끝낸다.

 

- 트랜잭션 두 번 사용

이 예제는 트랜잭션이 각각 따로 사용되는 경우이다. 트랜1이 완전히 끝나고 2가 수행된다.  트랜잭션 1에서 시작하고 커밋하고 2에서 시작하고 커밋하는 경우이다.

 

- 정리

두 트랜잭션에서는 다른 커낵션이 사용된다.

 

두 트랜잭션은 아예 별도이 트랜잭션이고 전혀 관련이 없기에 다른 커낵션을 사용한다. 1에서 커낵션을 생성하고 커밋하고 반납하고 2에서도 동일하게 동작한다.

 

- 전파

지금까지는 트랜잭션을 각각 사용하여 전혀 관련이 없다. 트랜잭션을 이미 하고 있는데 또 트랜잭션을 하면 어떻게 될까? 이미 하고 있는 트랜잭션에서 커밋을 하지 않았는데 또 트랜잭션이 중첩해서 시작해버린 것이다.

 

스프링은 이 경우 외부와 내부를 묶어서 하나로 만들고 내부가 외부에 참여하는 것이 기본 옵션이다.

 

→ 왜 나눌까?

트랜잭션이 사용중일 때 다른 트랜잭션이 발생하면 복잡한 상황이 발생한다. 내부에서 커밋하면 외부에서도 커밋하나? 롤백하나? 등등이다.

 

→ 대원칙

1) 모든 논리 트랜잭션이 커밋해야 외부 트랜잭션도 커밋
2) 하나라도 롤백하면 전체가 롤백

트랜잭션 안에서 또 시작하면 전체가 물리로 묶이지만, 내부는 트랜잭션이 생기고, 이게 커밋되려면 전부 다 커밋되어야 하고 하나라도 롤백하면 전체가 다 롤백된다.

 

모든 논리 트랜잭션이 커밋하면 전체가 커밋하고

 

하나라도 롤백하면 전체가 롤백한다.

 

- 전파 예제

외부 트랜잭션이 시작했는데 내부에서 또 만들어지고 둘 다 커밋하여 전체가 커밋되는 상황을 알아보자

 

→ 참여란?

내부가 외부에 참여한다. 참여란, 내부가 외부에 묶이는 것이다.

 

이 경우, 트랜잭션 매니저는 해당 트랜잭션이 신규인지 아닌지 확인한다. 외부 트랜잭션은 신규라서 isNew가 true이다. 내부 트랜잭션이 시자가면 participating으로 외부 트랜잭션에 참여하고 new가 false이다. 내부 트랜잭션이 커밋하면 아무일도 발생하지 않고, 외부 트랜잭션이 커밋하면 커밋되고 트랜잭션이 릴리즈된다.

 

원래는 커밋하면 로그가 남았는데, 내부는 무시하고 외부에서 커밋하면 끝난다. 커밋이나 롤백하면 아예 커낵션을 끊어버리기 때문에 참여하는 내부 트랜잭션은 물리 커낵션에 대한 작업을 아예 하지 못하고 넘어간다. 실행할 때도 물리 트랜잭션은 커낵션을 얻는 것을 안해도 된다. 외부 커밋을 하면 실제 커밋이 되고 커낵션을 돌려준다. 외부 트랜잭션만 시작하고, 커밋한다. 스프링은 여러 트랜잭션을 함께 사용할 경우 외부 트랜잭션이 물리 트랜을 관리한다.

 

- 동작 과정

→ 요청 흐름

 

1. 외부 요청 흐름

트랜잭션 매니저가 신규인지 확인하고 신규이면 외부 트랜잭션으로 파악하고 동기화 매니저에 데이터 소스로 커낵션을 만들고 넣어놓는다.

이때 isNew 여부가 담겨있으면 신규 트랜잭션으로 파악한다.

 

2. 내부 요청 흐름

외부를 하던 도중 커밋, 롤백 안하고 내부에서 트랜잭션을 시작한다. 동기화 매니저를 통해 이미 기존 트랜잭션이 있는지 확인한다. 외부에서도 이런 과정을 거치는데 동기화에 가보니 커낵션이 없어서 새로 만든다. 내부에서는 동기화에 가보니 커낵션이 있어서 기존 트랜잭션에 참여한다. 참여한다는 것은 아무것도 하지 않겠다는 의미로, 커낵션 획득도 안하고 커밋도 안 한다.

 

→ 응답 흐름

 

 

1. 내부 응답 흐름

내부에서 로직이 끝나고 커밋을 호출하면 신규 트랜잭션 여부에 다라서 다르게 동작하는데, 이 경우 신규 트랜잭션이 아니라서 커밋 호출을 안한다. 내부는 아무것도 안 하면 되어서 로그도 남지 않는다.

 

2. 외부 응답 흐름

외부 로직이 끝나고 외부 트랜잭션을 커밋한다. 트랜잭션 매니저는 커밋 시점에 신규 여부에 따라 다르게 동작한다고 했는데 신규라서 실제 커밋을 한다. 실제 커밋 코드는 2번이지만 한 번만 발생하여 참여가 가능하다.

 

- 핵심 정리

신규가 아닌 내부 트랜잭션은 물리 트랜잭션에 영향을 주면 안된다. 내부에서 커밋이나 롤백을 하면 커낵션을 반납해서 새로운 트랜잭션이 시작되기 때문에, 내부는 아무것도 안하고 종료한다. 트랜잭션 매니저에 커밋을 호출한다고 해서 항상 실제 커밋이 발생하는 것은 아니다. 외부 커낵션만 커밋할 수 있다. 신규 트랜잭션이 아니면 실제 물리 커낵션을 사용할 수 없다.

 

- 외부 롤백

내부는 커밋하는데 외부에서는 롤백하면 대원칙에 따라서 하나라도 롤백하면 전체가 다 롤백한다. 내부 트랜이 커밋해도 전체가 다 롤백한다.

 

내부에서는 커밋하고 외부에서 롤백하는 상황이다.

 

내부 트랜잭션이 시작하고 신규가 아니라서 아무일도 일어나지 않고 외부 트랜잭션 롤백에서 롤백한다. 이때도 역시 트랜잭션 매니저에서 신규 트랜잭션인지 확인하고 동작한다.

 

- 내부 롤백

근데 내부 롤백은 다르게 동작한다. 지금까지 외부 트랜만 영향을 줬는데, 내부에 의해 전체가 롤백해야 하는 상황이다.

 

외부는 커밋하는데 내부는 롤백하면

내부에서 롤백하고 외부에서 커밋할 때 롤백 마크가 되어있다는 예외가 발생한다.

 

내부에서 롤백할 때, 원래는 내부 트랜잭션이 물리 트랜잭션에 영향을 주면 안되어서 아무것도 안했지만, 이 경우 대원칙에 따라서 논리 트랜이 하나라도 롤백하면 전체가 롤백해야 하기 때문에 롤백 마크를 남긴다.

 

1. 내부 롤백 응답

동일하게 내부에서 롤백하려고 하면 신규인지 체크하고 신규가 아니라서 롤백은 못하지만 롤백 마크를 한다.

 

2. 외부 커밋 응답

역시 신규 여부를 보는데 신규라서 커밋한다. 근데 마크를 확인했더니 롤백이 되어있어서 전체 물리 트랜잭션을 롤백한다.

 

- REQUIRED_NEW

내부 롤백, 외부 커밋하는 상황에서 내부만 롤백하고 외부는 커밋하는 둘이 마치 다른 트랜잭션처럼 동작하는 법을 알아보자. 내부 트랜잭션에 문제가 발생해서 롤백해도 외부에 영향을 주지 않고 외부도 내부에 영향을 주지 않는다.

 

외부 트랜잭션은 커밋하고 내부에서만 롤백하고 싶은 경우가 있을 수도 있다. 이때 내부 트랜잭션을 완전히 별도의 트랜잭션처럼 만드는 옵션이 있다. 트랜잭션에 new 옵션을 붙이면 외부와 별도의 커낵션을 따로 사용하며, 서로 영향을 주지 않고 대원칙을 따르지 않는다.

 

→ 실행 로그

외부는 신규니깐 새로운 커낵션이 true이다. 내부를 시작하면 suspending으로 현재 트랜잭션을 잠시 미루고 새 커낵션을 획득한다. 이 내부 트랜잭션은 완전히 새로운 영역이고 새로운 커낵션에서 동작한다. 그러다, 내부에서 롤백하면 실제 롤백이 일어난다. 원래는 내부 트랜잭션이 물리에 영향을 주지 못했는데 내부 트랜잭션이 별도로 생기는 것이라서 롤백하고 커낵션을 반납한다.

 

- 실무에서 많이하는 실수

로그 레포지토리에서 예외가 발생해서 롤백을 할 때, 서비스에서 예외를 잡으면 전체 롤백은 안되지 않을 거라 생각합니다. 하지만 이 방법은 실패한다. 롤백 only 마크가 달리면 무조건 물리 트랜잭션이 롤백한다. UnexpectedRollbackException이 발생한다. 개발하다가 갑자기 UnexpectedRollbackException이 나오면 어딘가 롤백을 했구나를 알아야 한다.

 

-> 해결하는 방법

만약 동일 트랜잭션 내부에서 로그는 롤백되는데, 회원은 롤백 안되게 하고 싶으면 로그 레포지토리 트랜잭션에 REQUIRES_NEW를 넣으면 된다. 트랜잭션을 분리해야 한다.

Comments