분산 트랜잭션의 사가 패턴 - Go 예제 포함

Saga 패턴을 사용한 마이크로 서비스에서의 트랜잭션

Page content

사aga 패턴은 보상 작업을 갖춘 로컬 트랜잭션의 일련의 단계로 분산 트랜잭션을 분해함으로써 우아한 해결책을 제공합니다.

서비스 간 작업을 차단할 수 있는 분산 잠금에 의존하는 대신, Saga는 가역적 단계의 시퀀스를 통해 최종 일관성을 가능하게 하여 장기 실행 비즈니스 프로세스에 이상적입니다.

마이크로 서비스 아키텍처에서 서비스 간 데이터 일관성을 유지하는 것은 가장 어려운 문제 중 하나입니다. 작업이 독립적인 데이터베이스를 가진 여러 서비스를 넘나드는 경우 전통적인 ACID 트랜잭션은 작동하지 않으며, 이는 개발자들이 데이터 무결성을 보장하기 위한 대안적인 접근 방식을 모색하도록 만듭니다.

이 가이드는 오케스트레이션과 안무(Choreography) 접근법을 모두 다루는 실용적인 예제와 함께 Go에서 Saga 패턴 구현을 설명합니다. Go 기본 사항에 대한 빠른 참조가 필요하시면 Go 치트 시트에서 유용한 개요를 확인하실 수 있습니다.

construction worker with distributed transactions 이 멋진 이미지는 AI 모델 Flux 1 dev로 생성되었습니다.

Saga 패턴 이해하기

Saga 패턴은 1987년 Hector Garcia-Molina와 Kenneth Salem에 의해 처음 설명되었습니다. 마이크로 서비스의 맥락에서, 이는 각 트랜잭션이 단일 서비스 내에서 데이터를 업데이트하는 로컬 트랜잭션의 시퀀스입니다. 만약 어떤 단계가 실패하면, 이전 단계의 효과를 되돌리기 위해 보상 트랜잭션이 실행됩니다.

두 단계 커밋(2PC)을 사용하는 전통적인 분산 트랜잭션과 달리, Saga는 서비스 간 잠금을 유지하지 않으므로 장기 실행 비즈니스 프로세스에 적합합니다. 여기서 트레이드오프는 강한 일관성 대신 최종 일관성입니다.

주요 특징

  • 분산 잠금 없음: 각 서비스는 자체 로컬 트랜잭션을 관리합니다
  • 보상 작업: 모든 작업에는 해당하는 롤백 메커니즘이 있습니다
  • 최종 일관성: 시스템은 결국 일관된 상태에 도달합니다
  • 장기 실행: 초, 분, 심지어 시간 단위의 프로세스에 적합합니다

Saga 구현 접근법

Saga 패턴을 구현하는 두 가지 주요 접근법은 오케스트레이션과 안무(Choreography)입니다.

오케스트레이션 패턴

오케스트레이션에서는 중앙 조정자(오케스트레이터)가 전체 트랜잭션 흐름을 관리합니다. 오케스트레이터는 다음과 같은 역할을 합니다:

  • 올바른 순서로 서비스를 호출
  • 실패 처리 및 보상 트리거
  • Saga의 상태 유지
  • 재시도 및 시간 초과 조정

장점:

  • 중앙 집중식 제어 및 가시성
  • 이해 및 디버깅이 쉬움
  • 더 나은 오류 처리 및 복구
  • 전체 흐름의 단순화된 테스트

단점:

  • 단일 장애점(SPOF) (비록 이는 완화될 수 있음)
  • 유지 관리가 필요한 추가 서비스
  • 복잡한 흐름에 대해 병목 현상이 발생할 수 있음

Go 예제:

type OrderSagaOrchestrator struct {
    orderService    OrderService
    paymentService  PaymentService
    inventoryService InventoryService
    shippingService ShippingService
}

