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
실패 위치 inner1 inner2 inner3 outer inner1 실패 롤백 실행 안됨 실행 안됨 커밋 inner2 실패 커밋 롤백 실행 안됨 커밋 inner3 실패 커밋 커밋 롤백 커밋 NESTED(유지는 Outer가 커밋할때 커밋됨)
실패 위치 inner1 inner2 inner3 outer inner1 실패 S1 롤백 실행 안됨 실행 안됨 커밋 inner2 실패 유지 S2 롤백 실행 안됨 커밋 inner3 실패 유지 유지 S3 롤백 커밋
This post is licensed under CC BY 4.0 by the author.