프로필사진

Go, Vantage point

가까운 곳을 걷지 않고 서는 먼 곳을 갈 수 없다.


Github | https://github.com/overnew/

Blog | https://everenew.tistory.com/





티스토리 뷰

반응형

 

 

 

* 김영한님의 스프링 DB 2편 강좌를 수강하며 정리한 글입니다. *

 

스프링 DB 2편 - 데이터 접근 활용 기술 - 인프런 | 강의

백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의 소개 | 인

www.inflearn.com

 

 

 

이전 글에서는 스프링 트랜잭션의 방식에 대해 정리하였다.

 

 

이번에는 트랜잭션 간의 복잡한 상황에서 스프링의 대처를 확인해보자.

 

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

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

 

 

 

 

트랜잭션 두 개를 연이어 실행하고 로그를 확인하면 히카리 커낵션 풀에서 동일한 conn0를 사용하고 반환하는 것을 확인할 수 있다.

하지만, HikariProxyConnection@2039166717 와 같이 @뒤로는 다르다.

풀에 반환된 커낵션을 다시 사용하는 것은 맞지만, 커낵션을 다루는 프록시 객체는 다른 것으로 서로 다른 트랜잭션을 실행한다. 즉, 서로 다른 커낵션이라고 보는 것이 맞다.

 

따라서 트랜잭션1은 커밋하고 2는 롤백하여도 독립적으로 작용한다.

 

 

 

 

트랜잭션 전파

 

하지만 트랜잭션 내부에서 트랜잭션이 또 일어난다면 서로 같은 트랜잭션에서 실행되어야 할까?

이런 상황에서의 동작을 결정하는 것이 트랜잭션 전파이다.

 

 

스프링은 논리 트랜잭션과 물리 트랜잭션 개념을 도입한다.

 

논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶이고 다음의 원칙을 따른다.

 

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

2. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.

 

 

각 논리 트랜잭션은  트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위로, 모든 트랜잭션 매니저가 커밋을 해야 물리 트랜잭션이 커밋이 된다.

 

 

 

외부에서 트랜잭션이 시작되고 commit전에 이어서 내부 트랜잭션을 실행해 커밋을 해보자.

@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("inner.isNewTransaction() ={}", inner.isNewTransaction());
    log.info("내부 트랜잭션 커밋");
    txManager.commit(inner);

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

 

 

 

로그를 확인해보면 inner 트랜잭션은 isNewTransaction 여부가 false로 outer 트랜잭션에 참여하고 있다.

내부가 먼저 commit을 하더라도 이는 무시되고, outer가 commit시에 트랜잭션이 종료되고 커낵션이 반환된다.

 

내부가 먼저 commit하면 물리 트랜잭션이 종료돼버리기 때문에,

스프링은 트랜잭션을 처음 실행한 외부 트랜잭션이 물리 트랜잭션을 관리하도록 한다.

 

inner 트랜잭션 매니저는 기존 트랜잭션이 존재하는지 동기화 매니저를 통해 확인한다.

기존 트랜잭션이 존재 시, 아무것도 하지 않고 트랜잭션에 참여한다.

inner 로직들은 동기화 매니저의 커낵션을 같이 사용하게 된다.

 

 

 

외부의 롤백

 

외부 트랜잭션만 롤백되는 경우를 생각해보자.

 

 

 

내부는 외부 트랜잭션에 참여해 커밋하지만, 어떠한 동작도 일어나지 않는다.

외부가 롤백이 되면 물리 트랜잭션 전체가 롤백하고 트랜잭션이 종료된다.

 

 

 

내부의 롤백

 

반대로 내부에서 롤백이되면 어떻게 될까?

 

 

스프링 트랜잭션의 원칙에의해 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.

 

따라서 내부가 롤백이 되면 트랜잭션 동기화 매니저에 rollback-only로 마킹해둔다. 해당 트랜잭션이 롤백만 가능하도록 표시를 하는 것이다.

외부가 커밋을 하더라도 마킹에 의해 물리 트랜잭션이 롤백된다.

 

단, 외부는 분명 커밋을 호출했음에도 롤백이 됐을 수도 있는 모호성의 문제가 있다.

이를 확실히 알리기 위해 커밋을 했음에도 롤백이 된다면 UnexpectedRollbackException 런타임 예외가 발생한다.

 

 

 

외부와 내부 트랜잭션 분리, REQUIRES_NEW 

 

외부와 내부를 분리하면 별도의 물리 트랜잭션을 가지게되어 서로 커밋과 롤백도 별도로 이루어진다.

이것은 REQUIRES_NEW 옵션으로 구현할 수 있다.

 

@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()); //true

    log.info("내부 트랜잭션 롤백");
    txManager.rollback(inner); // 정상적으로 롤백

    log.info("외부 트랜잭션 커밋");
    txManager.commit(outer);    // 정상적으로 커밋
}

 

기존의 outer 커낵션은 동기화 매니저에 그대로 존재하되 일단 보류한다.

inner도 새로운 커낵션을 획득해 트랜잭션이 진행된다.

inner 트랜잭션은 롤백 시점에 신규 트랜잭션인지 여부를 파악한다.

신규 트랜잭션이라면 롤백이 독립적으로 이뤄지게 된다.

다시 외부 로직으로 복귀하면 보류된 커낵션이 실행되는 것이다.

 

이제 분리가 가능하므로 내부 트랜잭셔도 메서드로 추출하여 사용할 수 있다.

문제는 커낵션이 분리될 수록 DB의 커낵션 풀이 빠르게 고갈될 수 있다.

 

 

default 옵션이 REQUIRED로 기존 트랜잭션이 없다면 새로운 트랜잭션을 생성한다.

SUPPORT는 트랜잭션이 없으면 그래도 진행하지만, 기존 트랜잭션이 있으면 참여해서 진행한다.

NOT_SUPPORT는 트랜잭션이 있던 없던 보류시키고 트랜잭션 없이 진행한다.

이외에는 MANDATORY, NEVER, NESTED 등이 있다.

 

 

 

트랜잭션 전파 활용

 

@Transactional은 기본적으로 REQUIRED 옵션으로, 기존 트랜잭션이 있다면 편승한다.

문제가 발생하는 건 여러 데이터 서비스들이 하나의 물리 트랜잭션으로 묶이면서 발생한다.

한쪽에만 문제가 있어도 물리 트랜잭션으로 묶여있으므로 모두 롤백이 되어 데이터 정합성이 이뤄진다.

 

 

만약 한 쪽이 예외가 발생해도 정상 진행을 해야 되는 상황이라고 가정하자.

OrderRepository에서 발생하는 런타임 예외를 발생 OrderRepository에서 잡아서 정상적으로 흐름을 반환해도, 내부 트랜잭션은 rollback-only를 마킹한다.

이는 트랜잭션 AOP 프록시(구현체)에 예외가 넘어가기만 하면 rollback-only가 트랜잭션 매니저에 마킹을 하기 때문이다.

따라서 외부에서는 문제가 없어서 commit을 하더라도 rollback-only가 체크되어 있기 때문에 모두 롤백된다.

 

 

이를 해결하기 위해 물리 트랜잭션에서 분리시키는 REQUIRES_NEW 옵션을 사용한다.

REQUIRES_NEW는 항상 신규 트랜잭션을 생성한다.

 

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Order order){
    //...
}

 

 

이 경우 OrderRepository에 문제가 발생해도 다른 물리 트랜잭션은 롤백이 발생하지 않는다.

단, 물리 커낵션마다 다른 커낵션을 사용하게 되어, DB의 커낵션이 낭비될 수 있다.

성능이 중요한 곳에는 주의하여 사용하자.

 

 

 

반응형
댓글
반응형
인기글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함