JAVA

MSA와 분산 트랜잭션 — Saga 패턴

algml0703 2026. 6. 26. 15:27
반응형

MSA와 분산 트랜잭션 — Saga 패턴

하나의 서비스를 여러 마이크로서비스로 분리하면, 각 서비스가 자신만의 독립적인 데이터베이스를 갖게 된다. 이때 여러 서비스에 걸친 하나의 비즈니스 흐름(예: 주문 → 결제 → 재고 차감)을 처리하다 중간에 실패하면, 이미 처리된 앞 단계를 어떻게 되돌릴 것인가 하는 문제가 생긴다. 이것이 분산 트랜잭션(Distributed Transaction)의 딜레마이다.

@Transactional이 통하지 않는 이유

단일 DB 환경이라면 가장 바깥 메서드의 @Transactional 하나로 모든 작업을 묶어, 중간에 예외가 나면 전부 깔끔하게 롤백할 수 있다. 하지만 MSA에서는 각 서비스가 독립적인 데이터베이스를 가지고 있어 이 방식이 통하지 않는다.

스프링의 @Transactional단일 데이터베이스 커넥션 안에서만 유효하다. 트랜잭션의 시작·커밋·롤백은 모두 하나의 커넥션을 잡고 그 위에서 일어나기 때문이다. 그런데 서비스마다 물리적으로 다른 DB를 쓰면 커넥션 자체가 제각각이라, 프레임워크가 여러 DB를 하나의 트랜잭션으로 묶을 방법이 없다.

게다가 서비스 간 통신은 트랜잭션이 전파되지 않는 네트워크 호출(HTTP/gRPC)이기 때문에 한 서비스가 자기 트랜잭션을 커밋해 버리면, 그것을 호출한 다른 서비스에게는 이미 커밋된 데이터를 되돌릴 권한 자체가 없다. 결과적으로 한쪽은 커밋이 끝났는데 다른 쪽에서 실패하는 정합성 파괴가 발생하게 된다.

2단계 커밋(2PC) 같은 분산 트랜잭션 프로토콜로 여러 DB를 묶는 방법도 있다. 하지만 2PC는 커밋이 확정될 때까지 모든 참여자를 잠그고 기다리게 만들어 가용성과 성능을 크게 떨어뜨린다. 

Saga 패턴과 보상 트랜잭션

Saga 패턴의 핵심은 하나의 큰 트랜잭션 대신, 각 서비스의 로컬 트랜잭션을 순차적으로 이어 붙이고, 중간에 실패하면 이미 성공한 단계들을 거꾸로 되돌리는 "보상 트랜잭션(Compensating Transaction)"을 실행하는 것이다.

여기서 중요한 점은, 이미 커밋된 데이터를 DB 차원에서 롤백하는 게 아니라 "취소"라는 새로운 비즈니스 행위로 앞선 작업을 상쇄한다는 점이다. 결제가 이미 완료됐다면 DB를 되돌리는 대신 "결제 취소(환불)"라는 별도의 작업을 수행하는 식이다.

정상 흐름 (Forward) 보상 흐름 (Compensation)
주문 생성 (PENDING) 주문 취소 (CANCELED)
결제 완료 결제 취소 (PG 환불)
재고 차감 → 실패! (여기서 보상 시작)

재고 차감 단계에서 실패하면, 앞서 성공했던 결제와 주문을 역순으로 되감는다. 결제는 환불 처리하고, 주문은 상태를 '취소'로 바꿔 전체 정합성을 맞춘다.

Saga를 구현하는 두 가지 방식

① 코레오그래피 (Choreography) — 안무

중앙 통제 없이, 각 서비스가 Kafka·RabbitMQ 같은 비동기 메시지 큐로 이벤트를 주고받으며 알아서 다음 할 일이나 보상 트랜잭션을 수행하는 방식이다. 이벤트에 반응해 움직인다.

