Spring DB 2
트랜잭션 전파(propagation)
트랜잭션 안에 새로운 트랜잭션이 들어올 때 어떻게 동작할지를 결정하는 것을 트랜잭션 전파라고 한다.
스프링에서는 기본 옵션은 REQUIRED로 되어있다. 그 외에도 다양한 옵션들이 존재한다.
REQUIRED
만약 트랜잭션 1이 먼저 실행 중일 때 새로운 트랜잭션 2가 들어오면 REQUIRED 옵션은 하나의 트랜잭션 묶어준다. 트랜잭션 2가 트랜잭션 1로 참여하는 것이다.
앞으로 설명을 위해 처음 시작 트랜잭션을 시작한 트랜잭션 1을 외부 트랜잭션, 도중에 참여한 트랜잭션 2를 내부 트랜잭션이라고 칭하겠다.
논리 트랜잭션과 물리 트랜잭션
논리 트랜잭션 : 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위
물리 트랜잭션 : 실제 데이터 베이스에 적용되는 트랜잭션
이런 단위를 나눔으로써 트랜잭션 전파 상황에 대해 더 구분이 편하다.
이런 개념들을 도입해 2가지 원칙 새우는 것이 가능하다.
- 모든 논리 트랜잭션이 커밋되어어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
여기서 주의할 점은 처음 트랜잭션을 시작한 트랜잭션이 실제 물리 트랜잭션을 관리한다. 이를 통해 중복 커밋 문제를 해결한다.
요청 흐름
여기서 잘 살펴볼 부분은 외부 트랜잭션에서 트랜잭션 동기화 매니저에 커넥션을 보관할 때이다.
트랜잭션 매니저가 트랜잭션을 생성할 때 TransactionStatus에 트랜잭션 상태를 보관한다.
여기에 만약 처음 트랜잭션을 시작한 외부 트랜잭션이면 isNewTransaction에 true, 참여한 내부 트랜잭션이면 false를 반환한다.
응답 흐름
응답시에 로직 실행 후 논리 트랜잭션은 트랜잭션 매니저에 커밋을 호출한다.
그때
isNewTransaction
를 확인해 false면 물리 트랜잭션에 커밋 호출을 하지 않고, true일 때만 커밋을 호출하게 된다.
이렇듯 트랜잭션 매니저에 커밋을 호출한다고 항상 실제 커넥션에 물리 커밋이 발생하지 않는다는 점이 핵심이다.
신규 트랜잭션인 경우에만 실제 커넥션에 물리 커밋과 롤백을 수행한다.
트랜잭션이 내부에서 추가로 사용된다면 트랜잭션 매니저를 통해 모든 논리 트랜잭션이 커밋이 되어야 실제 물리 트랜잭션, 실제 커넥션의 커밋이나 롤백이 호출된다고 생각하자.
커밋 흐름과 롤백 흐름은 거의 비슷하다. 하지만 내부 트랜잭션이 롤백되었는데 외부 트랜잭션이 커밋된 경우에는 좀 더 살펴봐야할 부분이 있다.
내부 - 롤백, 외부 - 커밋 상황
- 여기서 주의할 점은 내부 트랜잭션에서 롤백을 요청했다고 그 순간 트랜잭션이 끝나는 것이 아니다.
- 아직 트랜잭션이 모두 끝난 것이 아니기 때문에 외부 트랜잭션까지 종료될 때까지
물리 트랜잭션은 이어져야한다.
- 위 그림과 같이 내부 트랜잭션에서 롤백을 요청하면 트랜잭션 동기화 매니저에
rollbackOnly=true
로 설정한다. 이를 통해 트랜잭션 매니저에게 롤백해야한다는 사실을 알려준다. - 또 외부 트랜잭션이 커밋할 때 롤백 마크가 되어있으면 트랜잭션을 롤백하고
UnexpectedRollbackException
예외를 던진다. 예외를 던지는 이유
- 애플리케이션 개발에서 중요한 기본 원칙은 모호함을 제거하는 것이다. 이렇게 커밋을 호출했는데 롤백이 발생한 경우와 같이 기대한 결과와 실제 결과가 다를 경우 예외를 발생히켜 명확하게 문제를 알려주는 것이다.
REQUIRES_NEW
REQUIRES_NEW는 트랜잭션 참여시 새로운 물리 트랜잭션을 만들어 각각의 별도의 트랜잭션을 만들어주는 옵션이다.
요청 흐름
응답 흐름
위의 그림과 같이 물리 트랜잭션이 명확하게 분리되어 사용된다.
REQUIRES_NEW를 사용하면 DB 커넥션을 2개 사용하게 된다는 점을 주의하자.
트랜잭션 복구 전략
만약 우리가 요청을 처리할 때 아래 그림과 같이 예외가 발생해도 복구해 정상 흐름으로 처리하고 싶다고 가정해보자.
MemberService에서 try-catch를 이용해 예외를 잡고 정상흐름으로 바꾸려고 했다.
하지만 우리의 예상과 달리 정상 흐름으로 변경이 안 되었다. 왜 이런 일이 발생할까?
그 이유는 바로 논리 트랜잭션 C에서 rollbackOnly를 설정했기 때문이다.
이 그림처럼 논리 트랜잭션에서 commit을 했다고 해도 이미 rollbackOnly가 설정되어있기 때문에 트랜잭션 매니저는 롤백으로 처리하고 예외를 던지게 된다.
만약 LogRepository에서 발생한 예외는 따로 처리하고 싶다면 코드 구조를 변경하거나 REQUIRES_NEW를 사용해 트랜잭션을 나누는 방법으로 처리해야한다.
REQUIRES_NEW
- 주의) REQUIRES_NEW는 하나의 HTTP 요청에 커넥션 2개를 동시에 사용하므로 성능 이슈가 발생할 수 있음구조 변경