func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
    sagaID := generateSagaID()
    
    // Step 1: Create order
    orderID, err := o.orderService.Create(order)
    if err != nil {
        return err
    }
    
    // Step 2: Reserve inventory
    if err := o.inventoryService.Reserve(order.Items); err != nil {
        o.orderService.Cancel(orderID) // Compensate
        return err
    }
    
    // Step 3: Process payment
    paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
    if err != nil {
        o.inventoryService.Release(order.Items) // Compensate
        o.orderService.Cancel(orderID)          // Compensate
        return err
    }
    
    // Step 4: Create shipment
    if err := o.shippingService.CreateShipment(orderID); err != nil {
        o.paymentService.Refund(paymentID)      // Compensate
        o.inventoryService.Release(order.Items) // Compensate
        o.orderService.Cancel(orderID)          // Compensate
        return err
    }
    
    return nil
}

안무(Choreography) 패턴

안무에서는 중앙 조정자가 없습니다. 각 서비스는 무엇을 해야 하는지 알고 이벤트를 통해 통신합니다. 서비스는 이벤트를 수신하고 그에 따라 반응합니다. 이러한 이벤트 기반 접근법은 AWS Kinesis와 같은 메시지 스트리밍 플랫폼과 결합될 때 특히 강력하며, 이는 마이크로 서비스 간 이벤트 배포를 위한 확장 가능한 인프라를 제공합니다. Kinesis를 사용한 이벤트 기반 마이크로 서비스 구현에 대한 포괄적인 가이드는 다음을 참조하십시오. AWS Kinesis를 사용한 이벤트 기반 마이크로 서비스 구축.

장점:

  • 탈중앙화 및 확장성
  • 단일 장애점 없음
  • 서비스가 느슨하게 결합됨
  • 이벤트 기반 아키텍처에 자연스러운 적합성

단점:

  • 전체 흐름을 이해하기 어려움
  • 디버깅 및 추적 어려움
  • 복잡한 오류 처리
  • 순환 의존성 위험

이벤트 기반 아키텍처 예제:

// Order Service
type OrderService struct {
    eventBus EventBus
    repo     OrderRepository
}

func (s *OrderService) CreateOrder(order Order) (string, error) {
    orderID, err := s.repo.Save(order)
    if err != nil {
        return "", err
    }
    
    s.eventBus.Publish("OrderCreated", OrderCreatedEvent{
        OrderID:    orderID,
        CustomerID: order.CustomerID,
        Items:      order.Items,
        Total:      order.Total,
    })
    
    return orderID, nil
}

func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
    return s.repo.Cancel(event.OrderID) // Compensation
}

// Payment Service
type PaymentService struct {
    eventBus EventBus
    client   PaymentClient
}

func (s *PaymentService) HandleOrderCreated(event OrderCreatedEvent) {
    paymentID, err := s.client.Charge(event.CustomerID, event.Total)
    if err != nil {
        s.eventBus.Publish("PaymentFailed", PaymentFailedEvent{
            OrderID: event.OrderID,
        })
        return
    }
    
    s.eventBus.Publish("PaymentSucceeded", PaymentSucceededEvent{
        OrderID:   event.OrderID,
        PaymentID: paymentID,
    })
}

func (s *PaymentService) HandleInventoryReservationFailed(event InventoryReservationFailedEvent) error {
    // Compensation: refund payment
    return s.client.Refund(event.PaymentID)
}

보상 전략

보상은 Saga 패턴의 핵심입니다. 각 작업은 그 효과를 역전시킬 수 있는 해당하는 보상을 가져야 합니다.

보상의 유형

  1. 가역적 작업: 직접적으로 되돌릴 수 있는 작업

    • 예: 예약된 재고 해제, 결제 환불
  2. 보상 작업: 역 효과를 달성하는 다른 작업

    • 예: 삭제 대신 주문 취소
  3. 비관적 보상: 해제할 수 있는 리소스를 사전 할당

    • 예: 결제 전 재고 예약
  4. 낙관적 보상: 작업을 실행하고 필요 시 보상

    • 예: 결제 먼저 하되, 재고가 없으면 환불

멱등성 요구 사항

모든 작업 및 보상은 멱등적이어야 합니다. 이는 실패한 작업을 재시도했을 때 중복된 효과가 발생하지 않도록 보장합니다.

func (s *PaymentService) Refund(paymentID string) error {
    // Check if already refunded
    payment, err := s.getPayment(paymentID)
    if err != nil {
        return err
    }
    
    if payment.Status == "refunded" {
        return nil // Already refunded, idempotent
    }
    
    // Process refund
    return s.processRefund(paymentID)
}

