스택큐힙리스트

트랜잭션 전파·격리 수준 완전 정복 본문

개발

트랜잭션 전파·격리 수준 완전 정복

스택큐힙리스트 2025. 7. 18. 19:10
반응형

1️⃣ 트랜잭션 전파(Propagation)란?

스프링 AOP 프록시가 현재 실행 중인 트랜잭션을 “새로 만들지, 이어받지, 잠깐 끊을지” 결정하는 규칙입니다.

전파 옵션 의미 (한 줄 요약)
REQUIRED (기본) ❝있으면 참여, 없으면 새로 생성❞ – 80 % 이상 이걸로 OK
REQUIRES_NEW ❝무조건 새 트랜잭션❞ – 외부 트랜잭션과 독립 커밋/롤백
NESTED ❝논리적 내부 트랜잭션❞ – Savepoint 기반 부분 롤백 가능
SUPPORTS / NOT_SUPPORTED ❝있으면 따라가고 / 완전히 비트랜잭션❞
MANDATORY / NEVER ❝반드시 있어야 함 / 있으면 안 됨❞ – Assertion 용
 

실전 팁

  • REQUIRES_NEW는 알림 메일·로그 저장처럼 메인 로직 실패와 분리하고 싶을 때.
  • NESTED는 배치 처리 중 일부 레코드만 롤백할 때 유용.
  • 트랜잭션 경계는 Service 계층에만! Controller나 Repository에 붙이지 말 것.

2️⃣ 트랜잭션 격리 수준(Isolation Level)은 왜 필요할까?

동시에 실행되는 트랜잭션 간 “더티 리드·반복 불가·팬텀 리드” 같은 이상 현상을 얼마나 막을지 정하는 레버입니다.

  • READ_UNCOMMITTED : 다른 트랜잭션의 미커밋 데이터를 읽음 → 거의 쓰지 않음
  • READ_COMMITTED : 커밋된 데이터만 읽음 (MySQL InnoDB 기본)
  • REPEATABLE_READ : 같은 트랜잭션 내에서 같은 결과 보장, 팬텀 리드는 허용
  • SERIALIZABLE : 가장 엄격, 사실상 순차 실행 — 성능 부담 大
  • DEFAULT : DB 벤더 기본값을 그대로 사용

성능 ↔ 일관성 트레이드오프
👉 OLTP 서비스는 대부분 READ_COMMITTED / REPEATABLE_READ가 안정적.
👉 대량 정산 배치나 회계 처리에는 SERIALIZABLE 고려.


3️⃣ 실전 예제: 전파 + 격리 한 방에 세팅

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final OrderRepository orderRepository;
    private final PayGateway payGateway;

    // 주문 결제 로직 – 메인 트랜잭션
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void placeOrder(OrderRequest req) {
        Order order = orderRepository.save(req.toEntity());

        try {
            // 부수 작업은 새 트랜잭션으로 분리
            sendPayRequest(order);
        } catch (RuntimeException e) {
            log.warn("결제 요청 실패, 주문만 롤백");
            throw e; // placeOrder 전체 롤백
        }
    }

    // 결제망 호출 – 실패해도 주문 테이블은 영향 없음
    @Transactional(propagation = Propagation.REQUIRES_NEW,
                   isolation     = Isolation.READ_COMMITTED)
    public void sendPayRequest(Order order) {
        payGateway.request(order);
    }
}
  • placeOrder() 실패 시 두 트랜잭션 모두 롤백.
  • sendPayRequest()만 실패하면 주문 트랜잭션에 영향 없고, 재시도 로직 붙이기 용이.

4️⃣ 흔한 실수

  1. REQUIRES_NEW 남발 → 커넥션 풀 고갈·Deadlock 가능.
  2. Service A → Service B 호출 시 각자 @Transactional 기본(REQUIRED) 달아 놓고 “중첩” 되는 줄 착각. 실제로는 단일 트랜잭션!
  3. Isolation 높이기로 동시성 버그 해결하려다 성능 폭락.

5️⃣ 마무리

  • 전파 옵션은 ”왜 분리해야 하는가?”를 먼저 묻고 설정
  • 격리 수준은 DB 기본값을 믿되, 비즈니스 요구로 올릴 땐 부하 테스트
  • 테스트 코드에서 @Transactional(propagation = Propagation.NOT_SUPPORTED) 로 “진짜 커밋” 상태 검증
반응형
Comments