Post

04 트랜잭션 전파

04 트랜잭션 전파

1. Note

  • 생각보다 단순한데 어려운 부분
    • REQUIRED_NEW와 NESTED를 잘 활용해서 써야할듯
    • 메인 비즈니스로직과 무관하게 커밋을 유지해야하는가 안해야하는가?
  • 트랜잭션은
    • 외부 트랜잭션은 묶는 역할에 가깝고,
    • 내부 트랜잭션은 작업단위에 가까움.(이걸 살릴것인가 말릴것인가)
  • 개념은 쉬운데 생각해내는 것이 어려울 듯.

2. 트랜잭션전파

1. 트랜잭션전파

  • 이미 진행 중인 트랜잭션이 있을 때, 새로 호출된 메서드가 그 트랜잭션을 어떻게 처리할지 결정하는 규칙
  • 트랜잭션을 “이어받을지”, “새로 만들지”, “무시할지”를 정하는 정책

2. 상황

1
2
3
4
5
6
7
8
9
A() {
  트랜잭션 시작
  B() 호출
}

# B호출은
# A의 트랜잭션에 참여할까?
# 아니면 새 트랜잭션을 만들까?
# 아예 트랜잭션 없이 실행할까?

3. 스프링에서 트랜잭션

1
2
3
4
@Transactional(propagation = Propagation.REQUIRED) // 트랜잭션 전파옵션설정
public void method() {
    // 비즈니스 로직
}

4. 전파 옵션 종류

구분옵션기존 트랜잭션 있을 때없을 때특징
기본값REQUIRED참여생성기본값, 가장 많이 사용
자주 사용REQUIRES_NEW중단 후 새로 생성생성항상 독립 트랜잭션
자주 사용SUPPORTS참여그냥 실행선택적 트랜잭션
 NOT_SUPPORTED중단그냥 실행트랜잭션 사용 안함
 MANDATORY참여예외 발생반드시 트랜잭션 필요
 NEVER예외 발생그냥 실행트랜잭션 있으면 안됨
자주 사용NESTED중첩 트랜잭션 생성생성롤백 지점 분리 (DB 지원 필요)

3. 자주 사용하는 트랜잭션 전파

1. REQUIRED

  • 현재 활성 상태의 트랜잭션이 있으면 해당 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 생성
  • 일반적으로 기본값으로 사용됨
  • 세부 동작
    • 트랜잭션 존재하면 기존 트랜잭션의 범위에 포함되어 실행, 호출한 메서드와 동일한 물리 트랜잭션을 공유
    • 트랜잭션이 없으면 새로운 트랜잭션을 시작하고, 메서드 실행이 완료되면 해당 트랜잭션을 커밋하거나 롤백
  • 주요 포인트
    • 여러 개의 데이터 변경 작업이 하나의 논리적인 단위로 묶여야 할 때.
    • 가장 일반적인 서비스 계층의 메서드에 적용. (예: 주문 생성, 사용자 정보 수정)
  • 소스 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    // ServiceA
    @Transactional // (propagation = Propagation.REQUIRED) 와 동일
    public void parentMethod() {
        // ... 로직 1 ...
        serviceB.childMethod(); // parentMethod의 트랜잭션에 참여
    }
      
    // ServiceB
    @Transactional(propagation = Propagation.REQUIRED)
    public void childMethod() {
        // ... 로직 2 ...
        // 여기서 예외 발생 시, 로직 1과 로직 2 모두 롤백됨
    }
    
  • Note
    • 은행 계좌 이체 시, A 계좌의 출금과 B 계좌의 입금은 논리적으로 분리할 수 없는 하나의 작업
    • 두 메서드 모두 REQUIRED로 설정하여 출금만 성공하고 입금이 실패하는 치명적인 데이터 불일치 상황을 방지

2. REQUIRES_NEW

  • 항상 새로운 트랜잭션을 생성
  • 이미 진행 중인 트랜잭션이 있다면, 그 트랜잭션을 일시 중단(suspend)하고 독립적인 새 트랜잭션을 시작
  • 세부 동작
    • 호출한 쪽의 트랜잭션과 완전히 별개의 물리 트랜잭션에서 실행
    • 자신의 트랜잭션은 호출한 쪽의 트랜잭션 상태(커밋/롤백)와 관계없이 독립적으로 커밋되거나 롤백
  • 주요 포인트
    • 메인 로직의 성공 여부와 관계없이 반드시 처리되어야 하는 작업.
    • 로그 기록, 이력(Audit) 저장 등 메인 트랜잭션과 분리되어야 하는 부가 기능에 사용
  • 소스코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    // ServiceA
    @Transactional
    public void parentMethod() {
        // ... 주문 처리 로직 ...
        try {
            logService.logAction("주문 시도"); // 독립적인 새 트랜잭션으로 실행
        } catch (Exception e) {
            // 로그 기록 실패는 주문 처리에 영향을 주지 않음
        }
        // 주문 처리 중 예외가 발생해 parentMethod가 롤백되어도, logAction은 이미 커밋되었을 수 있음
    }
      
    // LogService
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAction(String message) {
        // ... 로그 DB에 메시지 저장 ...
        // 이 메서드는 자체적으로 커밋/롤백됨
    }
    
  • Note(로그 및 이력 기록 하는 케이스)
    • 시나리오
      • OrderService의 requestOrder() 메서드가 구매를 처리하며
      • LogService의 recordHistory()를 호출해 “구매 시도” 이력을 남기는 경우
    • 상황
      • 재고 부족 등의 예외로 requestOrder()의 메인 트랜잭션이 롤백되더라도,
      • “구매 시도” 이력은 반드시 DB에 남아야함
      • 이때 recordHistory()에 REQUIRES_NEW를 적용하면,
      • 구매 트랜잭션의 성공 여부와 관계없이 로그 기록 트랜잭션을 독립적으로 커밋

