ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 스프링 트랜잭션 전파
    김영한의 스프링/데이터베이스 2024. 10. 4. 14:16

    트랜잭션 전파는 이미 기존에 실행 중인 트랜잭션에 추가로 트랜잭션이 수행하면 어떻게 동작할지 결정하는 것이다. 예시를 통해 트랜잭션 전파에서 사용되는 개념을 천천히 알아보자! (참고! 지금부터 사용하는 트랜잭션 전파의 옵션은 기본 옵션인 REQUIRED 기준으로 설명된다! -> 이해가 안된다면 그냥 넘어가자 ㅎㅎ)

     

    트랜잭션 전파와 관련된 여러가지 개념

    현재 상황은 기존에 실행 중인 트랜잭션이 있는데 갑자기 추가로 다른 트랜잭션이 생긴 과정이다. 이때 기존에 실행되고 있는 트랜잭션을 외부 트랜잭션이라고 부른다. 그에 반에 추가로 수행되는 트랜잭션은 내부 트랜잭션이라고 부른다. 이름이 이렇게 붙여진 이유를 생각하자면 처음 실행된 트랜잭션은 둘 중 상대적으로 밖에 위치 하고 있기에 외부 트랜잭션이라고 부르고 추가로 시작된 트랜잭션은 마치 기존 트랜잭션의 안에 참가하는 느낌이 있기에 내부 트랜잭션이라고 부른다고 생각하면 편하다.

     

    스프링에서 이 경우 다음과 같이 외부 트랜잭션과 내부 트랜잭션을 하나로 묶어서 하나의 트랜잭션을 만들어준다. 

     

    여기서 스프링은 트랜잭션 전파를 더 이해를 잘 돕기 위해 물리 트랜잭션논리 트랜잭션 개념으로 나눈다. 스프링은 외부 트랜잭션과 내부 트랜잭션을 하나로 묶는다고 하였는데 하나로 묶은 것이 물리 트랜잭션, 각각 외부 내부 트랜잭션을 논리 트랜잭션이라고 부른다. 즉, 논리 트랜잭션은 하나의 물리 트랜잭션으로 묶은다. 물리 트랜잭션은 실제 데이터 베이스에 적용되는 트랜잭션을 뜻하고, 실제 커넥션을 통해서 트랜잭션을 시작, 커밋, 롤백을 하는 단위이다. 쉽게 생각하면 여러 개의 논리 트랜잭션이 하나의 물리 트랜잭션으로 묶이면서 하나의 같은 커넥션을 사용한다고 생각하면 좋다!

    이러한 논리 트랜잭션의 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 겨웅에 나타난다. 즉, 단순히 트랜잭션이 하나만 돌아가는 경우에는 물리, 논리 둘로 구분하지 않는다.

     

    이렇게 스프링은 두 가지 개념으로 트랜잭션 전파를 나눴는데 그 이유는 여러 가지 복합적인 트랜잭션 상황에서 어떻게 흘러갈지 대원칙을 정하기위해서이다.

     

    원칙(중요!!!!)

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

    크게 3가지 상황으로 구분할 수 있다. 1) 모든 논리 트랜잭션이 커밋되어서 물리 트랜잭션이 커밋되는 경우 2) 내부 트랜잭션 중 하나 이상이 롤백되어서 물리 트랜잭션이 롤백되는 경우 3) 외부 트랜잭션이 롤백이 되어서 물리 트랜잭션이 롤백이 되는 경우. 이렇게 3가지로 나눌 수 있다. 이제부터 하나씩 살펴보자!

     

    1) 모든 논리 트랜잭션이 커밋되는 경우

    @Test
    void inner_commit(){
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction={}",outer.isNewTransaction());
        log.info("내부 트랜잭션 시작");
        TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction={}",inner.isNewTransaction());
        log.info("내부 트랜잭션 커밋");
        txManager.commit(inner);
        log.info("외부 트랜잭션 커밋");
        txManager.commit(outer);
    }

    이 테스트를 통해 확인해 보면 내부 트랜잭션이 시작할 때 새로운 커넥션을 생성하는 것이 아니라 'Participating in existing transcation' 즉, 기존의 트랜잭션에 참가한다는 뜻이다. 또한 트랜잭션을 시작할때 isNewTransaction을 확인하면 외부 트랜잭션은 true라고 뜨지만 내부 트랜잭션은 false라고 뜬다. 이 역시 기존의 트랜잭션에 참여하기 때문이다. 그리고 내부 트랜잭션이 커밋 될 때 로그는 찍히지만 사실상 아무런 일을 하지 않는다는 것을 알 수 있다.(사실 정말 아무런 일을 하지 않는 것은 아니다..ㅎㅎ 로그에만 집중하자!) 그 후 외부 트랜잭션도 커밋되는 순간 트랜잭션이 커밋이 되고 커넥션을 종료하는 모습을 알 수 있다.

    자세히 아래와 같이 동작을 하는데 각각의 논리 트랜잭션은 하나의 물리 트랜잭션으로 묶이기 때문에 하나의 트랜잭션 동기화 메니저를 통해 하나의 커넥션을 공유하는 것을 알 수 있다. 여기서 핵심은 트랜잭션 메니저에 커밋을 호출한다고 해서 항상 실제 커넥션에 물리 커밋이 발생하지 않는다는 점이다. 신규 트랜잭션인 경우만 실제 커넥션을 이용해서 물리 커밋과 롤백을 수행한다. 신규 트랜잭션이 아닌 참여하는 트랜잭션은 물리 커넥션을 사용하지 않는다.

    2) 외부 트랜잭션이 롤백하는 경우

    @Test
    void outer_rollback(){
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction={}",outer.isNewTransaction());
        log.info("내부 트랜잭션 시작");
        TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction={}",inner.isNewTransaction());
        log.info("내부 트랜잭션 커밋");
        txManager.commit(inner);
        log.info("외부 트랜잭션 롤백");
        txManager.rollback(outer);
    }

    트랜잭션이 생성되고 참여하는 것은 위와 동일하다. 내부 트랜잭션이 커밋을 해도 외부 트랜잭션이 롤백을 하면 전체 물리 커넥션은 롤백한다는 사실을 알 수 있다. 즉, 위에서 배운대로 내부 트랜잭션은 직접 물리 트랜잭션에 관여를 하지 않고 있다.

    3) 내부 트랜잭션이 롤백하는 경우

    @Test
    void inner_rollback(){
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction={}",outer.isNewTransaction());
        log.info("내부 트랜잭션 시작");
        TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction={}",inner.isNewTransaction());
        log.info("내부 트랜잭션 롤백");
        txManager.rollback(inner);
        log.info("외부 트랜잭션 커밋");
        txManager.commit(outer);
    }

    상황은 겉으로 보기에는 단순해 보인다. 내부 트랜젹션은 롤백을 했지만, 지금까지 정리한 내용을 살펴보면 외부 트랜잭션만 실제로 물리 트랜잭션에 영향을 주기 때문에 외부 트랜잭션이 커밋하면 커밋될 것 처럼 보인다. 하지만.. 결과는 롤백이 되면서 예외를 터트리게 된다. 예외 메시지를 보면 rollback-only라는 마크 때문에 트랜잭션은 롤백이 되었다는 사실을 알 수 있다. 이게 무슨 뜻일까..?

    설명을 하면 위에 사진처럼 동작한다. 먼저 내부 트랜잭션이 먼저 롤백을 하는데 이때 트랜잭션 동기화 메니저에서 커넥션을 들고와 롤백을한다. 하지만 실제 물리 트랜잭션에는 내부 트랜잭션이 직접적인 영향을 주지 않는다. 대신에!! 내부 트랜잭션이 롤백이 되면 트랜잭션 동기화 메니저에 rollbackOnly=true 라는 설정을 한다. 그 후 외부 트랜잭션이 커밋을 하려고 하는데 만약 트랜잭션 동기화 메니저의 rollbackOnly가 true로 설정이 되어 있다면 물리 트랜잭션을 커밋하는 것이 아니라 롤백을 한다. 이와 동시에 예외를 터트리게된다. 그 이유는 개발자 입장에서 외부 트랜잭션이 커밋이 되었기에 당연히 커밋이 될줄 알았는데 내부 트랜잭션에서 롤백이 되어 전체적으로 롤백이 되었기 때문에 개발자는 확실히 이 상황을 알고 있어야한다. 그래서 스프링은 기대하지 않은 롤백이 발생했다는 점을 명확하게 알려준다. 커밋을 호출 했는데 내부에서 롤백이 발생한 경우 모호하게 두면 심각한 문제가 발생한다. 이렇게 기대한 결과과 다른 경우 예외를 발생시켜서 명확하게 문제를 알려주는 것이 좋은 설계이다.

     

    이렇게 크게 3가지 상황을 알아보았다. 마지막으로 다시 트랜잭션 전파에 정리하자면 다음과 같다!! 정말 중요하니 꼭 기억하자! 대원칙을 알아야 다음에 어떤 상황이 와도 대처할 수 있다!!

    • 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다.
    • 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시한다.
    • 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인한다. 만약 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고, UnexceptedRollbackException 예외를 던진다.

     

    트랜잭션 전파 - REQURIES_NEW

    만약 이렇게 하나로 묶어서 트랜잭션을 관리하는 것이 아니라 늦게 시작해도 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 사용하는 방법은 없을까? 바로 REQURIES_NEW 옵션을 사용하면된다. 지금까지 처음 언급했듯이 기본적으로 옵션이 REQUIRED 였기 때문에 기존 트랜잭션이 있고, 중간에 시작하면 하나의 물리 트랜잭션으로 묶이게 된 것이다!

     @Test
     void inner_rollback_requires_new(){
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction={}",outer.isNewTransaction());
    
        log.info("내부 트랜잭션 시작");
        DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus inner = txManager.getTransaction(definition);
        log.info("inner.isNewTransaction={}",inner.isNewTransaction());
    
        log.info("내부 트랜잭션 롤백");
        txManager.rollback(inner);
    
        log.info("외부 트랜잭션 커밋");
        txManager.commit(outer);
            
    }

    내부 트랜잭션을 시작할 때 REQUIRES_NEW 옵션을 사용하면 로그 결과에서 처럼 기존 트랜잭션에 참여하는 것이 아니라 새로운 커넥션을 통해 트랜잭션을 시작한다. 그렇기에 내부 트랜잭션이 롤백이 되어도 기존의 외부 트랜잭션에 전혀 영향을 주지 않는다! 즉, 각각의 트랜잭션은 각각 다른 물리 트랜잭션인 것이다!

    '김영한의 스프링 > 데이터베이스' 카테고리의 다른 글

    [Spring] 스프링의 예외 처리  (2) 2024.10.02
    [Spring] 스프링의 트랜잭션 문제 해결  (1) 2024.10.02
    트랜잭션  (0) 2024.09.12
    커넥션 풀과 데이터소스  (0) 2024.09.11
    JDBC  (0) 2024.08.31
Designed by Tistory.