🏗️ 아키텍처

Saga Pattern

분산 트랜잭션을 관리하는 패턴

📖 상세 설명

Saga 패턴은 마이크로서비스 환경에서 여러 서비스에 걸친 분산 트랜잭션을 관리하는 패턴입니다. 전통적인 2PC(Two-Phase Commit)는 분산 환경에서 성능과 가용성 문제가 있어, Saga는 보상 트랜잭션(Compensating Transaction)을 통해 최종 일관성(Eventual Consistency)을 보장합니다.

Saga는 일련의 로컬 트랜잭션으로 구성됩니다. 각 로컬 트랜잭션은 자신의 데이터베이스를 업데이트하고 이벤트/메시지를 발행하여 다음 트랜잭션을 트리거합니다. 중간에 실패하면 이전 트랜잭션들의 보상 트랜잭션을 역순으로 실행하여 상태를 되돌립니다.

두 가지 구현 방식이 있습니다. Choreography(안무): 각 서비스가 이벤트를 발행/구독하여 자율적으로 협력합니다. 중앙 제어점이 없어 느슨한 결합이지만, 흐름 파악이 어렵습니다. Orchestration(오케스트레이션): 중앙 오케스트레이터가 각 단계를 지시합니다. 흐름이 명확하지만 오케스트레이터가 단일 장애점이 될 수 있습니다.

예시 - 주문 Saga: 1) 주문 생성 → 2) 재고 차감 → 3) 결제 처리 → 4) 배송 예약. 3단계 결제 실패 시: 2단계 보상(재고 복원) → 1단계 보상(주문 취소). 각 서비스는 자신의 보상 로직을 알고 있어야 합니다.

💻 코드 예제

// Saga Orchestration 패턴 구현 (TypeScript)

interface SagaStep<T> {
  name: string;
  execute: (context: T) => Promise<T>;
  compensate: (context: T) => Promise<T>;
}

class SagaOrchestrator<T> {
  private steps: SagaStep<T>[] = [];
  private executedSteps: SagaStep<T>[] = [];

  addStep(step: SagaStep<T>): this {
    this.steps.push(step);
    return this;
  }

  async execute(initialContext: T): Promise<T> {
    let context = initialContext;

    for (const step of this.steps) {
      try {
        console.log(`Executing: ${step.name}`);
        context = await step.execute(context);
        this.executedSteps.push(step);
      } catch (error) {
        console.error(`Failed at: ${step.name}`, error);
        await this.rollback(context);
        throw new SagaError(step.name, error);
      }
    }

    return context;
  }

  private async rollback(context: T): Promise<void> {
    // 역순으로 보상 트랜잭션 실행
    for (const step of [...this.executedSteps].reverse()) {
      try {
        console.log(`Compensating: ${step.name}`);
        await step.compensate(context);
      } catch (error) {
        console.error(`Compensation failed: ${step.name}`, error);
        // 보상 실패 시 알림/수동 개입 필요
      }
    }
  }
}

// 주문 Saga 정의
interface OrderContext {
  orderId: string;
  customerId: string;
  items: Array<{ productId: string; quantity: number }>;
  totalAmount: number;
  inventoryReserved?: boolean;
  paymentId?: string;
  shippingId?: string;
}

const orderSaga = new SagaOrchestrator<OrderContext>()
  .addStep({
    name: 'CreateOrder',
    execute: async (ctx) => {
      const order = await orderService.create({
        customerId: ctx.customerId,
        items: ctx.items,
        status: 'PENDING'
      });
      return { ...ctx, orderId: order.id };
    },
    compensate: async (ctx) => {
      await orderService.cancel(ctx.orderId);
      return ctx;
    }
  })
  .addStep({
    name: 'ReserveInventory',
    execute: async (ctx) => {
      await inventoryService.reserve(ctx.orderId, ctx.items);
      return { ...ctx, inventoryReserved: true };
    },
    compensate: async (ctx) => {
      if (ctx.inventoryReserved) {
        await inventoryService.release(ctx.orderId);
      }
      return ctx;
    }
  })
  .addStep({
    name: 'ProcessPayment',
    execute: async (ctx) => {
      const payment = await paymentService.charge({
        orderId: ctx.orderId,
        customerId: ctx.customerId,
        amount: ctx.totalAmount
      });
      return { ...ctx, paymentId: payment.id };
    },
    compensate: async (ctx) => {
      if (ctx.paymentId) {
        await paymentService.refund(ctx.paymentId);
      }
      return ctx;
    }
  })
  .addStep({
    name: 'ScheduleShipping',
    execute: async (ctx) => {
      const shipping = await shippingService.schedule(ctx.orderId);
      await orderService.updateStatus(ctx.orderId, 'CONFIRMED');
      return { ...ctx, shippingId: shipping.id };
    },
    compensate: async (ctx) => {
      if (ctx.shippingId) {
        await shippingService.cancel(ctx.shippingId);
      }
      return ctx;
    }
  });

// 사용
async function placeOrder(orderData: Omit<OrderContext, 'orderId'>) {
  try {
    const result = await orderSaga.execute(orderData as OrderContext);
    return { success: true, orderId: result.orderId };
  } catch (error) {
    if (error instanceof SagaError) {
      return { success: false, failedAt: error.step, error: error.message };
    }
    throw error;
  }
}

🗣️ 실무 대화 예시

마이크로서비스 설계에서:

"주문 처리가 재고, 결제, 배송 3개 서비스에 걸쳐 있어요. 2PC는 분산 환경에서 너무 느리고 장애 전파 위험이 있어서 Saga 패턴으로 구현합니다. 각 서비스가 자체 DB를 갖고, 실패 시 보상 트랜잭션으로 롤백해요."

기술 면접에서:

"Choreography vs Orchestration 언제 사용하나요?" - "서비스 수가 적고 흐름이 단순하면 Choreography가 좋아요. 서비스 간 이벤트로 자연스럽게 흘러가죠. 복잡한 비즈니스 로직이나 조건 분기가 많으면 Orchestration이 관리하기 쉽습니다."

장애 대응에서:

"보상 트랜잭션도 실패하면 어떡해요?" - "재시도 메커니즘과 Dead Letter Queue가 필요해요. 일정 횟수 재시도 후에도 실패하면 DLQ로 보내고, 운영팀에 알림 후 수동 처리합니다. 멱등성 보장이 핵심이에요."

⚠️ 주의사항

🔗 관련 용어

최종 일관성 Event Sourcing CQRS Message Queue Microservice

📚 더 배우기

📄 microservices.io - Saga Pattern 📄 AWS - Saga Pattern 가이드 📄 Microsoft Azure - Saga 분산 트랜잭션