Post

Spring DB 2

트랜잭션 전파(propagation)

  • 트랜잭션 안에 새로운 트랜잭션이 들어올 때 어떻게 동작할지를 결정하는 것을 트랜잭션 전파라고 한다.

  • 스프링에서는 기본 옵션은 REQUIRED로 되어있다. 그 외에도 다양한 옵션들이 존재한다.

REQUIRED

  • 만약 트랜잭션 1이 먼저 실행 중일 때 새로운 트랜잭션 2가 들어오면 REQUIRED 옵션은 하나의 트랜잭션 묶어준다. 트랜잭션 2가 트랜잭션 1로 참여하는 것이다.

    image

  • 앞으로 설명을 위해 처음 시작 트랜잭션을 시작한 트랜잭션 1을 외부 트랜잭션, 도중에 참여한 트랜잭션 2를 내부 트랜잭션이라고 칭하겠다.

논리 트랜잭션과 물리 트랜잭션

image

  • 논리 트랜잭션 : 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위

  • 물리 트랜잭션 : 실제 데이터 베이스에 적용되는 트랜잭션

  • 이런 단위를 나눔으로써 트랜잭션 전파 상황에 대해 더 구분이 편하다.

  • 이런 개념들을 도입해 2가지 원칙 새우는 것이 가능하다.

    • 모든 논리 트랜잭션이 커밋되어어야 물리 트랜잭션이 커밋된다.
    • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.

    image

  • 여기서 주의할 점은 처음 트랜잭션을 시작한 트랜잭션이 실제 물리 트랜잭션을 관리한다. 이를 통해 중복 커밋 문제를 해결한다.

    • 요청 흐름

    image

    • 여기서 잘 살펴볼 부분은 외부 트랜잭션에서 트랜잭션 동기화 매니저에 커넥션을 보관할 때이다.

    • 트랜잭션 매니저가 트랜잭션을 생성할 때 TransactionStatus에 트랜잭션 상태를 보관한다.

    • 여기에 만약 처음 트랜잭션을 시작한 외부 트랜잭션이면 isNewTransaction에 true, 참여한 내부 트랜잭션이면 false를 반환한다.

    • 응답 흐름

      image

    • 응답시에 로직 실행 후 논리 트랜잭션은 트랜잭션 매니저에 커밋을 호출한다.

    • 그때 isNewTransaction를 확인해 false면 물리 트랜잭션에 커밋 호출을 하지 않고, true일 때만 커밋을 호출하게 된다.

  • 이렇듯 트랜잭션 매니저에 커밋을 호출한다고 항상 실제 커넥션에 물리 커밋이 발생하지 않는다는 점이 핵심이다.

  • 신규 트랜잭션인 경우에만 실제 커넥션에 물리 커밋과 롤백을 수행한다.

  • 트랜잭션이 내부에서 추가로 사용된다면 트랜잭션 매니저를 통해 모든 논리 트랜잭션이 커밋이 되어야 실제 물리 트랜잭션, 실제 커넥션의 커밋이나 롤백이 호출된다고 생각하자.

  • 커밋 흐름과 롤백 흐름은 거의 비슷하다. 하지만 내부 트랜잭션이 롤백되었는데 외부 트랜잭션이 커밋된 경우에는 좀 더 살펴봐야할 부분이 있다.

  • 내부 - 롤백, 외부 - 커밋 상황

    image

    • 여기서 주의할 점은 내부 트랜잭션에서 롤백을 요청했다고 그 순간 트랜잭션이 끝나는 것이 아니다.
    • 아직 트랜잭션이 모두 끝난 것이 아니기 때문에 외부 트랜잭션까지 종료될 때까지 물리 트랜잭션은 이어져야한다.
    • 위 그림과 같이 내부 트랜잭션에서 롤백을 요청하면 트랜잭션 동기화 매니저에 rollbackOnly=true로 설정한다. 이를 통해 트랜잭션 매니저에게 롤백해야한다는 사실을 알려준다.
    • 또 외부 트랜잭션이 커밋할 때 롤백 마크가 되어있으면 트랜잭션을 롤백하고 UnexpectedRollbackException 예외를 던진다.
    • 예외를 던지는 이유 - 애플리케이션 개발에서 중요한 기본 원칙은 모호함을 제거하는 것이다. 이렇게 커밋을 호출했는데 롤백이 발생한 경우와 같이 기대한 결과와 실제 결과가 다를 경우 예외를 발생히켜 명확하게 문제를 알려주는 것이다.

REQUIRES_NEW

  • REQUIRES_NEW는 트랜잭션 참여시 새로운 물리 트랜잭션을 만들어 각각의 별도의 트랜잭션을 만들어주는 옵션이다.

    image

  • 요청 흐름

    image

  • 응답 흐름

    image

  • 위의 그림과 같이 물리 트랜잭션이 명확하게 분리되어 사용된다.

  • REQUIRES_NEW를 사용하면 DB 커넥션을 2개 사용하게 된다는 점을 주의하자.

트랜잭션 복구 전략

  • 만약 우리가 요청을 처리할 때 아래 그림과 같이 예외가 발생해도 복구해 정상 흐름으로 처리하고 싶다고 가정해보자.

    image

  • MemberService에서 try-catch를 이용해 예외를 잡고 정상흐름으로 바꾸려고 했다.

  • 하지만 우리의 예상과 달리 정상 흐름으로 변경이 안 되었다. 왜 이런 일이 발생할까?

  • 그 이유는 바로 논리 트랜잭션 C에서 rollbackOnly를 설정했기 때문이다.

    image

  • 이 그림처럼 논리 트랜잭션에서 commit을 했다고 해도 이미 rollbackOnly가 설정되어있기 때문에 트랜잭션 매니저는 롤백으로 처리하고 예외를 던지게 된다.

  • 만약 LogRepository에서 발생한 예외는 따로 처리하고 싶다면 코드 구조를 변경하거나 REQUIRES_NEW를 사용해 트랜잭션을 나누는 방법으로 처리해야한다.

  • REQUIRES_NEW - 주의) REQUIRES_NEW는 하나의 HTTP 요청에 커넥션 2개를 동시에 사용하므로 성능 이슈가 발생할 수 있음

    image

  • 구조 변경

    image-20240725002853180

This post is licensed under CC BY 4.0 by the author.