스프링 트랜잭션 전파

Lilac-_-P·2023년 5월 16일
0

스프링 DB

목록 보기
8/9

이전 글에서 스프링이 제공하는 스프링 트랜잭션이 어떻게 동작하는지에 대해 정리해보았다.

스프링 트랜잭션(이하 트랜잭션으로 통칭)을 독립적으로 각각 사용하면, 트랜잭션 별로 처리되어 트랜잭션 내의 동작에 따라 예외가 발생시에는 롤백하고, 문제가 없을시에는 커밋할 것이다. 그런데 만약에 트랜잭션을 독립적으로 각각 사용하는 것이 아니라, 한 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행하면 어떻게 될까?

즉, @Transactional이 붙은 메서드를 실행중이었는데, 메서드 실행중 내부에서 @Transactional이 붙은또다른 메서드를 실행하면 어떻게 될까?

기존의 트랜잭션과 별도의 트랜잭션을 진행해야할까? 아니면 기존 트랜잭션을 그대로 이어 받아서 트랜잭션을 수행해야할까? 이런 경우 어떻게 동작할지 결정하는 것을 트랜잭션 전파라 한다. 스프링 트랜잭션에서는 개발자가 이런 트랜잭션 전파 상황에서 동작방식을 결정할 수 있도록 다양한 트랜잭션 전파 옵션을 제공한다.

아래에서 설명할 트랜잭션 전파의 내용들은 트랜잭션 전파의 기본 옵션인 REQUIRED를 기준으로 설명한다.

스프링이 제공하는 트랜잭션 전파

트랜잭션 전파를 이해하기 위해서는 트랜잭션을 구분하기 위해 사용하는 몇가지 용어에 대한 이해가 필요하다.

  • 외부 트랜잭션과 내부 트랜잭션
  • 물리 트랜잭션과 논리 트랜잭션

외부 트랜잭션과 내부 트랜잭션

트랜잭션 전파는 여러 트랜잭션이 독립적으로 수행되는 경우에는 고려할 필요가 없다.

한 트랜잭션이 수행되는 도중 또다른 트랜잭션이 시작되는 경우에 트랜잭션 전파를 고려해야하는 것이다. 여기서 먼저 시작된 트랜잭션을 외부 트랜잭션, 외부 트랜잭션이 수행되던 도중 추가로 수행되는 트랜잭션을 내부 트랜잭션이라고 한다.

외부 트랜잭션은 상대적으로 밖에 있기 때문에 외부 트랜잭션이라 부르고, 처음 시작된 트랜잭션으로 이해하면 된다. 내부 트랜잭션은 외부에 트랜잭션이 수행되고 있는 도중에 호출되기 때문에 마치 내부에 있는 것처럼 보여서 내부 트랜잭션이라 한다.

메서드 호출과 비슷하게 생각하면 된다. 어떤 메서드가 실행되던 도중 내부에서 또 다른 메서드가 호출되면, 일반적으로 호출된 메서드를 내부 메서드라고 부르는 것을 떠올려보면 된다.

스프링은 트랜잭션 전파상황에서 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션으로 만들어준다(REQUIRED 기준). 즉, 내부 트래잭션이 외부 트랜잭션에 참여하는 것이다.

물리 트랜잭션과 내부 트랜잭션

스프링은 이해를 돕기 위해 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눈다.

논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이고, 물리 트랜잭션은 실제 데이터베이스에 적용되는 트랜잭션으로 실제 DB 커넥션을 통해서 트랜잭션을 시작하고, 실제 커넥션을 통해서 커밋, 롤백하는 단위이다.

여러 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다. 이런 논리 트랜잭션 개념은 트랜잭션이 진행되는 도중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타난다. 즉, 위에서 설명한 외부 트랜잭션과 내부 트랜잭션의 상황에 적용되는 것이다.

단지 외부 트랜잭션과 내부 트랜잭션은 트랜잭션이 시작된 순서를 기준으로 분류를 하는 것이고, 스프링은 트랜잭션 전파상황에서 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션으로 만들어주기 때문에 이 하나로 묶은 트랜잭션을 물리 트랜잭션으로, 묶은 트랜잭션를 이루는 개별적인 트랜잭션을 논리 트랜잭션으로 구분하는 것이다.