모범 사례

1. Saga 상태 관리

진행을 추적하고 복구를 활성화하기 위해 각 Saga 인스턴스의 상태를 유지하십시오. Saga 상태를 데이터베이스에 영구 저장할 때, 올바른 ORM을 선택하는 것은 성능과 유지 보수성에 중요합니다. PostgreSQL 기반 구현의 경우, Saga 상태 저장소 요구 사항에 가장 적합한 것을 선택하기 위해 PostgreSQL용 Go ORM 비교: GORM vs Ent vs Bun vs sqlc에서 비교를 참조하십시오:

type SagaState struct {
    ID           string
    Status       SagaStatus
    Steps        []SagaStep
    CurrentStep  int
    CreatedAt    time.Time
    UpdatedAt    time.Time
}

type SagaStep struct {
    Service     string
    Operation   string
    Status      StepStatus
    Compensated bool
    Data        map[string]interface{}
}

2. 시간 초과 처리

Saga가 무한정 걸리지 않도록 각 단계에 대한 시간 초과를 구현하십시오:

type SagaOrchestrator struct {
    timeout time.Duration
}

func (o *SagaOrchestrator) ExecuteWithTimeout(step SagaStep) error {
    ctx, cancel := context.WithTimeout(context.Background(), o.timeout)
    defer cancel()
    
    done := make(chan error, 1)
    go func() {
        done <- step.Execute()
    }()
    
    select {
    case err := <-done:
        return err
    case <-ctx.Done():
        // Timeout occurred, compensate
        if err := step.Compensate(); err != nil {
            return fmt.Errorf("compensation failed: %w", err)
        }
        return fmt.Errorf("step %s timed out after %v", step.Name(), o.timeout)
    }
}

3. 재시도 로직

일시적인 실패에 대해 지수 백오프를 구현하십시오:

func retryWithBackoff(operation func() error, maxRetries int) error {
    backoff := time.Second
    for i := 0; i < maxRetries; i++ {
        err := operation()
        if err == nil {
            return nil
        }
        
        if !isTransientError(err) {
            return err
        }
        
        time.Sleep(backoff)
        backoff *= 2
    }
    return fmt.Errorf("operation failed after %d retries", maxRetries)
}

4. Saga 상태를 위한 이벤트 소싱

완전한 감사 추적을 유지하기 위해 이벤트 소싱을 사용하십시오. 이벤트 저장소 및 재생 메커니즘을 구현할 때, Go 제네릭은 타입 안전하고 재사용 가능한 이벤트 처리 코드를 생성하는 데 도움이 됩니다. Go에서 제네릭을 사용하는 고급 패턴에 대해서는 Go 제네릭: 사용 사례 및 패턴을 참조하십시오.

type SagaEvent struct {
    SagaID    string
    EventType string
    Payload   []byte
    Timestamp time.Time
    Version   int64
}

type SagaEventStore struct {
    store EventRepository
}

func (s *SagaEventStore) AppendEvent(sagaID string, eventType string, payload interface{}) error {
    data, err := json.Marshal(payload)
    if err != nil {
        return fmt.Errorf("failed to marshal payload: %w", err)
    }
    
    version, err := s.store.GetNextVersion(sagaID)
    if err != nil {
        return fmt.Errorf("failed to get version: %w", err)
    }
    
    event := SagaEvent{
        SagaID:    sagaID,
        EventType: eventType,
        Payload:   data,
        Timestamp: time.Now(),
        Version:   version,
    }
    
    return s.store.Save(event)
}

func (s *SagaEventStore) ReplaySaga(sagaID string) (*Saga, error) {
    events, err := s.store.GetEvents(sagaID)
    if err != nil {
        return nil, fmt.Errorf("failed to get events: %w", err)
    }
    
    saga := NewSaga()
    for _, event := range events {
        if err := saga.Apply(event); err != nil {
            return nil, fmt.Errorf("failed to apply event: %w", err)
        }
    }
    
    return saga, nil
}

5. 모니터링 및 관찰성

포괄적인 로깅 및 추적을 구현하십시오:

