스프링DB

트랜잭션 전파

lby132 2022. 9. 16. 00:06

이번 글은 서로 연결돼있는 코드에서 트랜잭션이 두번 발생했을때 어떻게 되느냐 인데

@Test
void double_commit_rollback() {
    log.info("트랜잭션1 시작");
    final TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("트랜잭션1 커밋");
    txManager.commit(tx1);

    log.info("트랜잭션2 시작");
    final TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("트랜잭션2 롤백");
    txManager.rollback(tx2);
}

 

만약 이렇게 같은 target안에서 두 트랜잭션이 실행된다면 tx1는 커밋되고 tx2는 롤백된다. 

이렇게 커넥션이 각각 하나씩 생겨서 각자 할일을 한다.

HikariProxyConnection@448288866 wrapping conn0  -tx1이 얻은 connection

HikariProxyConnection@1151415961 wrapping conn0   - tx2가 얻은 connection

커넥션 이름은 같아서 같은 커넥션일것 같은데 히카리 커넥션풀이 반환해주는 커넥션을 다루는 프록시 객체의 주소가 다른걸 확인할수 있다. 결과적으로 보면 conn0이 같은걸 봐서 커넥션을 재사용된걸로 확인할 수 있고 HikariProxyConnection@448288866,

HikariProxyConnection@1151415961을 통해 각각의 커넥션 풀에서 커넥션을 조회한것을 확인할 수 있다.

그런데 

@Test
void outer_rollback() {
    log.info("외부 트랜잭션 시작");
    final TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());

    log.info("내부 트랜잭션 시작");
    final TransactionStatus inner = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("내부 트랜잭션 커밋");
    txManager.commit(inner);

    log.info("외부 트랜잭션 커밋");
    txManager.rollback(outer);
}

 

 

이렇게 전처럼 트랜잭션을 각각 사용하는게 아니라 하나의 트랜잭션이 이미 진행중인데 새로운 트랜잭션을 추가로 수행한다면?

이런 경우 어떻게 동작할지 결정하는 것을 트랜잭션 전파라고 한다. 스프링은 다양한 트랜잭션 전파 옵션을 제공하는데

기본이 REQUIRED이기때문에 이 기준으로 말하자면

 

스프링은 이런 경우 외부트랜잭션과 내부 트랜잭션을 하나의 트랜잭션으로 만들어준다. 내부 트랜잭션이 외부 트랜잭션에 참여하는것이다.

이게 아까 말한 기본 전파 옵션인 REQUIRED이고 다른 옵션으로 동작방식을 선택할 수 있다.

다시 위 코드를 살펴보면 외부 트랜잭션이 롤백되었으니 내부 트랜잭션도 커밋이 되지 않고 롤백이된다. 이게 내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻이다.

이때도 롤백이 되는데 이렇게 되면 내부 트랜잭션이 외부 트랜잭션을 따르는게 아닌가 싶었다. 커밋이 되어야 한다고 생각했다.

@Test
void inner_rollback() {
    log.info("외부 트랜잭션 시작");
    final TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());

    log.info("내부 트랜잭션 시작");
    final TransactionStatus inner = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("내부 트랜잭션 커밋");
    txManager.rollback(inner);

    log.info("외부 트랜잭션 커밋");
    Assertions.assertThatThrownBy(() -> txManager.commit(outer))
            .isInstanceOf(UnexpectedRollbackException.class);
}

생각이 짧았다 롤백이 된다는건 문제가 발생한거고 내부에서 롤백에 대한 문제가 발생하면 외부도 당연히 커밋이 되면 안되기 때문에 롤백이 발생한다. 그래서 외부 트랜잭션에서 커밋하려고하면 롤백으로 인한 에러인 UnexpectedRollbackException이 발생한다.

그런데 어떻게 된걸까 맨위 예제와 똑같이 트랜잭션을 두개 생성했는데 결과가 다르다.

그 이유는 이번처럼 트랜잭션 외부 내부로 실행했을때 커넥션을 하나만 생성한다.

그림처럼 외부 트랜잭션에서만 신규 트랜잭션으로 인지하고 내부 트랜잭션은 신규 트랜잭션이 아니라고 되어있다.

트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아 반환한다. 여기서 신규 트랜잭션 여부가 담겨있다.

final TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());  //true

이렇게 isNewTransaction()으로 확인해 보면 true, false를 반환하기 때문에 신규트랜잭션인지 아닌지 확인할 수 있다.

어쨋든 그래서 내부 트랜잭션은 신규 트랜잭션이 만들어지지 않기 때문에 커밋을 해도 아무런 반응이 없는 것이다.

그런데 내부에서 롤백을 하게 되면 내부 트랜잭션 매니저에서 트랜잭션 동기화 매니저에 rollbackOnly=true로 설정한다.

그리고 외부 트랜잭션에서 커밋을 요청하면 트랜잭션 매니저는 동기화매니저에서 rollbackOnly설정을 확인하고 true이면 아까 말한 UnexpectedRollbackException을 발생 시킨다. 만약 한곳만 롤백을 시키고 한곳은 그대로 커밋을 시키고 싶다면 여기서 쓸수 있는게

REQUIRES_NEW라는 전파옵션이다. 하나로 뭉쳐있는 물리 트랜잭션을 분리시키는 것이다.

이렇게 분리가 되어서 따로 동작하게 된다.  REQUIRES_NEW를 썻을때 요청 흐름을 보면

이런식으로 동작한다. 기본 전파 옵션인 REQUIRED와 다르게 내부 트랜잭션에도 신규 트랜잭션이 만들어진다.

그러면 이제 내부 트랜잭션에서 롤백을 해도 커밋을 해도 외부트랜잭션에 영항을 받지 않게 된다.

작업에 실패하더라도 모든 작업이 롤백되지 않는 장점이 있지만 단점은 하나의 요청에 커넥션을 두개 사용한다는 점이다. 

방법은 REQUIRES_NEW를 사용하지 않고 구조를 변경하는건데

이렇게 하면 http요청에 동시에 2개의 커넥션을 사용하지 않고 순차적으로 사용하고 반환하게 한다고 한다.

둘이 장단점이 있어 적절하게 선택해서 사용하면 될것 같다.

'스프링DB' 카테고리의 다른 글

초기화 시점에 트랜잭션적용  (0) 2023.06.01
스프링 트랜잭션 내부 호출시 발생하는 문제  (0) 2023.06.01
트랜잭션 이해  (0) 2022.09.15
트랜잭션 AOP 주의 사항  (0) 2022.09.14
트랜잭션 매니저 선택  (0) 2022.09.14