Spring Transaction Propagation

이재용·2025년 1월 1일
0

외부 트랜잭션이 수행중인데, 내부 트랜잭션이 추가로 수행됨
외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶인다.

외부 트랜잭션 : 처음 시작된 트랜잭션
내부 트랜잭션 : 외부에 트랜잭션이 수행되고 있는 도중에 호출된 트랜잭션

물리 트랜잭션, 논리 트랜잭션

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

원칙

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


모든 논리 트랜잭션이 커밋 되었으므로 물리 트랜잭션도 커밋된다.


외부 논리 트랜잭션이 롤백 되었으므로 물리 트랜잭션은 롤백된다.


내부 논리 트랜잭션이 롤백 되었으므로 물리 트랜잭션은 롤백된다.

하나의 물리 트랜잭션 동작 방식
문제 - 중복 커밋
외부 트랜잭션과 내부 트랜잭션의 커밋 중복 호출
해결
외부 트랜잭션만 물리 트랜잭션을 시작하고, 커밋한다.
처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다.

1. 모든 논리 트랜잭션 커밋

요청 흐름

핵심
트랜잭션 매니저에 커밋을 호출한다고해서 항상 실제 커넥션에 물리 커밋이 발생하지는 않는다.
신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행한다. 신규 트랜잭션이 아니면 실제 커밋을 호출하지 않는다.
트랜잭션이 내부에서 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다.

2. 외부 트랜잭션 롤백

응답 흐름

내부 트랜잭션
신규 트랜잭션이 아니기 때문에 트랜잭션 매니저가 실제 커밋을 호출하지 않는다.
외부 트랜잭션
신규 트랜잭션이기 때문에 트랜잭션 매니저가 DB 커넥션에 실제 롤백을 호출한다.

3. 내부 트랜잭션 롤백

응답 흐름

내부 트랜잭션
물리 트랜잭션을 롤백하지 않는 대신에 트랜잭션 동기화 매니저에 rollbackOnly=true 라는 표시를 해둔다.
외부 트랜잭션
트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly=true) 표시가 있는지 확인한다.

롤백 전용 표시가 있으면 물리 트랜잭션을 커밋하는 것이 아니라 롤백한다.
스프링은 이 경우 UnexpectedRollbackException 런타임 예외를 던진다.

외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 사용하는 방법

내부 트랜잭션을 시작할 때 REQUIRES_NEW 옵션을 사용
REQUIRES_NEW를 사용하면 DB커넥션이 동시에 2개 사용된다는 점을 주의

다양한 전파 옵션

  • REQUIRED
    가장 많이 사용하는 기본 설정이다. 기존 트랜잭션이 없으면 생성하고, 있으면 참여한다.
    트랜잭션이 필수라는 의미로 이해하면 된다. (필수이기 때문에 없으면 만들고, 있으면 참여한다.)

  • REQUIRES_NEW
    항상 새로운 트랜잭션을 생성한다.

  • SUPPORT
    트랜잭션을 지원한다는 뜻이다. 기존 트랜잭션이 없으면, 없는대로 진행하고, 있으면 참여한다.

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

활용

MemberService에만 @Transactional 코드를 추가

이렇게 하면 MemberService를 시작할 때 부터 종료할 때 까지의 모든 로직을 하나의 트랜잭션으로 묶을 수 있다.


@Transactional이 MemberService에만 붙어있기 때문에 여기에만 트랜잭션 AOP가 적용된다.
MemberService의 시작부터 끝까지, 관련 로직은 해당 트랜잭션이 생성한 커넥션을 사용하게 된다.

모든 곳에 @Transactional

이 경우 외부에 있는 신규 트랜잭션만 실제 물리 트랜잭션을 시작하고 커밋한다.
내부에 있는 트랜잭션은 물리 트랜잭션 시작하거나 커밋하지 않는다.

전파 롤백

회원과 회원 이력 로그를 처리하는 부분을 하나의 트랜잭션으로 묶은 덕분에 문제가 발생했을 때 회원과 회원 이력 로그가 모두 함께 롤백된다.

복구 REQUIRED

목표 - 회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
상황 - 모든 곳에 @Transactional

잘못된 해결
트랜잭션을 분리하지 않고 예외만 복구

내부 트랜잭션에서 rollbackOnly를 설정하기 때문에 예외를 잡아서 처리해도 결과적으로 물리 트랜잭션은 롤백된다.

잘된 해결
REQUIRES_NEW를 사용해서 트랜잭션을 분리
LogRepository - save() 에다가 @Transactional(propagation = Propagation.REQUIRES_NEW) 설정

REQUIRES_NEW를 사용하게 되면 물리 트랜잭션 자체가 완전히 분리되어 버린다.
REQUIRES_NEW는 신규 트랜잭션이므로 rollbackOnly 표시가 되지 않는다. 그냥 해당 트랜잭션이 물리 롤백되고 끝난다.
예외가 MemberService에 던져지고, MemberService는 해당 예외를 복구 -> 정상 흐름을 리턴 -> rollbackOnly가 없으므로 물리 트랜잭션을 커밋
결과적으로 회원 데이터는 저장되고, 로그 데이터만 롤백
참고
실전에서는 MemberService가 더 많은 리포지토리들을 호출하고 그 중에 LogRepository만 트랜잭션을 분리한다.
REQUIRES_NEW 를 사용하면 하나의 HTTP 요청에 동시에 2개의 데이터베이스 커넥션을 사용하게 된다. 따라서 성능이 중요한 곳에서는 이런 부분을 주의해서 사용해야 한다.

0개의 댓글