3. NESTED

  • 진행 중인 트랜잭션이 있는 경우, 해당 트랜잭션 내에 중첩된 트랜잭션을 생성
  • 진행 중인 트랜잭션이 없으면 REQUIRED와 동일하게 새 트랜잭션을 생성
  • 세부 동작:
    • 중첩 트랜잭션은 별도의 커밋/롤백이 불가능하며, 부모 트랜잭션의 커밋/롤백에 의존
    • 대신, 중첩 트랜잭션은 내부적으로 Savepoint를 사용
    • 중첩 트랜잭션 범위 내에서 예외가 발생하면 해당 Savepoint까지만 롤백
    • 부모 트랜잭션이 롤백되면, 자식 트랜잭션의 작업은 커밋되었더라도 모두 함께 롤백
  • 주요포인트
    • 하나의 큰 작업 단위 내에서 특정 로직만 실패 시 되돌리고 싶을 때.
    • 복잡한 업데이트 과정 중 일부 단계에서 오류가 발생해도 앞선 단계는 유지하고 싶을 경우.
  • 소스코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    // ServiceA
    @Transactional
    public void parentMethod() {
      // ... 로직 1 (사용자 포인트 적립) ...
      try {
          serviceB.nestedMethod(); // 중첩 트랜잭션 시작 (Savepoint 생성)
      } catch(Exception e) {
          // nestedMethod만 롤백되고(Savepoint로 복귀), 로직 1은 유지됨
      }
          // parentMethod의 마지막에 전체 트랜잭션이 커밋됨
      }
      
    // ServiceB
    @Transactional(propagation = Propagation.NESTED)
    public void nestedMethod() {
      // ... 로직 2 (이벤트 쿠폰 발급) ...
      // 여기서 예외가 발생하면 로직 2만 롤백됨
    } 
    
  • Note(선택적 기능의 부분 롤백)
    • 시나리오
      • PointService의 grantPoints() 메서드가 기본 포인트를 지급한 후,
      • 추가적으로 CouponService의 issueSpecialCoupon()을 호출하여 특별 쿠폰을 지급
    • 상황
      • 만약 쿠폰 발급 과정에서 오류가 발생하면,
      • 쿠폰 발급만 취소하고 이미 지급된 기본 포인트는 그대로 유지하고 싶을 때 NESTED
      • NESTED는 Savepoint를 이용해 이러한 부분 롤백을 가능하게 하여,
      • REQUIRED를 사용했을 때처럼 기본 포인트 지급까지 전체가 롤백되는 상황을 막아줌

4. SUPPORTS

  • 진행 중인 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행
  • 세부 동작
    • 호출한 쪽에 트랜잭션이 있으면 해당 트랜잭션에 참여하여 실행 (REQUIRED와 유사).
    • 호출한 쪽에 트랜잭션이 없으면, 트랜잭션 경계 없이 (non-transactional) 코드가 실행,
    • 이 경우 커밋이나 롤백 개념이 적용되지 않음(오토커밋처리)
  • 주요 포인트
    • 데이터 변경이 없는 단순 조회(Read-Only) 작업에 적합
    • 트랜잭션이 필수적이지는 않지만, 다른 트랜잭션 작업에 포함될 때 함께 묶이면 좋은 경우에 사용
  • 소스코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    // ServiceA
    @Transactional
    public void parentMethod() {
        // ... 데이터 변경 로직 ...
        productService.getProductInfo(123L); // parentMethod의 트랜잭션에 참여하여 조회
    }
      
    // ProductService
    @Transactional(propagation = Propagation.SUPPORTS)
    public Product getProductInfo(Long id) {
        // ... 상품 정보 조회 로직 ...
        // 이 메서드가 트랜잭션 없이 단독으로 호출되어도 정상 동작함
    }
    
  • Note
    • 상품 목록을 조회하는 메서드는 그 자체로는 트랜잭션이 필요 없지만,
    • 다른 트랜잭션(예: 구매 처리) 내에서 호출될 경우,
    • 해당 트랜잭션의 일관된 데이터 뷰(Read Consistency) 내에서 실행되는 것이 유리

5. Memo

  • REQUIRES_NEW

    실패 위치inner1inner2inner3outer
    inner1 실패롤백실행 안됨실행 안됨커밋
    inner2 실패커밋롤백실행 안됨커밋
    inner3 실패커밋커밋롤백커밋
  • NESTED(유지는 Outer가 커밋할때 커밋됨)

    실패 위치inner1inner2inner3outer
    inner1 실패S1 롤백실행 안됨실행 안됨커밋
    inner2 실패유지S2 롤백실행 안됨커밋
    inner3 실패유지유지S3 롤백커밋
This post is licensed under CC BY 4.0 by the author.