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로 보내고, 운영팀에 알림 후 수동 처리합니다. 멱등성 보장이 핵심이에요."