분산 트랜잭션의 사가 패턴 - Go 예제 포함
Saga 패턴을 사용한 마이크로 서비스에서의 트랜잭션
사aga 패턴은 보상 작업을 갖춘 로컬 트랜잭션의 일련의 단계로 분산 트랜잭션을 분해함으로써 우아한 해결책을 제공합니다.
서비스 간 작업을 차단할 수 있는 분산 잠금에 의존하는 대신, Saga는 가역적 단계의 시퀀스를 통해 최종 일관성을 가능하게 하여 장기 실행 비즈니스 프로세스에 이상적입니다.
마이크로 서비스 아키텍처에서 서비스 간 데이터 일관성을 유지하는 것은 가장 어려운 문제 중 하나입니다. 작업이 독립적인 데이터베이스를 가진 여러 서비스를 넘나드는 경우 전통적인 ACID 트랜잭션은 작동하지 않으며, 이는 개발자들이 데이터 무결성을 보장하기 위한 대안적인 접근 방식을 모색하도록 만듭니다.
이 가이드는 오케스트레이션과 안무(Choreography) 접근법을 모두 다루는 실용적인 예제와 함께 Go에서 Saga 패턴 구현을 설명합니다. Go 기본 사항에 대한 빠른 참조가 필요하시면 Go 치트 시트에서 유용한 개요를 확인하실 수 있습니다.
이 멋진 이미지는 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 패턴의 핵심입니다. 각 작업은 그 효과를 역전시킬 수 있는 해당하는 보상을 가져야 합니다.
보상의 유형
-
가역적 작업: 직접적으로 되돌릴 수 있는 작업
- 예: 예약된 재고 해제, 결제 환불
-
보상 작업: 역 효과를 달성하는 다른 작업
- 예: 삭제 대신 주문 취소
-
비관적 보상: 해제할 수 있는 리소스를 사전 할당
- 예: 결제 전 재고 예약
-
낙관적 보상: 작업을 실행하고 필요 시 보상
- 예: 결제 먼저 하되, 재고가 없으면 환불
멱등성 요구 사항
모든 작업 및 보상은 멱등적이어야 합니다. 이는 실패한 작업을 재시도했을 때 중복된 효과가 발생하지 않도록 보장합니다.
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는 분산 시스템 전반에 데이터 무결성을 유지하는 탄력적이고 확장 가능한 마이크로 서비스를 구축할 수 있게 해줍니다.
유용한 링크
- Microservices Patterns by Chris Richardson
- Saga Pattern - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Documentation
- Go Cheat Sheet
- Go Generics: Use Cases and Patterns
- Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Implementing CQRS in Go
- Building CLI Applications in Go with Cobra & Viper
- Implementing Service Mesh with Istio and Linkerd
- Building Event-Driven Microservices with AWS Kinesis