f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
173 lines
5.1 KiB
Markdown
173 lines
5.1 KiB
Markdown
---
|
|
id: backend-saga-patterns
|
|
title: Saga — 분산 트랜잭션 / 보상
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [backend, saga, distributed-transaction, vibe-coding]
|
|
tech_stack: { language: "TS", applicable_to: ["Backend"] }
|
|
applied_in: []
|
|
aliases: [saga, choreography, orchestration, compensation, distributed transaction]
|
|
---
|
|
|
|
# Saga
|
|
|
|
> 마이크로서비스 = 2PC 못 함. **여러 step 의 트랜잭션 + 실패 시 보상 (compensating)**. **Choreography (이벤트 연쇄) vs Orchestration (중앙 조정자)**. Temporal / AWS Step Functions 가 modern.
|
|
|
|
## 📖 핵심 개념
|
|
- 각 step = 로컬 트랜잭션.
|
|
- 실패 시 compensation = 이전 step 의 반대 작업.
|
|
- Choreography: 서비스 간 이벤트 흐름.
|
|
- Orchestration: 한 saga manager 가 차례로 호출.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### 시나리오: 주문 = 결제 + 재고 + 배송
|
|
```
|
|
1. Payment.charge → 실패 시: 끝
|
|
2. Inventory.reserve → 실패 시: Payment.refund
|
|
3. Shipping.create → 실패 시: Inventory.release + Payment.refund
|
|
4. Order.markComplete
|
|
```
|
|
|
|
### Choreography (이벤트 연쇄)
|
|
```ts
|
|
// payment-service
|
|
on(PaymentCharged) {
|
|
publish(new InventoryReserveRequested({ orderId, items }));
|
|
}
|
|
on(PaymentFailed) {
|
|
publish(new OrderFailed({ orderId, reason: 'payment' }));
|
|
}
|
|
|
|
// inventory-service
|
|
on(InventoryReserveRequested) {
|
|
try {
|
|
await reserve(orderId, items);
|
|
publish(new InventoryReserved({ orderId }));
|
|
} catch {
|
|
publish(new InventoryReserveFailed({ orderId }));
|
|
}
|
|
}
|
|
on(InventoryReserveFailed) {
|
|
publish(new RefundRequested({ orderId })); // 보상 트리거
|
|
}
|
|
```
|
|
|
|
장점: 의존성 적음. 단점: flow 가 코드에 분산 — 추적 어려움.
|
|
|
|
### Orchestration (중앙)
|
|
```ts
|
|
class OrderSaga {
|
|
constructor(private orderId: string) {}
|
|
|
|
async run(): Promise<SagaResult> {
|
|
const compensations: (() => Promise<void>)[] = [];
|
|
try {
|
|
const payment = await Payment.charge(this.orderId);
|
|
compensations.push(() => Payment.refund(payment.id));
|
|
|
|
const reservation = await Inventory.reserve(this.orderId);
|
|
compensations.push(() => Inventory.release(reservation.id));
|
|
|
|
const shipping = await Shipping.create(this.orderId);
|
|
compensations.push(() => Shipping.cancel(shipping.id));
|
|
|
|
await Order.markComplete(this.orderId);
|
|
return { ok: true };
|
|
} catch (e) {
|
|
// 역순 보상
|
|
for (const c of compensations.reverse()) {
|
|
try { await c(); } catch (cerr) { /* 보상 실패 — 알람 */ }
|
|
}
|
|
return { ok: false, error: e };
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Temporal (workflow engine)
|
|
```ts
|
|
import { proxyActivities } from '@temporalio/workflow';
|
|
|
|
const { charge, refund, reserve, release, ship } = proxyActivities<typeof activities>({
|
|
startToCloseTimeout: '1 minute',
|
|
});
|
|
|
|
export async function orderSaga(orderId: string): Promise<void> {
|
|
let paymentId: string | undefined;
|
|
let reservationId: string | undefined;
|
|
|
|
try {
|
|
paymentId = await charge(orderId);
|
|
reservationId = await reserve(orderId);
|
|
await ship(orderId);
|
|
} catch (e) {
|
|
if (reservationId) await release(reservationId);
|
|
if (paymentId) await refund(paymentId);
|
|
throw e;
|
|
}
|
|
}
|
|
```
|
|
|
|
Temporal: 자동 재시도, idempotent activity, history replay, time travel debug.
|
|
|
|
### Idempotent activity
|
|
```ts
|
|
async function charge(orderId: string): Promise<string> {
|
|
// 같은 orderId 로 두 번 호출 — 한 번만 실제 charge
|
|
const existing = await db.payments.find({ orderId });
|
|
if (existing) return existing.id;
|
|
|
|
const id = await stripe.paymentIntents.create({ idempotencyKey: orderId, ... });
|
|
await db.payments.insert({ id, orderId });
|
|
return id;
|
|
}
|
|
```
|
|
|
|
### State machine + persistent
|
|
```ts
|
|
type SagaState =
|
|
| { step: 'INIT' }
|
|
| { step: 'PAYMENT_DONE'; paymentId: string }
|
|
| { step: 'INVENTORY_DONE'; paymentId: string; reservationId: string }
|
|
| { step: 'COMPLETE' }
|
|
| { step: 'COMPENSATING'; compensated: string[] };
|
|
|
|
async function step(state: SagaState): Promise<SagaState> {
|
|
// 한 step 만 진행 후 DB 에 state 저장
|
|
// 실패 / crash 후에도 resume
|
|
}
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| 2-3 step 단순 | Orchestration (코드 안) |
|
|
| 많은 서비스 + 비동기 | Choreography (이벤트) |
|
|
| 강 보장 + 디버깅 | Temporal / Step Functions |
|
|
| 단일 DB | 일반 트랜잭션 |
|
|
| 외부 API 만 | Idempotency + retry |
|
|
| 시간 오래 (며칠) | Temporal / Cadence |
|
|
|
|
## ❌ 안티패턴
|
|
- **2PC 시도**: HTTP / 다른 DB 간 원자성 불가.
|
|
- **보상 누락**: 실패 후 일관성 깨짐.
|
|
- **Idempotency 없음**: 재시도 시 두 번 실행.
|
|
- **State 메모리만 — crash 시 사라짐**: persistent.
|
|
- **순서 보장 가정 (이벤트 큐)**: 항상 보장 X.
|
|
- **Choreography 거대**: flow 추적 불가. orchestration 으로.
|
|
- **보상도 실패 — 무시**: 알람 + 수동 복구.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- 단순 = orchestration in-code.
|
|
- 복잡 = Temporal / Step Functions.
|
|
- 모든 step idempotent + 보상 명시.
|
|
|
|
## 🔗 관련 문서
|
|
- [[Backend_Event_Sourcing]]
|
|
- [[Backend_Outbox_Pattern]]
|