티스토리 뷰
작성일: 2025-11-02
카테고리: Design Pattern, Microservices, Distributed Systems
난이도: 중급
TL;DR
- Saga Pattern: 분산 시스템에서 여러 독립 서비스 간 트랜잭션을 보상 트랜잭션으로 관리
- 두 가지 방식: Choreography (이벤트 기반) vs Orchestration (중앙 조정)
- imprun의 선택: State Machine + Timeout으로 충분 (단일 서비스 내 작업)
- 교훈: 패턴보다 문제 이해가 먼저, 복잡한 게 항상 좋은 건 아님
들어가며
imprun.dev는 "API 개발부터 AI 통합까지, 모든 것을 하나로 제공"하는 Kubernetes 기반 API 플랫폼입니다.
API Gateway를 생성할 때 여러 단계의 리소스 생성이 필요합니다:
graph LR
Step1["1. Gateway DB 저장"]
Step2["2. K8s Deployment"]
Step3["3. K8s Service"]
Step4["4. K8s Ingress"]
Step5["5. Environment 활성화"]
Step1 --> Step2
Step2 --> Step3
Step3 --> Step4
Step4 --> Step5
style Step1 stroke:#2563eb,stroke-width:2px
style Step2 stroke:#16a34a,stroke-width:2px
style Step3 stroke:#16a34a,stroke-width:2px
style Step4 stroke:#16a34a,stroke-width:2px
style Step5 stroke:#ea580c,stroke-width:2px
우리가 선택한 방식:
- ✅ State Machine (Phase 전환: Created → Starting → Started)
- ✅ Timeout (5분) + Graceful Degradation
- ✅ 단순하고 충분함
나중에 Saga Pattern을 알게 됨:
- 📚 마이크로서비스 아키텍처 공부 중 발견
- 🤔 비교해보니 우리 케이스엔 불필요
- ✅ 현재 구현으로 충분해서 안 씀
왜 Saga가 불필요했나?:
- 우리: 단일 서비스 내 작업 (MongoDB + Kubernetes API)
- Saga: 여러 독립 서비스 간 분산 트랜잭션용
- 상황이 다름
교훈:
- 패턴을 아는 것보다 문제를 정확히 이해하는 것이 먼저
- 복잡한 패턴이 항상 좋은 건 아님
- 단순함이 최고
이 글은 imprun.dev 플랫폼 구축 경험을 바탕으로, Saga Pattern이 무엇인지, 언제 필요하고 언제 불필요한지를 솔직하게 공유합니다.
Saga Pattern이란?
기본 개념
Saga Pattern: 분산 트랜잭션을 여러 개의 로컬 트랜잭션으로 분할하고, 각 단계마다 보상 트랜잭션(Compensating Transaction)을 정의하는 패턴
핵심 아이디어:
- ACID 트랜잭션 대신 최종 일관성(Eventual Consistency) 달성
- 각 단계는 독립적으로 커밋
- 실패 시 이미 완료된 단계를 보상 트랜잭션으로 롤백
예시: 온라인 주문 시스템
마이크로서비스 구조:
graph TB
Order["주문 서비스"]
Payment["결제 서비스"]
Inventory["재고 서비스"]
Delivery["배송 서비스"]
Order --> Payment
Payment --> Inventory
Inventory --> Delivery
style Order stroke:#2563eb,stroke-width:2px
style Payment stroke:#16a34a,stroke-width:2px
style Inventory stroke:#ea580c,stroke-width:2px
style Delivery stroke:#0891b2,stroke-width:2px
Saga 플로우:
sequenceDiagram
participant Order as 주문 서비스
participant Payment as 결제 서비스
participant Inventory as 재고 서비스
participant Delivery as 배송 서비스
Order->>Order: 1. 주문 생성 ✅
Order->>Payment: 2. 결제 요청
Payment->>Payment: 결제 처리 ✅
Payment->>Inventory: 3. 재고 차감 요청
Inventory->>Inventory: 재고 차감 ✅
Inventory->>Delivery: 4. 배송 요청
Delivery->>Delivery: ❌ 배송 실패!
Note over Delivery: 보상 트랜잭션 시작
Delivery->>Inventory: 재고 복원 요청
Inventory->>Inventory: 재고 복원 ✅
Inventory->>Payment: 결제 취소 요청
Payment->>Payment: 환불 처리 ✅
Payment->>Order: 주문 취소 요청
Order->>Order: 주문 취소 ✅
각 단계의 트랜잭션:
| 단계 | 트랜잭션 | 보상 트랜잭션 |
|---|---|---|
| 1 | 주문 생성 | 주문 취소 |
| 2 | 결제 처리 | 환불 처리 |
| 3 | 재고 차감 | 재고 복원 |
| 4 | 배송 시작 | 배송 취소 |
Saga의 두 가지 구현 방식
1. Choreography (안무, 이벤트 기반)
특징: 각 서비스가 이벤트를 발행/구독하며 독립적으로 동작
아키텍처:
graph TB
subgraph "이벤트 버스"
EventBus["Event Bus<br/>(Kafka, RabbitMQ)"]
end
Order["주문 서비스"]
Payment["결제 서비스"]
Inventory["재고 서비스"]
Delivery["배송 서비스"]
Order -->|OrderCreated| EventBus
EventBus -->|구독| Payment
Payment -->|PaymentCompleted| EventBus
EventBus -->|구독| Inventory
Inventory -->|InventoryReserved| EventBus
EventBus -->|구독| Delivery
Delivery -->|DeliveryFailed| EventBus
EventBus -->|구독| Inventory
Inventory -->|InventoryRestored| EventBus
EventBus -->|구독| Payment
style EventBus stroke:#ea580c,stroke-width:3px
style Order stroke:#2563eb,stroke-width:2px
style Payment stroke:#16a34a,stroke-width:2px
style Inventory stroke:#16a34a,stroke-width:2px
style Delivery stroke:#dc2626,stroke-width:2px
이벤트 플로우:
// 주문 서비스
class OrderService {
async createOrder(order: Order) {
await this.db.orders.insert(order)
// 이벤트 발행
await this.eventBus.publish('OrderCreated', {
orderId: order.id,
userId: order.userId,
items: order.items,
})
}
// 보상 트랜잭션
@Subscribe('PaymentFailed')
async cancelOrder(event: PaymentFailedEvent) {
await this.db.orders.updateOne(
{ id: event.orderId },
{ status: 'Cancelled' }
)
}
}
// 결제 서비스
class PaymentService {
@Subscribe('OrderCreated')
async processPayment(event: OrderCreatedEvent) {
try {
await this.paymentGateway.charge(event.userId, event.amount)
// 성공 이벤트
await this.eventBus.publish('PaymentCompleted', {
orderId: event.orderId,
})
} catch (error) {
// 실패 이벤트 (보상 트랜잭션 트리거)
await this.eventBus.publish('PaymentFailed', {
orderId: event.orderId,
reason: error.message,
})
}
}
// 보상 트랜잭션
@Subscribe('InventoryReservationFailed')
async refundPayment(event: InventoryReservationFailedEvent) {
await this.paymentGateway.refund(event.orderId)
await this.eventBus.publish('PaymentRefunded', {
orderId: event.orderId,
})
}
}
장점:
- ✅ 서비스 간 낮은 결합도
- ✅ 각 서비스가 독립적으로 확장 가능
- ✅ 중앙 Orchestrator 불필요
단점:
- ❌ 전체 플로우 파악 어려움 (이벤트가 흩어져 있음)
- ❌ 순환 의존성 위험
- ❌ 디버깅 복잡 (이벤트 추적 필요)
2. Orchestration (오케스트레이션, 중앙 조정)
특징: 중앙 Orchestrator가 각 단계를 직접 호출하고 관리
아키텍처:
graph TB
Orchestrator["Saga Orchestrator"]
Order["주문 서비스"]
Payment["결제 서비스"]
Inventory["재고 서비스"]
Delivery["배송 서비스"]
Orchestrator -->|1. 주문 생성| Order
Orchestrator -->|2. 결제 요청| Payment
Orchestrator -->|3. 재고 차감| Inventory
Orchestrator -->|4. 배송 시작| Delivery
Delivery -.->|실패| Orchestrator
Orchestrator -.->|보상: 재고 복원| Inventory
Orchestrator -.->|보상: 환불| Payment
Orchestrator -.->|보상: 주문 취소| Order
style Orchestrator stroke:#ea580c,stroke-width:3px
style Order stroke:#2563eb,stroke-width:2px
style Payment stroke:#16a34a,stroke-width:2px
style Delivery stroke:#dc2626,stroke-width:2px,stroke-dasharray: 5 5
Orchestrator 구현:
class OrderSagaOrchestrator {
async executeOrderSaga(order: Order): Promise<SagaResult> {
const saga = new SagaBuilder()
// Step 1: 주문 생성
.addStep({
name: 'CreateOrder',
action: async () => {
return await this.orderService.createOrder(order)
},
compensation: async (orderId) => {
await this.orderService.cancelOrder(orderId)
},
})
// Step 2: 결제 처리
.addStep({
name: 'ProcessPayment',
action: async (orderId) => {
return await this.paymentService.charge(orderId)
},
compensation: async (paymentId) => {
await this.paymentService.refund(paymentId)
},
})
// Step 3: 재고 차감
.addStep({
name: 'ReserveInventory',
action: async (orderId) => {
return await this.inventoryService.reserve(orderId)
},
compensation: async (reservationId) => {
await this.inventoryService.restore(reservationId)
},
})
// Step 4: 배송 시작
.addStep({
name: 'StartDelivery',
action: async (orderId) => {
return await this.deliveryService.start(orderId)
},
compensation: async (deliveryId) => {
await this.deliveryService.cancel(deliveryId)
},
})
.build()
// Saga 실행
return await saga.execute()
}
}
// Saga 실행 엔진
class Saga {
private steps: SagaStep[] = []
private completedSteps: any[] = []
async execute(): Promise<SagaResult> {
try {
// 순차 실행
for (const step of this.steps) {
const result = await step.action(...this.completedSteps)
this.completedSteps.push(result)
}
return { success: true }
} catch (error) {
// 실패 시 보상 트랜잭션 역순 실행
await this.compensate()
return { success: false, error }
}
}
private async compensate() {
// 완료된 단계를 역순으로 보상
for (let i = this.completedSteps.length - 1; i >= 0; i--) {
const step = this.steps[i]
const result = this.completedSteps[i]
try {
await step.compensation(result)
} catch (error) {
// 보상 실패 로깅 (추가 처리 필요)
this.logger.error(`Compensation failed for step ${step.name}`, error)
}
}
}
}
장점:
- ✅ 전체 플로우가 한 곳에 명확히 정의됨
- ✅ 상태 관리 용이 (Orchestrator가 추적)
- ✅ 디버깅/모니터링 쉬움
- ✅ 타임아웃/재시도 정책 중앙 관리
단점:
- ❌ Orchestrator가 단일 실패 지점
- ❌ Orchestrator가 복잡해질 수 있음
- ❌ 서비스 간 결합도 증가
imprun에서 Saga를 안 쓴 이유
Gateway 생성 플로우 분석
imprun의 Gateway 생성 단계:
// instance-task.service.ts
async handleStartingPhase() {
// 1. Gateway DB 조회
const gateway = await this.db.findOne({ phase: 'Starting' })
// 2. Kubernetes Deployment 생성
await this.instanceService.create(gateway.gatewayId)
// 3. Kubernetes 리소스 확인
const instance = await this.instanceService.get(gateway.gatewayId)
// 4. Environment 활성화
await this.db.collection('Environment').updateMany(
{ gatewayId, state: 'Inactive' },
{ $set: { state: 'Active' } }
)
// 5. Gateway 상태 업데이트
await this.db.collection('ApiGateway').updateOne(
{ gatewayId },
{ $set: { phase: 'Started' } }
)
}
Saga가 필요 없는 이유
1. 단일 서비스 내 작업:
- 모든 단계가 동일한 NestJS 서비스 내에서 실행
- MongoDB와 Kubernetes API 호출만 존재
- 분산 트랜잭션이 아님
2. 리소스는 멱등적(Idempotent):
- Kubernetes 리소스: 이미 존재하면 생성 스킵
- MongoDB 업데이트:
updateOne은 멱등적
// ✅ Kubernetes는 멱등적
await k8s.createDeployment(deployment)
// 이미 존재하면 오류 대신 기존 리소스 반환
// ✅ MongoDB 업데이트도 멱등적
await db.updateOne({ gatewayId }, { $set: { phase: 'Started' } })
// 여러 번 실행해도 동일한 결과
3. State Machine으로 충분:
Phase전환으로 명확한 상태 관리- 타임아웃으로 실패 자동 정리
- Graceful Degradation으로 안전한 롤백
// State Machine + Timeout
if (waitingTime > 1000 * 60 * 5) { // 5분 타임아웃
await db.updateOne(
{ gatewayId },
{
$set: {
state: ApiGatewayState.Stopped, // 자동 롤백
phase: ApiGatewayPhase.Started,
},
}
)
}
4. 보상 트랜잭션의 복잡도:
Saga 방식 (과도함):
class GatewayCreationSaga {
async execute(gateway: ApiGateway) {
const saga = new SagaBuilder()
.addStep({
action: () => this.db.insert(gateway),
compensation: () => this.db.delete(gateway),
})
.addStep({
action: () => this.k8s.createDeployment(gateway),
compensation: () => this.k8s.deleteDeployment(gateway),
})
.addStep({
action: () => this.k8s.createService(gateway),
compensation: () => this.k8s.deleteService(gateway),
})
// ... 복잡한 보상 로직
}
}
현재 방식 (충분함):
// 타임아웃 시 자동 정리
if (timeout) {
state = 'Stopped' // 사용자가 재시도 가능
}
// 삭제 시 단순 정리
async deleteGateway(gatewayId: string) {
await this.k8s.deleteDeployment(gatewayId)
await this.k8s.deleteService(gatewayId)
await this.db.delete({ gatewayId })
}
복잡도 vs 실익 비교
| 측면 | Saga Pattern | State Machine + Timeout |
|---|---|---|
| 구현 복잡도 | 높음 (Orchestrator + 보상) | 낮음 (Phase 전환) |
| 유지보수 | 어려움 (단계 추가 시) | 쉬움 (Phase만 추가) |
| 디버깅 | 복잡 (보상 추적) | 간단 (Phase 로그) |
| 성능 | 오버헤드 있음 | 오버헤드 없음 |
| 분산 트랜잭션 | ✅ 지원 | ❌ 단일 서비스만 |
| 멱등성 | 필수 | 자동 (K8s, MongoDB) |
| 실익 | ❌ 과도함 | ✅ 충분함 |
언제 Saga를 사용해야 하나?
Saga가 적합한 경우
1. 여러 독립 서비스 간 트랜잭션:
graph LR
Order["주문 서비스<br/>(독립 DB)"]
Payment["결제 서비스<br/>(독립 DB)"]
Inventory["재고 서비스<br/>(독립 DB)"]
Delivery["배송 서비스<br/>(독립 DB)"]
Order --> Payment
Payment --> Inventory
Inventory --> Delivery
style Order stroke:#2563eb,stroke-width:2px
style Payment stroke:#16a34a,stroke-width:2px
style Inventory stroke:#ea580c,stroke-width:2px
style Delivery stroke:#0891b2,stroke-width:2px
특징:
- ✅ 각 서비스가 독립적인 데이터베이스 소유
- ✅ 서비스 간 직접 DB 접근 불가
- ✅ 강한 일관성보다 최종 일관성 허용 가능
2. 장시간 실행 트랜잭션 (Long-Running Transaction):
주문 승인 → 결제 처리 (외부 API, 3초) →
재고 확인 (레거시 시스템, 5초) →
배송 스케줄링 (외부 업체, 10초)특징:
- ✅ 각 단계가 수초~수분 소요
- ✅ 중간 단계 실패 가능성 높음
- ✅ 전체를 하나의 트랜잭션으로 묶으면 락 시간 과다
3. 비즈니스 프로세스가 복잡한 경우:
여행 예약 = 항공편 예약 + 호텔 예약 + 렌터카 예약 + 투어 예약특징:
- ✅ 각 단계가 별도 서비스/파트너사
- ✅ 중간 실패 시 이미 완료된 단계 취소 필요
- ✅ 보상 트랜잭션이 비즈니스적으로 의미 있음
Saga를 피해야 하는 경우
1. 단일 서비스 내 작업:
- ❌ imprun의 Gateway 생성 (MongoDB + Kubernetes)
- ❌ 로컬 트랜잭션으로 충분
- ✅ 대안: State Machine + Timeout
2. 강한 일관성이 필요한 경우:
- ❌ 금융 거래 (이중 결제 방지)
- ❌ 재고 정확성이 critical한 경우
- ✅ 대안: ACID 트랜잭션 (단일 DB) 또는 2PC
3. 보상 트랜잭션이 불가능한 경우:
- ❌ 이메일 발송 (이미 보낸 이메일은 취소 불가)
- ❌ 외부 API 호출 (보상 API 미제공)
- ✅ 대안: 최종 단계에서만 실행 (Saga 마지막 단계)
4. 간단한 플로우:
- ❌ 2-3단계의 단순 작업
- ❌ 실패 가능성 낮음
- ✅ 대안: 단순 재시도 + 타임아웃
대안 패턴
1. State Machine (imprun 사용 중)
적합한 경우:
- 단일 서비스 내 작업
- 명확한 상태 전환
- 타임아웃으로 실패 처리 가능
예시:
// State: Running, Stopped, Restarting
// Phase: Created, Starting, Started, Stopping, Stopped
// Created → Starting → Started (성공)
// Created → Starting → Started (타임아웃 시 state=Stopped)
2. Outbox Pattern
적합한 경우:
- 이벤트 발행과 DB 트랜잭션의 원자성 필요
- 메시지 유실 방지
예시:
// 트랜잭션 내에서 이벤트를 outbox 테이블에 저장
await db.transaction(async (session) => {
await db.orders.insert(order, { session })
// 이벤트를 outbox에 저장 (트랜잭션 일부)
await db.outbox.insert({
eventType: 'OrderCreated',
payload: order,
published: false,
}, { session })
})
// 별도 프로세스가 outbox 폴링하여 이벤트 발행
setInterval(async () => {
const events = await db.outbox.find({ published: false })
for (const event of events) {
await eventBus.publish(event.eventType, event.payload)
await db.outbox.updateOne({ id: event.id }, { published: true })
}
}, 1000)
3. Event Sourcing
적합한 경우:
- 모든 상태 변경을 이벤트로 기록
- 과거 상태 재구성 필요
- 감사 추적(Audit Trail) 중요
예시:
// 이벤트 스토어에 모든 변경 기록
await eventStore.append('gateway-123', [
{ type: 'GatewayCreated', data: { gatewayId: '123' } },
{ type: 'DeploymentCreated', data: { deploymentName: 'runtime-123' } },
{ type: 'ServiceCreated', data: { serviceName: 'service-123' } },
])
// 현재 상태 = 이벤트 재생
const events = await eventStore.getEvents('gateway-123')
const currentState = events.reduce((state, event) => {
return applyEvent(state, event)
}, initialState)
마무리
핵심 요약
Saga Pattern은 분산 시스템에서 여러 독립 서비스에 걸친 트랜잭션을 관리하는 강력한 패턴입니다. 하지만 복잡도가 높아 모든 경우에 적합하지는 않습니다.
imprun.dev는 Gateway 생성 시 Saga를 고려했지만, 다음 이유로 피했습니다:
- 단일 서비스 내 작업 (MongoDB + Kubernetes)
- 멱등적 리소스 (재시도 안전)
- State Machine + Timeout으로 충분
- 복잡도 대비 실익 낮음
의사결정 플로우
graph TB
Start["트랜잭션 필요?"]
Multi["여러 독립 서비스?"]
LongRunning["장시간 실행?"]
Compensate["보상 가능?"]
Saga["✅ Saga 사용"]
StateMachine["State Machine"]
Transaction["ACID 트랜잭션"]
Retry["단순 재시도"]
Start -->|Yes| Multi
Start -->|No| Retry
Multi -->|Yes| LongRunning
Multi -->|No| StateMachine
LongRunning -->|Yes| Compensate
LongRunning -->|No| Transaction
Compensate -->|Yes| Saga
Compensate -->|No| Transaction
style Saga stroke:#16a34a,stroke-width:3px
style StateMachine stroke:#2563eb,stroke-width:2px
style Transaction stroke:#ea580c,stroke-width:2px
style Retry stroke:#4b5563,stroke-width:2px
언제 Saga를 사용하나?
Saga Pattern 권장:
- ✅ 여러 독립 서비스 간 트랜잭션
- ✅ 장시간 실행 트랜잭션 (수초~수분)
- ✅ 최종 일관성 허용 가능
- ✅ 보상 트랜잭션 정의 가능
State Machine + Timeout 권장 (imprun):
- ✅ 단일 서비스 내 작업
- ✅ 명확한 상태 전환
- ✅ 타임아웃으로 실패 처리 가능
- ✅ 멱등적 리소스
ACID 트랜잭션 권장:
- ✅ 강한 일관성 필요
- ✅ 단일 데이터베이스 내 작업
- ✅ 짧은 실행 시간 (밀리초)
실제 적용 결과
imprun.dev 환경:
- ✅ State Machine으로 충분함
- ✅ 타임아웃(5분)으로 자동 정리
- ✅ 단순하고 명확함
- ✅ 현재 구현으로 만족
운영 경험:
- Gateway 생성 성공률: 98% (타임아웃 2%)
- 평균 생성 시간: 1-2분 (정상 케이스)
- 수동 개입: 거의 없음
참고 자료
논문/아티클
공식 문서
관련 글
태그: #SagaPattern #Microservices #DistributedSystems #StateMachine #DesignPattern
"모든 패턴에는 적절한 사용처가 있다. 중요한 것은 복잡도와 실익을 균형있게 판단하는 것이다."
🤖 이 블로그는 실제 프로덕션 환경에서 Saga Pattern을 검토하고 대안을 선택한 경험을 바탕으로 작성되었습니다.
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| MongoDB 인덱스 생성 베스트 프랙티스: 수동 vs 자동, 그리고 Hybrid 접근 (0) | 2025.11.02 |
|---|---|
| MongoDB 연결 타임아웃 50% 해결기: Connection Pool 분리가 부른 나비효과 (1) | 2025.11.02 |
| API Gateway 생성 실패 대응: Timeout과 Graceful Degradation 패턴 (0) | 2025.11.02 |
| imprun Platform 아키텍처: API 개발부터 AI 통합까지 (0) | 2025.11.02 |
| API Platform의 Consumer 인증 설계: Application-Grant 아키텍처 (0) | 2025.11.02 |
- Total
- Today
- Yesterday
- react
- troubleshooting
- Developer Tools
- Kubernetes
- Go
- ai 개발 도구
- Ontology
- Rag
- authentication
- claude code
- workflow
- PYTHON
- security
- frontend
- knowledge graph
- authorization
- SHACL
- Tax Analysis
- Tailwind CSS
- 개발 도구
- AI agent
- Next.js
- Claude
- architecture
- api gateway
- AI
- AI Development
- LLM
- backend
- LangChain
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |