Spring/[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]

[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 스프링 트랜잭션 전파 - 기본

h2boom 2024. 11. 29. 23:16

스프링 트랜잭션 전파

스프링 트랜잭션 - 커밋, 롤백

  • 여러 트랜잭션을 각각 따로 사용하는 경우
    •  트랜잭션이 각각 수행되면서 사용되는 커넥션도 각각 다르다.
      • 커넥션 풀이 있는 경우 커넥션을 사용하고 반납하다보면 물리적으로는 같은 커넥션을 사용할 수는 있지만 완전히 다른 커넥션으로 취급된다.
      • 윗 그림은 커넥션 풀을 사용하지 않는다고 가정했을 때 동작 방식
    • 이 경우 트랜잭션을 각자 관리하기 때문에 전체 트랜잭션을 묶을 수 없다.

스프링 트랜잭션 - 전파

  • 트랜잭션 전파(propagation) : 어떤 트랜잭션이 동작중인 과정에서 다른 트랜잭션을 실행할 경우 어떻게 동작할지 결정하는 것.

 

  • 트랜잭션 전파의 기본 옵션인 REQUIRED를 기준으로 아래 내용 작성
  • 외부 트랜잭션이 수행중일 때 내부 트랜잭션이 추가로 수행되는 경우
    • 스프링은 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션으로 만들어주는 방식( = 내부 트랜잭션이 외부 트랜잭션에 참여하는 것)이 기본 동작이며 옵션을 통해 다른 동작 선택 가능

 

  • 물리 트랜잭션 vs 논리 트랜잭션
    • 스프링에서는 논리 트랜잭션과 물리 트랜잭션으로 개념을 나눈다.
    • 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다.
    • 물리 트랜잭션 : 실제 DB에 적용되는 트랜잭션을 의미한다.
      • 실제 커넥션을 통해 커밋, 롤백하는 단위
    • 논리 트랜잭션 : 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위
      • 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 논리 트랜잭션 개념이 나타난다.
    • 단순히 트랜잭션이 하나인 경우에는 둘을 구분하지 않는다.
    • 트랜잭션 원칙
      • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
      • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
      • => 모든 트랜잭션 매니저를 커밋해야 물리 트랜잭션이 커밋된다.
        하나의 트랜잭션 매니저라도 롤백하면 물리 트랜잭션은 롤백된다.

 

  • 외부 트랜잭션과 내부 트랜잭션 모두 커밋되는 경우

@Test
public 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);
}
  • 외부, 내부 트랜잭션이 모두 커밋인 경우 예제
    • 외부 트랜잭션 outer가 수행중일 때 내부 트랜잭션 inner를 추가로 수행
    • 외부 트랜잭션이 처음 수행된 트랜잭션이기에 isNewTransaction = true가 된다.
    • 내부 트랜잭션을 시작하는 시점에는 외부 트랜잭션이 이미 진행중이기에 내부 트랜잭션은 외부 트랜잭션에 참여한다.
      • 이 경우 신규 트랜잭션이 아니기에 isNewTransaction = false가 된다.
    • 트랜잭션 참여 : 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 의미
      => 외부 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 의미
      • 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것.

 

외부 트랜잭션 시작
Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@1943867171 wrapping conn0] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@1943867171 wrapping conn0] to manual commit
outer.isNewTransaction()=true

내부 트랜잭션 시작
Participating in existing transaction
inner.isNewTransaction()=false
내부 트랜잭션 커밋

외부 트랜잭션 커밋
Initiating transaction commit
Committing JDBC transaction on Connection [HikariProxyConnection@1943867171 
wrapping conn0]Releasing JDBC Connection [HikariProxyConnection@1943867171 wrapping conn0] after transaction
  • 테스트 코드 실행 결과
    • 내부 트랜잭션을 시작할 때 메시지 Participating in existing transaction => 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 의미
    • 외부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통한 물리 트랜잭션을 시작(manual commit)하고 DB 커넥션을 통해 커밋을 한다.
      • 내부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통해 커밋하는 로그가 전혀 없다.
    • 외부 트랜잭션만 물리 트랜잭션을 시작하고 커밋한다.
      • 한 커넥션에서 커밋 / 롤백을 한 번 밖에 못하기에 내부 트랜잭션에서 커밋하면 트랜잭션이 끝나버리기 때문에 처음 시작한 외부 트랜잭션까지 이어갈 수 없기에 내부 트랜잭션은 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다.
    • 스프링에서는 여러 트랜잭션이 함께 사용되는 경우 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 함으로 트랜잭션 중복 커밋 문제를 해결한다.

 

  • 요청 흐름 - 외부 트랜잭션
    • 1.getTransaction() 호출로 외부 트랜잭션 시작
    • 2.트랜잭션 매니저는 데이터 소스를 통해 커넥션 생성
    • 3.생성한 커넥션을 수동 커밋 모드로 설정 -> 물리 트랜잭션 시작
    • 4.트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션 보관
    • 5.트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 보관하며 신규 트랜잭션 여부가 true로 담겨있다.
    • 6.로직1이 수행되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용
  • 요청 흐름 - 내부 트랜잭션
    • 7.getTransaction() 호출로 내부 트랜잭션 시작
    • 8.트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해 기존 트랜잭션이 존재하는지 확인
    • 9.기존 트랜잭션이 존재하므로 기존 트랜잭션에 참여
      = 아무것도 하지 않는다는 의미
      • 물리 트랜잭션이 진행중이므로 이후 로직에서 트랜잭션 동기화 매니저에 보관된 기존에 시작된 트랜잭션을 자연스럽게 사용하게 된다.
    • 10.트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 반환하며 신규 트랜잭션의 여부는 false이다.
    • 11.로직2가 수행되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 외부 트랜잭션이 보관한 커넥션을 획득해서 사용한다.
  • 응답 흐름 - 내부 트랜잭션
    • 12.로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋
    • 13.트랜잭션 매니저는커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
      • 내부 트랜잭션은 신규 트랜잭션이 아니기에 실제 커밋을 호출하지 않는다.
        • 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 종료되므로 아직 트랜잭션이 끝나지 않았기에 실제 커밋을 호출하면 안되며 외부 트랜잭션이 종료될 때까지 이어져야 한다.
  • 응답 흐름 - 외부 트랜잭션
    • 14.로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋
    • 15.트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
      • 외부 트랜잭션은 신규 트랜잭션이기에 DB 커넥션에 실제 커밋을 호출한다.
      • 트랜잭션 매니저에 커밋하는 것이 논리적인 커밋이라면 실제 커넥션에 커밋하는 것은 물리적인 커밋으로 볼 수 있다.
    • 16.실제 DB에 커밋이 반영되고 물리 트랜잭션도 종료된다.

 

  • 트랜잭션 매니저에 커밋을 호출하더라도 항상 실제 커넥션에 물리 커밋이 발생하지 않는다.
    • 신규 트랜잭션의 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행한다.
      신규 트랜잭션이 아니면 실제 커넥션을 사용하지 않는다.

스프링 트랜잭션 전파 - 외부 롤백

  • 내부 트랜잭션은 커밋되고 외부 트랜잭션이 롤백되는 경우
    • 외부 트랜잭션이 물리 트랜잭션을 시작하고 롤백한다.
    • 내부 트랜잭션은 물리 트랜잭션에 직접 관여하지 않는다.

 

  • 요청 흐름은 둘 다 커밋하는 예제와 동일
  • 응답 흐름 - 내부 트랜잭션
    • 1.로직2가 끝난 후 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋
    • 2.신규 트랜잭션이 아니기에 트랜잭션 매니저는 실제 커넥션의 커밋을 호출하지 않는다.
  • 응답 흐름 - 외부 트랜잭션
    • 3.로직1이 끝난 후 트랜잭션 매니저를 통해 외부 트랜잭션을 롤백
    • 4.신규 트랜잭션이기에 트랜잭션 매니저는 실제 커넥션의 롤백을 호출한다.
    • 5.실제 DB에 롤백이 반영되고 물리 트랜잭션이 종료된다.

스프링 트랜잭션 전파 - 내부 롤백

  • 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않는다.
    • 내부 트랜잭션은 롤백되고 외부 트랜잭션은 커밋이 될 때 전체를 롤백하려면 어떻게 해야할까? 

 

  • 내부 트랜잭션은 롤백되고 외부 트랜잭션은 커밋되는 경우
    • 내부 트랜잭션 롤백 -> 실제 물리 트랜잭션은 롤백되지 않지만 기존 트랜잭션을 롤백 전용으로 표시한다.
      • Participating transaction failed - marking existing transaction as rollback-only
    • 외부 트랜잭션 커밋 -> 커밋을 호출했지만 전체 트랜잭션이 롤백 전용으로 표시되어 있기에 물리 트랜잭션을 롤백한다.
      • Global transaction is marked as rollback-only but transactional code requested commit

 

  • 요청 흐름은 이전 예제들과 동일
  • 응답 흐름 - 내부 트랜잭션
    • 1.로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백
    • 2.신규 트랜잭션이 아니기에 실제 커넥션의 롤백을 호출하지 않는다.
    • 3.내부 트랜잭션은 물리 트랜잭션을 롤백하지 않는 대신 트랜잭션 동기화 매니저에 rollbackOnly=true 라는 표시를 해둔다.
  • 응답 흐름 - 외부 트랜잭션
    • 4.로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋
    • 5.신규 트랜잭션이기에 실제 커넥션의 커밋을 호출한다.
      • 실제 커밋 호출 전 트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly=true) 표시가 있는지 확인하고 표시가 있는 경우 물리 트랜잭션을 커밋하는 것이 아닌 롤백을 한다.
    • 6.실제 DB에 롤백이 반영되고 물리 트랜잭션이 종료된다.
    • 7.트랜잭션 매니저에 커밋을 호출한 개발자 입장에 롤백 전용(rollbackOnly=true)표시로 인해 롤백이 된 상황이므로 UnexpectedRollbackException 런타임 예외를 던진다.
      • 커밋을 시도했지만 기대하지 않은 롤백이 발생했다는 것을 명확히 알려주기 위함

 

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

스프링 트랜잭션 전파 - REQUIRES_NEW

  • REQUIRES_NEW : 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법
    • 커밋과 롤백이 각각 별도로 이뤄진다.
  • 물리 트랜잭션을 분리하기 위해 내부 트랜잭션 시작 시 REQUIRES_NEW 옵션 사용
    • 별도의 물리 트랜잭션을 가지는 것은 DB 커넥션을 따로 사용하는 것

 

@Test
public 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 예제 코드
    • 외부 트랜잭션 시작 -> 새 커넥션 획득, 물리 트랜잭션 시작(신규 트랜잭션O)
    • 내부 트랜잭션 REQUIRES_NEW 옵션으로 시작 -> 새 커넥션 획득, 물리 트랜잭션 시작(신규 트랜잭션O)
    • 내부 트랜잭션 롤백 -> 신규 트랜잭션이므로 물리 트랜잭션 롤백 수행
    • 외부 트랜잭션 커밋 -> 신규 트랜잭션이므로 물리 트랜잭션 커밋 수행

 

  • 요청 흐름 - 외부 트랜잭션
    • 1.getTransaction() 호출로 외부 트랜잭션 시작
    • 2.트랜잭션 매니저는 데이터 소스를 통해 커넥션 con1 생성
    • 3.생성한 커넥션을 수동 커밋 모드로 설정 -> 물리 트랜잭션 시작
    • 4.트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션 보관
    • 5.트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 보관하며 신규 트랜잭션 여부가 true로 담겨있다.
    • 6.로직1이 수행되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용
  • 요청 흐름 - 내부 트랜잭션
    • 7.REQUIRES_NEW 옵션과 함께 getTransaction() 호출로 내부 트랜잭션 시작
      • 트랜잭션 매니저는 REQUIRES_NEW 옵션을 확인하고 기존 트랜잭션에 참여하는 것이 아닌 새 트랜잭션을 시작
    • 8.트랜잭션 매니저는 데이터 소스를 통해 커넥션 con2 생성
    • 9. 생성한 커넥션을 수동 커밋 모드로 설정 -> 물리 트랜잭션 시작
    • 10. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션 보관
      • 기존 외부 트랜잭션이 사용하는 con1 커넥션은 잠시 보류되고 내부 트랜잭션의 con2 커넥션이 사용된다.
    • 11. 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 보관하며 신규 트랜잭션 여부가 true로 담겨있다.
    • 12. 로직2가 수행되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저에 있는 con2 커넥션을 획득해서 사용한다.

  • 응답 흐름 - 내부 트랜잭션
    • 1.로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백
    • 2.신규 트랜잭션이기에 트랜잭션 매니저는 실제 con2 커넥션의 물리 트랜잭션을 롤백한다.
      • 트랜잭션은 종료되고 con2 커넥션은 종료되거나 커넥션 풀에 반납된다.
      • con1 커넥션의 보류가 끝나고 다시 사용한다.
  • 응답 흐름 - 외부 트랜잭션
    • 1.로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋
    • 2.신규 트랜잭션이기에 트랜잭션 매니저는 실제 con1 커넥션의 물리 트랜잭션을 커밋한다.
      • rollbackOnly 설정을 체크하지만 없으므로 커밋
      • 트랜잭션은 종료되고 con1 커넥션은 종료되거나 커넥션 풀에 반납된다.

 

  • REQUIRES_NEW 옵션을 사용하면 물리 트랜잭션이 명확하게 분리된다.
    • DB 커넥션이 동시에 여러 개 사용될 수 있기에 조심할 것

스프링 트랜잭션 전파 - 전파 옵션

  • 전파 옵션을 별도로 설정하지 않으면 REQUIRED 옵션을 사용한다.
    • 실무에서 대부분 REQUIRED 옵션을 사용

 

  • 트랜잭션 전파 옵션
    • REQUIRED : 가장 많이 사용하는 기본 설정으로 기존 트랜잭션이 없으면 생성하고 있으면 참여한다.
    • REQUIRES_NEW : 항상 새로운 트랜잭션을 생성한다.
    • SUPPORT : 트랜잭션을 지원한다는 의미로 기존 트랜잭션이 없으면 없는대로 진행하고 있으면 참여한다.
    • NOT_SUPPORT : 트랜잭션을 지원하지 않는다는 의미로 트랜잭션 없이 진행하며 기존 트랜잭션이 있더라도 보류한다.
    • MANDATORY : 의무 사항으로 트랜잭션이 반드시 있어야 하며 기존 트랜잭션이 없으면 예외가 발생한다.
    • NEVER : 트랜잭션을 사용하지 않는다는 의미로 기존 트랜잭션이 있으면 예외가 발생한다.
      기존 트랜잭션도 허용하지 않는다는 강한 부정의 의미
    • NESTED : 기존 트랜잭션이 없으면 트랜잭션을 생성하고 있으면 중첩 트랜잭션을 만든다.
      • 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만 외부 트랜잭션에게 영향을 주지는 못한다.
      • ex) 중첩 트랜잭션이 롤백되어도 외부 트랜잭션 커밋 가능
        외부 트랜잭션이 롤백되면 중첩 트랜잭션도 롤백된다.
      • JDBC savepoint 기능을 사용, JPA에서는 중첩 트랜잭션을 사용할 수 없다.

 

  • isolation, timeout, readOnly는 트랜잭션 처음 시작 시에만 적용되기에 트랜잭션이 참여하는 경우에는 적용되지 않는다.

정리

  • 트랜잭션 전파(propagation) : 어떤 트랜잭션이 동작중인 과정에서 다른 트랜잭션을 실행할 경우 어떻게 동작할지 결정하는 것.

 

  • 물리 트랜잭션 vs 논리 트랜잭션
    • 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다.
    • 물리 트랜잭션 : 실제 DB에 적용되는 트랜잭션을 의미한다.
      • 실제 커넥션을 통해 커밋, 롤백하는 단위
    • 논리 트랜잭션 : 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위
      • 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 논리 트랜잭션 개념이 나타난다.
    • 트랜잭션 원칙
      • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
      • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
      • => 모든 트랜잭션 매니저를 커밋해야 물리 트랜잭션이 커밋된다.
        하나의 트랜잭션 매니저라도 롤백하면 물리 트랜잭션은 롤백된다.
  • 외부 트랜잭션만 물리 트랜잭션을 시작하고 커밋한다.
    • 한 커넥션에서 커밋 / 롤백을 한 번 밖에 못하기에 내부 트랜잭션은 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다.

 

  • 트랜잭션 롤백 / 커밋되는 경우
    • 내부, 외부 트랜잭션 모두 커밋되는 경우
      =>물리 트랜잭션 커밋
    • 내부 트랜잭션 커밋, 외부 트랜잭션 롤백되는 경우
      => 물리 트랜잭션은 외부 트랜잭션에 의해 결정되므로 롤백
    • 내부 트랜잭션 롤백, 외부 트랜잭션 커밋되는 경우
      => 내부 트랜잭션 롤백 시 트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly = true) 표시를 함으로 물리 트랜잭션이 롤백되고 예외 발생

 

  • REQUIRES_NEW : 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법
    • REQUIRES_NEW 옵션을 지정하고 내부 트랜잭션 시작하면 각각 별도의 커넥션과 물리 트랜잭션을 사용하고 서로에게 영향을 미치지 않는다.
    • REQUIRES_NEW 옵션을 사용하면 여러 개의 커넥션이 동시에 사용될 수 있기에 조심할 것

 

  • 스프링 트랜잭션 전파 옵션은 REQUIRED가 기본 옵션이며 가장 많이 사용하는 옵션이다.

출처 : [인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard

 

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

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

www.inflearn.com