// 재고 서비스: 차감 실패 시 '재고 부족' 이벤트를 발행
@KafkaListener(topics = "payment-completed")
public void onPaymentCompleted(PaymentCompletedEvent event) {
    try {
        inventoryService.decrease(event.getProductId(), event.getQuantity());
    } catch (OutOfStockException e) {
        // 보상 트랜잭션을 트리거하는 이벤트 발행
        eventPublisher.publish(new StockFailedEvent(event.getOrderId()));
    }
}

// 결제 서비스: '재고 부족' 이벤트를 듣고 스스로 결제 취소(보상)
@KafkaListener(topics = "stock-failed")
public void onStockFailed(StockFailedEvent event) {
    paymentService.cancel(event.getOrderId());          // PG 환불
    eventPublisher.publish(new PaymentCanceledEvent(event.getOrderId()));
}

// 주문 서비스: '결제 취소' 이벤트를 듣고 주문 상태 변경(보상)
@KafkaListener(topics = "payment-canceled")
public void onPaymentCanceled(PaymentCanceledEvent event) {
    orderService.cancel(event.getOrderId());            // 상태 CANCELED
}

서비스 간 결합도가 매우 낮아 유연하고 확장에 유리하다. 다만 비즈니스 흐름이 여러 서비스에 파편화되어, 전체 트랜잭션의 상태를 한눈에 모니터링하거나 추적하기 어렵다.

② 오케스트레이션 (Orchestration) — 지휘

'Saga 오케스트레이터'라는 중앙 관리 로직이 존재해, "주문 생성해", "결제해", "(실패 시) 결제 취소해"라고 각 서비스에 차례로 명령을 내리는 방식이다. 지휘자가 오케스트라 전체를 통제하는 것과 같다.

// Saga 오케스트레이터: 전체 흐름을 한 곳에서 통제
@RequiredArgsConstructor
public class OrderSagaOrchestrator {

    private final PaymentClient paymentClient;
    private final InventoryClient inventoryClient;
    private final OrderRepository orderRepository;

    public void execute(OrderRequest request) {
        Long orderId = orderRepository.create(request); // 1. 주문 생성

        try {
            paymentClient.pay(orderId);                  // 2. 결제
            inventoryClient.decrease(orderId);           // 3. 재고 차감
            orderRepository.complete(orderId);           // 4. 주문 완료
        } catch (Exception e) {
            // 실패 지점에 따라 역순으로 보상 트랜잭션 실행
            paymentClient.cancel(orderId);               // 결제 취소(환불)
            orderRepository.cancel(orderId);             // 주문 취소
            throw new SagaFailedException(orderId, e);
        }
    }
}

비즈니스 흐름을 중앙에서 직관적으로 통제하고 에러 추적이 쉽다. 다만 오케스트레이터에 로직이 집중되어, 또 다른 형태의 강결합을 유발할 수 있다.

두 방식 비교

항목 코레오그래피 오케스트레이션
제어 주체 각 서비스(이벤트) 중앙 오케스트레이터
결합도 낮음 중앙에 집중
흐름 추적·모니터링 어려움 쉬움
적합한 상황 단계 적고 느슨한 연결 단계 많고 복잡한 흐름

Saga 패턴 적용 시 고려사항

Saga는 즉시 일관성이 아니라 최종 일관성(Eventual Consistency)을 받아들이는 패턴이다. 잠깐이지만 "한 단계는 완료됐는데 보상은 아직 적용 전"인 중간 상태가 존재한다. 이를 안전하게 운영하려면 다음을 반드시 고려해야 한다.

  • 멱등성(Idempotency): 네트워크 재전송으로 같은 이벤트가 두 번 와도 동일한 보상이 중복 실행되지 않도록, 각 작업은 여러 번 실행돼도 결과가 같아야 한다.
  • 보상 실패 대응: 보상 트랜잭션(예: PG 환불)마저 실패할 수 있다. 재시도 큐, 데드레터 큐(DLQ), 수동 처리 알림 같은 안전망이 필요하다.
  • 중간 상태 노출 관리: 최종 확정 전의 중간 상태는 '처리 중'으로 보여주고, 확정된 뒤 결과를 반영해 중간 상태가 사용자 경험을 깨지 않도록 한다.
반응형