그럼 왜 굳이 이렇게 논리 트랜잭션과 물리 트랜잭션으로 구분을 해야하는 걸까?
트랜잭션이 사용중일 때 또 다른 트랜잭션이 내부에서 사용되면 여러가지 복잡한 상황이 발생한다. 이때 논리 트랜잭션 개념을 도입하면 다음과 같은 단순한 원칙을 만들 수 있기 때문이다.

트랜잭션 전파 처리 원칙

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

그림을 통해 확인하면 더 이해하기 쉽다.

Case 1 - 모든 논리 트랜잭션이 커밋되어야, 물리 트랜잭션이 커밋된다.


Case 2 - 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.(외부 트랜잭션이 롤백됌)


Case 3 - 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.(내부 트랜잭션이 롤백됌)


트랜잭션 전파 과정 - Case 1 (외부, 내부 모두 커밋)

  1. 외부 트랜잭션은 처음 수행된 트랜잭션이기 때문에, 신규 트랜잭션(isNewTransaction=true)이 된다.
  2. 내부 트랜잭션을 시작하는 시점에 이미 외부 트랜잭션이 진행중인 상태이므로 내부 트랜잭션은 외부 트랜잭션에 참여한다. 즉, 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻이다.
  3. 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여하므로 신규 트랜잭션(isNewTransaction=false)이 아니다.
  4. 트랜잭션 내에서 모든 동작이 수행된 후, 내부 트랜잭션이 먼저 커밋하고, 외부 트랜잭션도 커밋한다.
  5. 외부 트랜잭션이 커밋되었기 때문에, 실제 물리 트랜잭션 또한 커밋된다.

트랜잭션 전파를 처리하는 원칙이 위에서 설명한 바와 같이 정해져있기 때문에, 코드 상에서 외부 트랜잭션과 내부 트랜잭션이 각각 트랜잭션 매니저를 통해 getTransaction(), commit(), rollback() 메서드를 호출하지만, 외부 트랜잭션만 실제 물리 트랜잭션을 시작하고, 커밋 또는 롤백한다.

만약 내부 트랜잭션이 호출한 commit() 메서드에 의해 실제 물리 트랜잭션이 커밋된다면 트랜잭션이 끝나버리기 때문에, 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없다. 따라서 내부 트랜잭션은 DB 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다.

스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다. 이를 통해 코드상의 트랜잭션 중복 커밋 문제를 해결한다.

참고.
트랜잭션 전파 과정에서 외부와 내부 트랜잭션은 모두 동일한 트랜잭션 매니저와 트랜잭션 동기화 매니저를 사용하기 때문에, 동일한 DB 커넥션을 사용함을 보장받는다. 또, isNewTransaction 의 값을 통해, 실제 물리 트랜잭션을 커밋 또는 롤백을 할지를 결정할 수 있다. 즉, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.

트랜잭션 전파 과정 - Case 2 (외부 롤백, 내부 커밋)

논리 트랜잭션이 하나라도 롤백되면 전체 물리 트랜잭션을 롤백된다.

  1. 외부 트랜잭션은 처음 수행된 트랜잭션이기 때문에, 신규 트랜잭션(isNewTransaction=true)이 된다.
  2. 내부 트랜잭션을 시작하는 시점에 이미 외부 트랜잭션이 진행중인 상태이므로 내부 트랜잭션은 외부 트랜잭션에 참여한다. 즉, 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻이다.
  3. 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여하므로 신규 트랜잭션(isNewTransaction=false)이 아니다.
  4. 트랜잭션 내에서 모든 동작이 수행된 후, 내부 트랜잭션이 먼저 커밋하지만, 외부 트랜잭션은 롤백한다.
  5. 논리 트랜잭션이 하나라도 롤백되면, 실제 물리 트랜잭션 또한 롤백된다.

트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 외부 트랜잭션은 신규 트랜잭션(isNewTransaction=true)이다. 따라서 DB 커넥션에 실제 롤백을 호출하여 실제 물리 트랜잭션은 롤백된다.