func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
    span := tracer.StartSpan("saga.create_order")
    defer span.Finish()
    
    span.SetTag("saga.id", sagaID)
    span.SetTag("order.id", order.ID)
    
    logger.WithFields(log.Fields{
        "saga_id": sagaID,
        "order_id": order.ID,
        "step": "create_order",
    }).Info("Saga started")
    
    // ... saga execution
    
    return nil
}

일반적인 패턴 및 안티 패턴

따라야 할 패턴

  • Saga 조정자 패턴: 오케스트레이션을 위한 전용 서비스 사용
  • 아웃박스 패턴: 신뢰할 수 있는 이벤트 게시 보장
  • 멱등성 키: 모든 작업에 고유 키 사용
  • Saga 상태 머신: Saga를 상태 머신으로 모델링

피해야 할 안티 패턴

  • 동기적 보상: 보상이 완료될 때까지 기다리지 마십시오
  • 중첩 Saga: Saga가 다른 Saga를 호출하지 마십시오 (대신 서브 Saga를 사용)
  • 공유 상태: Saga 단계 간 상태를 공유하지 마십시오
  • 장기 실행 단계: 너무 오래 걸리는 단계를 분해하십시오

도구 및 프레임워크

다음 프레임워크는 Saga 패턴 구현에 도움이 될 수 있습니다:

  • Temporal: 내장 Saga 지원이 있는 워크플로우 오케스트레이션 플랫폼
  • Zeebe: 마이크로 서비스 오케스트레이션을 위한 워크플로우 엔진
  • Eventuate Tram: Spring Boot용 Saga 프레임워크
  • AWS Step Functions: 서버리스 워크플로우 오케스트레이션
  • Apache Camel: Saga 지원이 있는 통합 프레임워크

관리 및 모니터링을 위한 CLI 인터페이스가 필요한 오케스트레이터 서비스의 경우, Cobra 및 Viper를 사용한 Go CLI 애플리케이션 구축은 Saga 오케스트레이터와 상호 작용하기 위한 명령줄 도구 생성을 위한 훌륭한 패턴을 제공합니다.

Kubernetes에서 Saga 기반 마이크로 서비스를 배포할 때, 서비스 메시를 구현하면 관찰성, 보안 및 트래픽 관리가 크게 향상될 수 있습니다. Istio 및 Linkerd를 사용한 서비스 메시 구현은 서비스 메시가 분산 추적 및 서킷 브레이킹과 같은 교차 관심사를 제공함으로써 분산 트랜잭션 패턴을 어떻게 보완하는지 다룹니다.

Saga 패턴 사용 시기

다음과 같은 경우 Saga 패턴을 사용하십시오:

  • ✅ 작업이 여러 마이크로 서비스를 넘나드는 경우
  • ✅ 장기 실행 비즈니스 프로세스
  • ✅ 최종 일관성이 허용되는 경우
  • ✅ 분산 잠금을 피해야 하는 경우
  • ✅ 서비스가 독립적인 데이터베이스를 가진 경우

다음과 같은 경우 피하십시오:

  • ❌ 강한 일관성이 필요한 경우
  • ✅ 작업이 간단하고 빠른 경우
  • ❌ 모든 서비스가 동일한 데이터베이스를 공유하는 경우
  • ❌ 보상 로직이 너무 복잡한 경우

결론

Saga 패턴은 마이크로 서비스 아키텍처에서 분산 트랜잭션을 관리하는 데 필수적입니다. 복잡성을 도입하기는 하지만, 서비스 경계 간 데이터 일관성을 유지하기 위한 실용적인 해결책을 제공합니다. 더 나은 제어와 가시성을 위해 오케스트레이션을 선택하거나, 확장성과 느슨한 결합을 위해 안무를 선택하십시오. 항상 작업이 멱등적이며, 적절한 보상 로직을 구현하고, 포괄적인 관찰성을 유지하십시오.

성공적인 Saga 구현의 핵심은 일관성 요구 사항을 이해하고, 보상 로직을 신중하게 설계하며, 사용 사례에 맞는 올바른 접근법을 선택하는 것입니다. 올바른 구현을 통해 Saga는 분산 시스템 전반에 데이터 무결성을 유지하는 탄력적이고 확장 가능한 마이크로 서비스를 구축할 수 있게 해줍니다.

유용한 링크

구독하기

시스템, 인프라, AI 엔지니어링에 관한 새 글을 받아보세요.