트랜잭션 전파 과정 - Case 3 (외부 커밋, 내부 롤백)

  1. 외부 트랜잭션은 처음 수행된 트랜잭션이기 때문에, 신규 트랜잭션(isNewTransaction=true)이 된다.
  2. 내부 트랜잭션을 시작하는 시점에 이미 외부 트랜잭션이 진행중인 상태이므로 내부 트랜잭션은 외부 트랜잭션에 참여한다. 즉, 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻이다.
  3. 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여하므로 신규 트랜잭션(isNewTransaction=false)이 아니다.
  4. 트랜잭션 내에서 모든 동작이 수행된 후, 내부 트랜잭션이 먼저 롤백된다.
  5. 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 내부 트랜잭션은 신규 트랜잭션이 아니므로 실제 물리 트랜잭션을 롤백하지않는다. 대신, 트랜잭션 동기화 매니저에 rollbackOnly=true 라는 표시를 해둔다.
  6. 그 후, 외부 트랜잭션이 커밋한다.
  7. 트랜잭션 매니저는 외부 트랜잭션이 신규 트랜잭션이므로 DB 커넥션에 실제 커밋을 호출해야하지만, 그 전에 트랜잭션 동기화 매니저에 롤백전용(rollbackOnly) 표시가 있는지 확인한다.
  8. 롤백전용 표시가 있다면, 물리 트랜잭션을 커밋하는 것이 아니라 롤백한다. DB에도 롤백이 반영된다.
  9. 트랜잭션 매니저에 커밋을 호출한 개발자 입장에서는 분명 커밋을 기대했는데, 롤백 전용 표시로 인해 실제로는 롤백이 되버렸다. 시스템 입장에서는 '커밋을 호출했지만 롤백이 되었다' 는 것을 분명하게 알려주어야한다.
  10. 스프링은 이 경우 UnexpectedRollbackException 런타임 예외를 던진다. 그래서 커밋을 시도했지만, 기대하지 않은 롤백인 발생했다는 것을 명확하게 알려준다.

결국, 논리 트랜잭션이 하나라도 롤백되었기 때문에 전체 물리 트랜잭션도 롤백되었다.

내부 트랜잭션이 롤백을 했지만, 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않는다. 그런데 여기서 외부 트랜잭션은 커밋을 해버린다. 외부 트랜잭션만이 실제 물리 트랜잭션에 영향을 주기 때문에, 스프링은 이를 해결하기 위한 방법으로 rollbackOnly라는 추가적인 값을 사용한다. 그리고 이렇게 롤백이 된 경우 런타임 예외를 던져서 이를 분명하게 알려준다.


트랜잭션 분리

지금까지는 트랜잭션이 전파되는 경우에 대해서 알아보았다. 그렇다면 의도적으로 외부 트랜잭션과 내부 트랜잭션을 분리해서 사용하려면 어떻게 해야할까? 스프링은 이를 위한 방법 또한 제공한다.

바로 트랜잭션 전파 옵션을 REQUIRES_NEW 로 사용하는 것이다.

이전까지의 모든 트랜잭션 전파는 스프링의 트랜잭션 전파 옵션의 기본값인 REQUIRED을 기준으로 동작했다. 하지만, 트랜잭션 전파 옵션을 REQUIRES_NEW로 사용하면, 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용한다. 그러므로 커밋과 롤백도 각각 별도로 이루어지게 된다. 외부 트랜잭션과 내부 트랜잭션은 서로에게 아무런 영향을 주지 않는다. 아래의 그림을 참고하자.

참고.
별도의 물리 트랜잭션을 사용한다는 말은 곧 DB 커넥션을 따로 사용한다는 뜻이다.

REQUIRES_NEW를 통한 트랜잭션 분리 흐름

  1. 트랜잭션 매니저를 통해 외부 트랜잭션을 시작한다.(커넥션 생성 및 수동 커밋 모드 설정)
  2. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 트랜잭션을 보관한다.
  3. 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TranactionStatus에 담아서 반환하는데, 여기에 신규 트랜잭션의 여부가 담겨있다. 트랜잭션을 처음 시작했으므로 신규 트랜잭션이다.(isNewTransaction=true)
  4. 외부 트랜잭션에 속하는 로직이 수행된다.
  5. 외부 트랜잭션에 속하는 로직이 수행되던 중 트랜잭션 매니저를 통해 내부 트랜잭션이 REQUIRES_NEW로 시작된다.
  6. 트랜잭션 매니저는 REQUIRES_NEW 옵션을 확인하고, 기존 트랜잭션에 참여하지 않고 새로운 트랜잭션을 시작한다.(새로운 커넥션 생성 및 수동 커밋 모드 설정)
  7. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션을 보관한다.
    • 이때, 이미 보관되고 있던 외부 트랜잭션의 커넥션은 잠시 보류되고, 내부 트랜잭션의 커넥션이 사용된다.
  8. 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TranactionStatus에 담아서 반환한다.(내부 트랜잭션도 신규 트랜잭션)
  9. 내부 트랜잭션에 속하는 로직이 모두 수행된 후, 내부 트랜잭션을 커밋하거나 롤백한다.
  10. 내부 트랜잭션이 사용하던 커넥션은 종료되거나 커넥션 풀에 반환되고, 보류중이던 외부 트랜잭션의 커넥션이 다시 사용된다.
  11. 외부 트랜잭션의 로직이 모두 수행되고, 외부 트랜잭션 또한 커밋하거나 롤백한다.
  12. 외부 트랜잭션이 사용하던 커넥션도 종료되거나 커넥션 풀에 반환된다.

즉, REQUIRES_NEW 옵션을 사용하면 물리 트랜잭션이 명확하게 분리된다. 또한, 데이터베이스 커넥션이 동시에 2개가 사용된다는 점을 주의해야한다.

참고.
REQUIRES_NEW 옵션에 의해 동시에 사용되는 여러 개의 데이터베이스 커넥션은 각각 서로 다른 쓰레드에 의해 사용되는 것이 아니다. 위의 과정에서도 설명했지만, 외부 트랜잭션에서 사용되던 커넥션은 내부 트랜잭션이 시작될 때 트랜잭션 동기화 매니저에 의해 잠시 보류된다. 외부 트랜잭션과 내부 트랜잭션 모두 하나의 쓰레드에 의해서 동작하고, 정확한 원리는 모르지만 트랜잭션 동기화 매니저 내부에서 ThreadLocal을 이용해서 각 트랜잭션에 맞는 커넥션을 사용할 수 있도록 이를 관리할 것으로 추측된다.

스프링 트랜잭션 전파 옵션

스프링은 다양한 트랜잭션 전파 옵션을 제공한다. 별도의 설정을 하지 않으면 REQUIRED가 기본으로 사용된다. 옵션들의 종류는 다음과 같다.

  • REQUIRED : 트랜잭션이 필수(없으면 만들고, 있으면 참여한다.)
    • 기존 트랜잭션 없음 : 새로운 트랜잭션 생성
    • 기존 트랜잭션 있음 : 기존 트랜잭션에 참여
  • REQUIRES_NEW : 항상 새로운 트랜잭션 생성
    • 기존 트랜잭션 없음 : 새로운 트랜잭션 생성
    • 기존 트랜잭션 있음 : 새로운 트랜잭션 생성
  • SUPPORT : 트랜잭션을 지원(기존 트랜잭션이 없으면 없는대로 진행, 있으면 참여)
    • 기존 트랜잭션 없음 : 트랜잭션 없이 진행
    • 기존 트랜잭션 있음 : 기존 트랜잭션에 참여
  • NOT_SUPPORT : 트랜잭션을 지원하지 않음
    • 기존 트랜잭션 없음 : 트랜잭션 없이 진행
    • 기존 트랜잭션 있음 : 트랜잭션 없이 진행(! 기존 트랜잭션은 보류한다.)
  • MANDATORY : 트랜잭션이 의무 사항(트랜잭션이 반드시 있어야한다. 기존 트랜잭션이 없으면 예외 발생)
    • 기존 트랜잭션 없음 : 기존 트랜잭션이 없으면 예외가 발생(IllegalTransactionStateException)
    • 기존 트랜잭션 있음 : 기존 트랜잭션에 참여
  • NEVER : 트랜잭션을 사용하지 않음(기존 트랜잭션도 허용하지 않는 강한 부정의 의미)
    • 기존 트랜잭션 없음 : 트랜잭션 없이 진행
    • 기존 트랜잭션 있음 : 기존 트랜잭션이 있으면 예외가 발생(IllegalTransactionStateException)
  • NESTED
    • 기존 트랜잭션 없음 : 새로운 트랜잭션 생성
    • 기존 트랜잭션 있음 : 중첩 트랜잭션을 생성한다.
      • 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만, 중첩 트랜잭션은 외부에 영향을 주지 않는다.
      • 중첩 트랜잭션(위치상 내부 트랜잭션)이 롤백되어도 외부 트랜잭션은 커밋할 수 있다.
      • 외부 트랜잭션이 롤백되면 중첩 트랜잭션도 함께 롤백된다.

참고.
트랜잭션 전파 옵션의 isolation, timeout, readOnly 는 트랜잭션이 처음 시작될 때만 적용된다. 트랜잭션에 참여하는 경우에는 기존의 것을 받아서 사용하기 때문에 적용되지 않는다.

profile
열심히 하자

0개의 댓글