TL;DR

  • 보상 트랜잭션과 Saga 패턴 - 분산 시스템에서 “되돌리기”의 핵심 개념을 빠르게 파악할 수 있다.
  • 배경과 이유를 통해 왜 필요한지 맥락을 이해할 수 있다.
  • 특징과 상세 내용을 통해 실무 적용 포인트를 확인할 수 있다.

1. 개념

보상 트랜잭션과 Saga 패턴 - 분산 시스템에서 “되돌리기”의 핵심 정의와 문제 공간을 간단히 정리한다.

2. 배경

이 주제가 등장한 기술적·조직적 배경과 기존 접근의 한계를 설명한다.

3. 이유

왜 지금 이 방식을 채택해야 하는지, 기대 효과와 트레이드오프를 함께 정리한다.

4. 특징

핵심 동작 방식, 장단점, 적용 시 주의점을 빠르게 훑을 수 있도록 요약한다.

5. 상세 내용

보상 트랜잭션과 Saga 패턴 - 분산 시스템에서 “되돌리기”

작성일: 2026-02-26 카테고리: Backend / Distributed Systems / Transaction 포함 내용: Compensating Transaction, Saga Pattern, 분산 트랜잭션, 2PC, Eventual Consistency, Orchestration, Choreography, 롤백, 멱등성


1. 먼저 이해해야 할 것: 트랜잭션이란?

1.1 트랜잭션 = “다 되거나, 아무것도 안 되거나”

┌─────────────────────────────────────────────────────────────────┐
│              트랜잭션이란 무엇인가?                              │
│                                                                   │
│  트랜잭션(Transaction) = 하나의 작업 단위                        │
│  "전부 성공하거나, 전부 실패해야 하는 작업 묶음"                 │
│                                                                   │
│  비유: 은행 송금                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  A 계좌에서 -10만원                                      │    │
│  │  B 계좌에서 +10만원                                      │    │
│  │                                                          │    │
│  │  ❌ A에서 빠졌는데 B에 안 들어가면?                      │    │
│  │     → 돈이 증발! (10만원이 공중분해)                     │    │
│  │                                                          │    │
│  │  ✅ 트랜잭션을 쓰면?                                     │    │
│  │     → 둘 다 성공하거나, 둘 다 실패 (All or Nothing)      │    │
│  │     → B에 입금 실패 시 A 출금도 자동 취소!               │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  일상 비유:                                                     │
│  ├── 편의점에서 물건 사기 = 트랜잭션                             │
│  │   (돈 주기 + 물건 받기 = 둘 다 되거나 둘 다 안 되거나)       │
│  ├── 돈만 주고 물건 못 받으면? → 거래 취소! (롤백)               │
│  └── 물건만 받고 돈 안 내면? → 그것도 안 됨!                     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

1.2 ACID - 트랜잭션의 4가지 약속

┌─────────────────────────────────────────────────────────────────┐
│            ACID = 트랜잭션이 지켜야 할 4가지 원칙               │
│                                                                   │
│  A = Atomicity (원자성)                                          │
│  ├── "전부 되거나, 전부 안 되거나"                                │
│  ├── 원자(atom)처럼 더 이상 쪼갤 수 없는 단위                    │
│  └── 비유: 스위치 ON/OFF (반만 켤 수 없음)                       │
│                                                                   │
│  C = Consistency (일관성)                                        │
│  ├── "데이터가 항상 올바른 상태"                                  │
│  ├── 규칙 위반하는 데이터가 저장되면 안 됨                       │
│  └── 비유: 잔고가 마이너스가 되면 안 됨                          │
│                                                                   │
│  I = Isolation (격리성)                                          │
│  ├── "동시에 실행되는 작업끼리 서로 안 보임"                      │
│  ├── A가 수정 중인 데이터를 B가 읽으면 안 됨                     │
│  └── 비유: 시험 볼 때 옆 사람 답안지 안 보이게 칸막이            │
│                                                                   │
│  D = Durability (지속성)                                         │
│  ├── "한번 저장(커밋)되면 절대 유실 안 됨"                        │
│  ├── 서버가 꺼져도, 정전이 나도 데이터 보존                     │
│  └── 비유: 계약서에 도장 찍으면 없어지지 않음                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

1.3 DB 트랜잭션의 자동 롤백

┌─────────────────────────────────────────────────────────────────┐
│              DB 트랜잭션 = 자동 되돌리기의 마법                  │
│                                                                   │
│  롤백(ROLLBACK)이란?                                            │
│  ├── "하던 작업을 전부 취소하고 원래 상태로 되돌리기"            │
│  ├── 게임에서 세이브 포인트로 돌아가는 것과 같음                 │
│  └── DB가 알아서 해줌!                                           │
│                                                                   │
│  Spring @Transactional 예시 (Kotlin):                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @Transactional                                          │    │
│  │  fun transfer(from: Long, to: Long, amount: Int) {       │    │
│  │      accountRepository.debit(from, amount)  // 출금       │    │
│  │      accountRepository.credit(to, amount)   // 입금       │    │
│  │      // ↑ 여기서 예외 발생하면?                           │    │
│  │      // → DB가 자동으로 출금도 취소! (ROLLBACK)           │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  동작 원리:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  1. 트랜잭션 시작 (BEGIN)                                │    │
│  │  2. 출금 실행 → "임시 메모"에 기록 (아직 확정 아님)      │    │
│  │  3. 입금 실행 → 실패! 예외 발생!                         │    │
│  │  4. ROLLBACK → "임시 메모"를 버림                        │    │
│  │  5. 결과: 출금도 안 된 상태로 원복                       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  핵심: 이건 "하나의 DB" 안에서만 가능한 마법!                    │
│        DB가 커밋 전까지 데이터를 확정하지 않기 때문              │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2. 문제의 시작: 분산 시스템에서는 자동 롤백이 안 된다

2.1 하나의 DB가 아닌 여러 시스템

┌─────────────────────────────────────────────────────────────────┐
│            마이크로서비스 = 각자 자기 DB를 가짐                  │
│                                                                   │
│  옛날 (모놀리스):                                               │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │    ┌──────────────────────────────┐                      │    │
│  │    │       하나의 큰 서버          │                      │    │
│  │    │  ┌────────┐ ┌────────┐      │                      │    │
│  │    │  │ 주문   │ │ 결제   │      │                      │    │
│  │    │  └────────┘ └────────┘      │                      │    │
│  │    │  ┌────────┐ ┌────────┐      │                      │    │
│  │    │  │ 재고   │ │ 알림   │      │                      │    │
│  │    │  └────────┘ └────────┘      │                      │    │
│  │    └─────────────┬────────────────┘                      │    │
│  │                  │                                        │    │
│  │            ┌─────┴─────┐                                  │    │
│  │            │  하나의 DB │  ← @Transactional 하나로 끝!    │    │
│  │            └───────────┘                                  │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  지금 (마이크로서비스):                                         │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐        │    │
│  │  │ 주문   │  │ 결제   │  │ 재고   │  │ 알림   │        │    │
│  │  │ 서비스 │  │ 서비스 │  │ 서비스 │  │ 서비스 │        │    │
│  │  └───┬────┘  └───┬────┘  └───┬────┘  └───┬────┘        │    │
│  │      │           │           │            │              │    │
│  │  ┌───┴──┐   ┌───┴──┐   ┌───┴──┐    이메일/SMS          │    │
│  │  │ DB 1 │   │ DB 2 │   │ DB 3 │    PG사 API            │    │
│  │  └──────┘   └──────┘   └──────┘    S3 스토리지          │    │
│  │                                                          │    │
│  │  → 각각 다른 DB, 다른 시스템, 다른 네트워크!             │    │
│  │  → @Transactional 하나로 묶을 수 없음!                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2.2 왜 자동 롤백이 안 되는가? (핵심!)

┌─────────────────────────────────────────────────────────────────┐
│          외부 시스템은 "임시 메모"가 없다!                       │
│                                                                   │
│  DB 트랜잭션의 비밀:                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  커밋(COMMIT) 전까지는 실제로 저장되지 않음!             │    │
│  │                                                          │    │
│  │  ① "임시 메모"에 적어둠 (undo log)                       │    │
│  │  ② 모든 작업 성공 → COMMIT → 확정!                       │    │
│  │  ③ 중간에 실패 → ROLLBACK → 임시 메모 버림              │    │
│  │                                                          │    │
│  │  비유: 연필로 적다가 마음에 안 들면 지우개로 지움        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  외부 시스템은 다름:                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  S3에 파일 업로드 → 즉시 올라감! 되돌릴 수 없음!        │    │
│  │  PG사에 결제 요청 → 즉시 결제됨! 자동 취소 안 됨!       │    │
│  │  이메일 발송      → 이미 보냄!   회수 불가!              │    │
│  │  SMS 전송         → 이미 도착!   회수 불가!              │    │
│  │                                                          │    │
│  │  비유: 볼펜으로 쓴 건 지우개로 안 지워짐!                │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  핵심 비유: 편지와 우체통                                       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  DB 트랜잭션 = 아직 편지를 쓰고 있는 상태               │    │
│  │                마음에 안 들면 찢으면 됨 (ROLLBACK)       │    │
│  │                                                          │    │
│  │  외부 호출   = 이미 편지를 우체통에 넣은 상태            │    │
│  │                꺼낼 수 없음! (회수 불가)                 │    │
│  │                → 새 편지를 보내야 함 (보상 트랜잭션)     │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2.3 실제 시나리오로 이해하기

┌─────────────────────────────────────────────────────────────────┐
│           온라인 쇼핑몰 주문 처리 시나리오                      │
│                                                                   │
│  주문 처리 흐름:                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  1단계: 주문 생성 (주문 DB)     → ✅ 성공               │    │
│  │  2단계: 결제 처리 (PG사 API)    → ✅ 성공               │    │
│  │  3단계: 재고 차감 (재고 DB)     → ❌ 실패! (재고 부족)  │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  현재 상황:                                                     │
│  ├── 1단계: 주문이 이미 DB에 저장됨 (되돌려야 함)                │
│  ├── 2단계: 결제가 이미 완료됨 (돈이 빠져나감!) (되돌려야 함)   │
│  └── 3단계: 재고가 없어서 실패 (할 게 없음)                     │
│                                                                   │
│  자동 ROLLBACK? → 불가!                                         │
│  ├── 1단계의 주문 DB와 2단계의 PG사는 별개 시스템                │
│  ├── PG사에 이미 결제된 건 DB가 알아서 취소 못 함                │
│  └── 각 시스템이 "한 몸"이 아니기 때문                           │
│                                                                   │
│  그러면 어떻게?                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "내가 직접 되돌리는 코드"를 작성해야 함!               │    │
│  │                                                          │    │
│  │  → 2단계 되돌리기: PG사 환불 API 호출                    │    │
│  │  → 1단계 되돌리기: 주문 상태를 '취소'로 변경             │    │
│  │                                                          │    │
│  │  이것이 바로 "보상 트랜잭션"!                            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3. 보상 트랜잭션 (Compensating Transaction)

3.1 보상 트랜잭션이란?

┌─────────────────────────────────────────────────────────────────┐
│                    보상 트랜잭션의 정의                          │
│                                                                   │
│  보상 트랜잭션 (Compensating Transaction)                        │
│  = "원래 작업의 효과를 취소하는 역방향 작업"                     │
│  = DB ROLLBACK의 수동 버전                                       │
│  = "이미 보낸 편지를 회수할 수 없으니, 정정 편지를 보내는 것"   │
│                                                                   │
│  비유: 레스토랑에서 주문 취소                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  상황: 스테이크를 주문했는데 취소하고 싶음               │    │
│  │                                                          │    │
│  │  ✅ 아직 요리 시작 전이라면?                              │    │
│  │     → 주방에 "취소요!" 한마디면 끝 (DB ROLLBACK)         │    │
│  │                                                          │    │
│  │  ❌ 이미 고기를 꺼내고 불을 켰다면?                      │    │
│  │     → 자동으로 냉장고에 돌아가지 않음!                   │    │
│  │     → 셰프가 직접 불 끄고, 고기를 냉장고에 넣어야 함    │    │
│  │     → 이게 보상 트랜잭션!                                │    │
│  │                                                          │    │
│  │  더 심한 경우: 이미 요리가 완성됐다면?                   │    │
│  │     → 원재료로 되돌릴 수 없음 (스테이크 → 생고기 불가)  │    │
│  │     → "환불"이라는 다른 방식으로 보상                    │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3.2 각 단계별 보상 트랜잭션 예시

┌─────────────────────────────────────────────────────────────────┐
│            원래 작업 → 보상 트랜잭션 매핑표                     │
│                                                                   │
│  원래 작업                  보상 트랜잭션 (되돌리기)             │
│  ─────────────────────────────────────────────────               │
│  주문 생성 (INSERT)     →  주문 취소                             │
│                             (UPDATE status='CANCELLED')          │
│                                                                   │
│  결제 처리 (PG사 결제)  →  결제 취소                             │
│                             (PG사 환불 API 호출)                 │
│                                                                   │
│  재고 차감 (UPDATE -1)  →  재고 복원                             │
│                             (UPDATE +1)                          │
│                                                                   │
│  S3 파일 업로드         →  S3 파일 삭제                          │
│                             (deleteObject 호출)                  │
│                                                                   │
│  포인트 차감            →  포인트 복원                            │
│                             (포인트 다시 지급)                   │
│                                                                   │
│  쿠폰 사용 처리         →  쿠폰 미사용 처리                     │
│                             (쿠폰 상태 복원)                     │
│                                                                   │
│  이메일 발송            →  ???                                   │
│                             (보상 불가! → "취소 이메일" 전송)    │
│                                                                   │
│  주의: 모든 작업이 "깨끗하게" 되돌려지는 건 아님!               │
│        → 이메일처럼 보상 불가능한 작업도 존재함                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3.3 보상 트랜잭션 vs DB ROLLBACK 비교

┌─────────────────────────────────────────────────────────────────┐
│           보상 트랜잭션 vs DB ROLLBACK 핵심 차이                │
│                                                                   │
│  DB ROLLBACK:                                                    │
│  ├── "하던 일을 취소" (커밋 전이므로 흔적 없음)                  │
│  ├── 아예 안 한 것처럼 됨                                        │
│  ├── DB가 자동으로 해줌                                          │
│  └── 비유: 연필로 쓰다가 지우개로 지움 (깨끗함)                 │
│                                                                   │
│  보상 트랜잭션:                                                 │
│  ├── "이미 한 일을 되돌리는 새로운 일" (흔적이 남음!)            │
│  ├── 원래 기록 + 보상 기록 둘 다 남음                            │
│  ├── 개발자가 직접 코드를 작성해야 함                            │
│  └── 비유: 볼펜으로 쓴 뒤 줄 긋고 옆에 정정 (흔적 남음)         │
│                                                                   │
│  왜 흔적을 남기는가?                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  감사 추적 (Audit Trail)                                 │    │
│  │                                                          │    │
│  │  ❌ 나쁜 방식:                                           │    │
│  │     결제됨 → DELETE (결제 기록 삭제)                      │    │
│  │     → "이 돈이 어디 갔지?" 추적 불가!                    │    │
│  │                                                          │    │
│  │  ✅ 좋은 방식:                                           │    │
│  │     결제됨 → 환불 기록 INSERT                            │    │
│  │     → 원래 결제 기록 + 환불 기록 둘 다 보존              │    │
│  │     → "1월 3일 결제, 1월 5일 환불" 이력 추적 가능        │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  비교 표:                                                       │
│  ┌──────────────┬──────────────────┬──────────────────────┐    │
│  │              │ DB ROLLBACK       │ 보상 트랜잭션         │    │
│  ├──────────────┼──────────────────┼──────────────────────┤    │
│  │ 실행 주체    │ DB가 자동         │ 개발자가 직접 코딩    │    │
│  │ 흔적 유무    │ 없음 (깨끗)      │ 있음 (이력 남음)      │    │
│  │ 실패 가능성  │ 거의 없음        │ 보상 자체가 실패 가능 │    │
│  │ 적용 범위    │ 단일 DB 내       │ 여러 시스템 간        │    │
│  │ 복잡도       │ 매우 낮음        │ 높음                  │    │
│  │ 비유         │ 지우개로 지움    │ 정정 편지를 보냄      │    │
│  └──────────────┴──────────────────┴──────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3.4 보상 트랜잭션의 어려운 점

┌─────────────────────────────────────────────────────────────────┐
│              보상 트랜잭션 구현 시 주의할 점                     │
│                                                                   │
│  문제 1: 보상 자체가 실패하면?                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  상황: 결제는 이미 됐고, 재고 부족으로 주문 실패         │    │
│  │        → 환불(보상) 시도                                 │    │
│  │        → 그런데 PG사 서버가 다운!                        │    │
│  │        → 환불 API 호출 실패!                             │    │
│  │                                                          │    │
│  │  해결:                                                   │    │
│  │  ├── 재시도 (Retry with Exponential Backoff)             │    │
│  │  │   1차: 1초 후 재시도                                  │    │
│  │  │   2차: 2초 후 재시도                                  │    │
│  │  │   3차: 4초 후 재시도                                  │    │
│  │  │   4차: 8초 후 재시도 ...                              │    │
│  │  │                                                       │    │
│  │  ├── 최종 실패 시                                        │    │
│  │  │   → Dead Letter Queue(DLQ)에 저장                     │    │
│  │  │   → 운영팀에 알림                                     │    │
│  │  │   → 수동 개입으로 처리                                │    │
│  │  │                                                       │    │
│  │  └── DLQ(Dead Letter Queue)란?                           │    │
│  │      = "처리 실패한 메시지를 모아두는 곳"                │    │
│  │      = 비유: 배달 불가 우편물 보관소                     │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  문제 2: 같은 보상이 여러 번 실행되면?                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  상황: 네트워크 장애로 환불 응답을 못 받음               │    │
│  │        → "환불 안 된 거 같은데?" → 다시 환불 호출        │    │
│  │        → 실제로는 첫 번째 환불이 성공했었음              │    │
│  │        → 결과: 2번 환불! (고객에게 2배 지급!)            │    │
│  │                                                          │    │
│  │  해결: 멱등성(Idempotency) 보장                         │    │
│  │                                                          │    │
│  │  멱등성이란?                                             │    │
│  │  = "같은 작업을 여러 번 해도 결과가 한 번 한 것과 같음" │    │
│  │  = 비유: 엘리베이터 버튼. 10번 눌러도 1번 누른 것과 같음│    │
│  │                                                          │    │
│  │  구현: 멱등키(Idempotency Key) 사용                     │    │
│  │  ┌─────────────────────────────────────────────────┐    │    │
│  │  │  refund(paymentId="PAY-001",                     │    │    │
│  │  │         idempotencyKey="REFUND-PAY-001")         │    │    │
│  │  │                                                  │    │    │
│  │  │  1차 호출: 환불 실행 → 성공                      │    │    │
│  │  │  2차 호출: "이미 REFUND-PAY-001 처리됨" → 무시  │    │    │
│  │  │  3차 호출: "이미 REFUND-PAY-001 처리됨" → 무시  │    │    │
│  │  │                                                  │    │    │
│  │  │  → 몇 번을 호출해도 환불은 1번만 됨!             │    │    │
│  │  └─────────────────────────────────────────────────┘    │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4. Saga 패턴 - 보상 트랜잭션의 체계적 관리

4.1 Saga란?

┌─────────────────────────────────────────────────────────────────┐
│                       Saga 패턴이란?                             │
│                                                                   │
│  Saga = "긴 트랜잭션을 여러 개의 로컬 트랜잭션으로 쪼개고,     │
│          실패 시 보상 트랜잭션으로 되돌리는 패턴"                │
│                                                                   │
│  기원:                                                          │
│  ├── 1987년, Hector Garcia-Molina & Kenneth Salem 논문          │
│  ├── "SAGAS" 논문에서 최초 제안                                 │
│  └── 원래는 DB 내 장시간 트랜잭션 문제 해결용                    │
│                                                                   │
│  비유: 여행 계획                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  여행 계획:                                              │    │
│  │  ① 항공권 예약 ✅                                       │    │
│  │  ② 호텔 예약   ✅                                       │    │
│  │  ③ 렌터카 예약 ❌ 실패! (차량 없음)                     │    │
│  │                                                          │    │
│  │  여행 자체를 취소해야 한다면?                            │    │
│  │  → 역순으로 되돌리기!                                   │    │
│  │  ② 호텔 취소 (보상)                                     │    │
│  │  ① 항공권 취소 (보상)                                   │    │
│  │                                                          │    │
│  │  각 단계마다 "취소하는 방법"이 미리 정해져 있음!         │    │
│  │  이것이 Saga 패턴의 핵심                                 │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Saga의 구성:                                                   │
│  ├── T1, T2, T3 ... = 각 단계의 로컬 트랜잭션                   │
│  ├── C1, C2, C3 ... = 각 단계의 보상 트랜잭션                   │
│  └── Ti 실패 시 → C(i-1), C(i-2), ..., C1 역순 실행            │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  정상:  T1 → T2 → T3 → T4 (모두 성공!)                 │    │
│  │                                                          │    │
│  │  실패:  T1 → T2 → T3(실패!) → C2 → C1                  │    │
│  │         ────────────────────── ──────────                │    │
│  │         정방향 실행             역방향 보상               │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.2 Saga의 두 가지 실행 방식

4.2.1 Choreography (안무, 이벤트 기반)

┌─────────────────────────────────────────────────────────────────┐
│            Choreography = 이벤트 기반 (안무 방식)               │
│                                                                   │
│  안무(Choreography)란?                                          │
│  ├── 발레 공연에서 각 무용수가 음악에 맞춰 자율적으로 춤       │
│  ├── 중앙에서 지시하는 사람 없음                                 │
│  └── 각자가 "다음에 뭘 할지" 알고 있음                           │
│                                                                   │
│  소프트웨어에서의 Choreography:                                 │
│  ├── 각 서비스가 이벤트를 발행하고, 다음 서비스가 구독          │
│  ├── 중앙 조정자 없음 = 각 서비스가 자율적으로 반응            │
│  └── Kafka, RabbitMQ 같은 메시지 브로커 사용                    │
│                                                                   │
│  비유: 도미노                                                   │
│  ├── 하나가 넘어지면 다음이 알아서 넘어짐                       │
│  ├── 누가 "넘어져라!" 하고 지시하지 않음                         │
│  └── 각 도미노가 자기 역할을 알고 있음                           │
│                                                                   │
│  정상 흐름:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주문 서비스                                             │    │
│  │    │ 주문 생성                                           │    │
│  │    └─ [주문생성됨] 이벤트 발행 ──→ Kafka                │    │
│  │                                          │               │    │
│  │  결제 서비스 (구독)  ◄───────────────────┘               │    │
│  │    │ 결제 처리                                           │    │
│  │    └─ [결제완료됨] 이벤트 발행 ──→ Kafka                │    │
│  │                                          │               │    │
│  │  재고 서비스 (구독)  ◄───────────────────┘               │    │
│  │    │ 재고 차감                                           │    │
│  │    └─ [재고차감됨] 이벤트 발행 ──→ Kafka                │    │
│  │                                          │               │    │
│  │  알림 서비스 (구독)  ◄───────────────────┘               │    │
│  │    └─ 확인 이메일 발송                                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실패 시 (역방향 보상):                                         │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  재고 서비스: 재고 차감 실패!                            │    │
│  │    └─ [재고차감실패] 이벤트 발행 ──→ Kafka              │    │
│  │                                          │               │    │
│  │  결제 서비스 (구독)  ◄───────────────────┘               │    │
│  │    │ 결제 취소 (보상)                                    │    │
│  │    └─ [결제취소됨] 이벤트 발행 ──→ Kafka                │    │
│  │                                          │               │    │
│  │  주문 서비스 (구독)  ◄───────────────────┘               │    │
│  │    └─ 주문 취소 (보상)                                   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  장점:                                                          │
│  ├── 느슨한 결합 (서비스 간 직접 호출 없음)                     │
│  ├── 새 서비스 추가가 쉬움 (이벤트만 구독하면 됨)               │
│  └── 단일 장애 지점 없음                                        │
│                                                                   │
│  단점:                                                          │
│  ├── 전체 흐름 파악이 어려움 (누가 뭘 듣고 있지?)               │
│  ├── 디버깅이 매우 어려움 (이벤트가 어디로 갔지?)               │
│  └── 순환 의존성 위험 (A→B→C→A 이벤트 무한루프)                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.2.2 Orchestration (오케스트레이션, 중앙 조정자)

┌─────────────────────────────────────────────────────────────────┐
│          Orchestration = 중앙 조정자 방식 (지휘자)              │
│                                                                   │
│  오케스트레이션이란?                                            │
│  ├── 오케스트라의 지휘자(Conductor)처럼                         │
│  ├── 한 명이 전체 흐름을 관리하고 지시                          │
│  └── "너 먼저, 그다음 너, 실패하면 되돌려!" 지시                 │
│                                                                   │
│  소프트웨어에서의 Orchestration:                                │
│  ├── Saga Orchestrator라는 "지휘자 서비스"가 존재              │
│  ├── 각 서비스에 "이것 해!" 하고 명령을 보냄                    │
│  └── 실패 감지 시 역순으로 보상 명령을 보냄                     │
│                                                                   │
│  정상 흐름:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │           [Saga Orchestrator] (지휘자)                   │    │
│  │                    │                                      │    │
│  │    ①──→ 주문 서비스: "주문 생성해!" ──→ ✅ 성공         │    │
│  │    ②──→ 결제 서비스: "결제 처리해!" ──→ ✅ 성공         │    │
│  │    ③──→ 재고 서비스: "재고 차감해!" ──→ ✅ 성공         │    │
│  │    ④──→ 알림 서비스: "이메일 보내!" ──→ ✅ 성공         │    │
│  │                                                          │    │
│  │    → Saga 성공 완료!                                     │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실패 시 (역순 보상):                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │           [Saga Orchestrator] (지휘자)                   │    │
│  │                    │                                      │    │
│  │    ①──→ 주문 서비스: "주문 생성해!" ──→ ✅ 성공         │    │
│  │    ②──→ 결제 서비스: "결제 처리해!" ──→ ✅ 성공         │    │
│  │    ③──→ 재고 서비스: "재고 차감해!" ──→ ❌ 실패!        │    │
│  │                                                          │    │
│  │    !! 실패 감지! 역순으로 보상 시작 !!                   │    │
│  │                                                          │    │
│  │    ④──→ 결제 서비스: "결제 취소해!" (보상)               │    │
│  │    ⑤──→ 주문 서비스: "주문 취소해!" (보상)               │    │
│  │                                                          │    │
│  │    → Saga 보상 완료 (모두 원복됨)                        │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  장점:                                                          │
│  ├── 전체 흐름이 한 곳에서 명확하게 보임                        │
│  ├── 디버깅이 쉬움 (Orchestrator 로그만 보면 됨)               │
│  ├── 복잡한 분기/조건 처리 가능                                 │
│  └── 새로운 단계 추가/수정이 Orchestrator만 변경하면 됨        │
│                                                                   │
│  단점:                                                          │
│  ├── Orchestrator가 SPOF(단일 장애 지점)이 될 수 있음          │
│  ├── Orchestrator에 로직이 집중됨 (God Object 위험)             │
│  └── 서비스 간 결합도가 Choreography보다 높음                   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.3 Choreography vs Orchestration 비교표

┌─────────────────────────────────────────────────────────────────┐
│         Choreography vs Orchestration 비교                      │
│                                                                   │
│  ┌────────────────┬──────────────────┬──────────────────────┐  │
│  │                │ Choreography      │ Orchestration         │  │
│  │                │ (안무/이벤트)     │ (지휘자/중앙관리)    │  │
│  ├────────────────┼──────────────────┼──────────────────────┤  │
│  │ 비유           │ 도미노           │ 오케스트라 지휘자     │  │
│  │ 통신 방식      │ 이벤트 발행/구독 │ 명령/응답             │  │
│  │ 중앙 조정자    │ 없음             │ Orchestrator 존재    │  │
│  │ 결합도         │ 낮음 (느슨)      │ 중간                  │  │
│  │ 흐름 파악      │ 어려움           │ 쉬움                  │  │
│  │ 디버깅         │ 어려움           │ 쉬움                  │  │
│  │ 복잡한 분기    │ 어려움           │ 쉬움                  │  │
│  │ 단일 장애점    │ 없음             │ Orchestrator          │  │
│  │ 적합한 경우    │ 단순한 흐름      │ 복잡한 비즈니스 로직 │  │
│  │                │ 서비스 3개 이하  │ 서비스 4개 이상       │  │
│  └────────────────┴──────────────────┴──────────────────────┘  │
│                                                                   │
│  현업에서의 선택 기준:                                          │
│  ├── 단순한 흐름 (2-3단계) → Choreography                      │
│  ├── 복잡한 흐름 (4단계+) → Orchestration                      │
│  ├── 조건 분기가 많으면 → Orchestration                        │
│  └── 팀이 분산되어 있으면 → Choreography                       │
│                                                                   │
│  실무 팁:                                                       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  대부분의 실무 프로젝트에서는 Orchestration을 선택       │    │
│  │  이유: 흐름 파악, 디버깅, 유지보수가 훨씬 쉬움          │    │
│  │  Choreography는 간단한 알림/로깅 같은 부수 효과에 적합   │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5. 왜 이런 것이 나오게 되었는가? (역사적 배경)

5.1 모놀리스 시대 (2000년대 이전)

┌─────────────────────────────────────────────────────────────────┐
│              모놀리스 시대: 아무 문제 없었다                     │
│                                                                   │
│  모놀리스(Monolith)란?                                          │
│  ├── "하나의 거대한 덩어리"라는 뜻                               │
│  ├── 하나의 앱, 하나의 서버, 하나의 DB                           │
│  └── 모든 기능이 한 프로젝트 안에 있음                           │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │   ┌─────────────────────────────────┐                    │    │
│  │   │       하나의 큰 서버             │                    │    │
│  │   │  주문 + 결제 + 재고 + 알림      │                    │    │
│  │   └────────────────┬────────────────┘                    │    │
│  │                    │                                      │    │
│  │              ┌─────┴─────┐                                │    │
│  │              │  하나의 DB │                                │    │
│  │              └───────────┘                                │    │
│  │                                                          │    │
│  │   @Transactional 하나면 끝!                              │    │
│  │   모든 작업이 하나의 DB에서 일어나니까                   │    │
│  │   자동 ROLLBACK이 완벽하게 작동!                         │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  이 시절 트랜잭션은 정말 간단했음:                              │
│  ├── 실패? → DB가 알아서 ROLLBACK                                │
│  ├── 보상 트랜잭션? → 뭐 그게?                                   │
│  └── Saga? → 몰라도 됨!                                         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5.2 2PC (Two-Phase Commit) 시도 (2000년대)

┌─────────────────────────────────────────────────────────────────┐
│          2PC = 여러 DB를 하나처럼 묶으려는 시도                  │
│                                                                   │
│  2PC (Two-Phase Commit)란?                                      │
│  = "2단계 커밋 프로토콜"                                         │
│  = 여러 DB/시스템이 함께 커밋하거나 함께 롤백하게 만드는 방법   │
│                                                                   │
│  비유: 결혼식 서약                                              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  주례(Coordinator): "신랑, 이 여자를 아내로 맞이하시     │    │
│  │                      겠습니까?"                          │    │
│  │  신랑: "네" (Phase 1: 준비 완료)                         │    │
│  │                                                          │    │
│  │  주례: "신부, 이 남자를 남편으로 맞이하시겠습니까?"      │    │
│  │  신부: "네" (Phase 1: 준비 완료)                         │    │
│  │                                                          │    │
│  │  주례: "둘 다 동의했으니, 부부로 선언합니다!"            │    │
│  │        (Phase 2: 커밋!)                                  │    │
│  │                                                          │    │
│  │  만약 신부가 "아니오"라고 했다면?                        │    │
│  │  → 전체 취소! (Phase 2: 롤백!)                           │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  2PC 동작 과정:                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Phase 1 (Prepare, 준비):                                │    │
│  │  ┌──────────────┐                                        │    │
│  │  │ Coordinator  │───"준비됐어?"──→ DB A → "OK!"          │    │
│  │  │   (조정자)   │───"준비됐어?"──→ DB B → "OK!"          │    │
│  │  │              │───"준비됐어?"──→ DB C → "OK!"          │    │
│  │  └──────────────┘                                        │    │
│  │                                                          │    │
│  │  Phase 2 (Commit/Rollback, 확정/취소):                   │    │
│  │  ┌──────────────┐                                        │    │
│  │  │ Coordinator  │───"커밋해!"────→ DB A → ✅             │    │
│  │  │ (모두 OK면)  │───"커밋해!"────→ DB B → ✅             │    │
│  │  │              │───"커밋해!"────→ DB C → ✅             │    │
│  │  └──────────────┘                                        │    │
│  │                                                          │    │
│  │  하나라도 "아니오"면?                                    │    │
│  │  ┌──────────────┐                                        │    │
│  │  │ Coordinator  │───"롤백해!"────→ DB A → 취소           │    │
│  │  │ (하나 실패)  │───"롤백해!"────→ DB B → 취소           │    │
│  │  │              │───"롤백해!"────→ DB C → 취소           │    │
│  │  └──────────────┘                                        │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  2PC의 심각한 문제점:                                           │
│  ├── 느림: 모든 참여자가 응답할 때까지 대기 (블로킹!)            │
│  ├── 가용성 저하: 참여자 하나가 죽으면 전체 멈춤                 │
│  ├── 확장성 한계: 참여자가 많을수록 느려짐                       │
│  ├── 단일 장애점: Coordinator가 죽으면 전부 불확실 상태          │
│  └── 현대 시스템 미지원: NoSQL, 메시지 큐, 외부 API는           │
│       2PC를 아예 지원하지 않음!                                  │
│                                                                   │
│  결론: 2PC는 "이론적으로 좋지만 현실에서 못 쓰는" 방식         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5.3 마이크로서비스 시대 (2010년대~)

┌─────────────────────────────────────────────────────────────────┐
│         마이크로서비스 시대와 Saga의 부상                        │
│                                                                   │
│  마이크로서비스란?                                              │
│  ├── 하나의 큰 앱을 작은 서비스 여러 개로 쪼갬                   │
│  ├── 각 서비스가 자체 DB를 가짐 (Database per Service)           │
│  ├── 서비스 간 통신은 네트워크 (HTTP, gRPC, Kafka)              │
│  └── Netflix, Amazon, 배달의민족 등이 이 방식                    │
│                                                                   │
│  왜 마이크로서비스가 유행했나?                                  │
│  ├── 모놀리스가 너무 커져서 배포가 힘들어짐                     │
│  ├── 한 기능 수정 → 전체 서버 재배포 → 위험!                    │
│  ├── 팀이 커지면서 코드 충돌이 잦아짐                            │
│  └── 서비스별로 독립 배포, 독립 확장 필요                        │
│                                                                   │
│  그래서 생긴 문제:                                              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  각 서비스가 자기 DB만 관리                              │    │
│  │  → 여러 서비스에 걸친 트랜잭션이 불가능!                 │    │
│  │  → 2PC는 현실적으로 불가 (느림, 미지원)                  │    │
│  │  → 그러면 어떻게?                                        │    │
│  │                                                          │    │
│  │  → Saga 패턴 + 보상 트랜잭션이 사실상 표준!             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  시대별 흐름 정리:                                              │
│  ├── 2000년대 이전: 모놀리스 → @Transactional로 충분            │
│  ├── 2000년대:     2PC 시도 → 느리고 확장 안 됨                 │
│  ├── 2010년대~:    마이크로서비스 → Saga가 표준                 │
│  └── 현재:         Saga + Eventual Consistency가 주류           │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5.4 Eventual Consistency (최종 일관성)

┌─────────────────────────────────────────────────────────────────┐
│          Eventual Consistency = 결국에는 맞아진다               │
│                                                                   │
│  일관성(Consistency)의 두 종류:                                 │
│                                                                   │
│  Strong Consistency (강한 일관성):                               │
│  ├── "데이터 변경 즉시 모든 곳에서 동일하게 보임"                │
│  ├── 은행 ATM에서 출금하면 즉시 잔고 반영                       │
│  └── 비유: 칠판에 쓰면 교실의 모든 학생이 즉시 봄              │
│                                                                   │
│  Eventual Consistency (최종 일관성):                             │
│  ├── "지금 당장은 다를 수 있지만, 시간이 지나면 결국 같아짐"    │
│  ├── 즉시 일관성을 포기하는 대신 가용성과 성능을 얻음           │
│  └── 비유: 편지 보내기                                          │
│                                                                   │
│  비유: 은행 이체                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Strong Consistency:                                     │    │
│  │  "A에서 출금 → B에 즉시 입금" (동시에!)                  │    │
│  │  → 편리하지만 시스템이 무거움                            │    │
│  │                                                          │    │
│  │  Eventual Consistency:                                   │    │
│  │  "A에서 출금 → ... 3~5분 후 → B에 입금"                  │    │
│  │  → 즉시는 아니지만, 결국에는 맞아짐                     │    │
│  │  → 대부분의 은행 이체가 실제로 이렇게 동작!             │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Saga와의 관계:                                                 │
│  ├── Saga는 Eventual Consistency를 전제로 함                    │
│  ├── 각 단계가 진행되는 동안 데이터가 "중간 상태"일 수 있음     │
│  │   (주문은 생성됐지만, 결제가 아직 안 된 상태)                │
│  ├── 모든 단계가 완료되면 비로소 일관된 상태가 됨               │
│  └── 실패 시 보상을 통해 일관된 상태로 복원                     │
│                                                                   │
│  CAP 정리 (참고):                                               │
│  ├── 분산 시스템은 C(일관성), A(가용성), P(분할내성) 중         │
│  │   세 개를 동시에 만족할 수 없다는 이론                       │
│  ├── 마이크로서비스는 보통 A+P를 선택 (일관성 양보)             │
│  └── → Eventual Consistency로 타협                              │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6. 실전 구현 패턴

6.1 Orchestration Saga 구현 (Kotlin 예시)

┌─────────────────────────────────────────────────────────────────┐
│           Orchestration Saga 코드 구현 (Kotlin)                 │
│                                                                   │
│  핵심 구조:                                                     │
│  ├── SagaStep: 원래 작업 + 보상 작업을 한 쌍으로 정의           │
│  ├── SagaOrchestrator: 단계를 순서대로 실행                     │
│  └── 실패 시 완료된 단계를 역순으로 보상                         │
│                                                                   │
│  Step 1: Saga 단계 정의                                         │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // 각 단계의 "실행"과 "보상"을 쌍으로 정의              │    │
│  │  data class SagaStep(                                     │    │
│  │      val name: String,                                    │    │
│  │      val id: String                                       │    │
│  │  )                                                        │    │
│  │                                                          │    │
│  │  enum class SagaResult {                                  │    │
│  │      SUCCESS,       // 모든 단계 성공                     │    │
│  │      COMPENSATED    // 실패 후 보상 완료                  │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Step 2: Saga Orchestrator 구현                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  class OrderSaga(                                         │    │
│  │      private val orderService: OrderService,              │    │
│  │      private val paymentService: PaymentService,          │    │
│  │      private val inventoryService: InventoryService       │    │
│  │  ) {                                                      │    │
│  │      fun execute(request: OrderRequest): SagaResult {     │    │
│  │          val completedSteps = mutableListOf<SagaStep>()   │    │
│  │                                                          │    │
│  │          try {                                            │    │
│  │              // Step 1: 주문 생성                         │    │
│  │              val orderId = orderService                   │    │
│  │                  .create(request)                         │    │
│  │              completedSteps.add(                          │    │
│  │                  SagaStep("ORDER", orderId)               │    │
│  │              )                                            │    │
│  │                                                          │    │
│  │              // Step 2: 결제                              │    │
│  │              val paymentId = paymentService               │    │
│  │                  .charge(request.amount)                  │    │
│  │              completedSteps.add(                          │    │
│  │                  SagaStep("PAYMENT", paymentId)           │    │
│  │              )                                            │    │
│  │                                                          │    │
│  │              // Step 3: 재고 차감                         │    │
│  │              inventoryService.deduct(                     │    │
│  │                  request.productId,                       │    │
│  │                  request.quantity                         │    │
│  │              )                                            │    │
│  │              completedSteps.add(                          │    │
│  │                  SagaStep("INVENTORY",                    │    │
│  │                           request.productId)             │    │
│  │              )                                            │    │
│  │                                                          │    │
│  │              return SagaResult.SUCCESS                    │    │
│  │                                                          │    │
│  │          } catch (e: Exception) {                         │    │
│  │              // 실패! 역순으로 보상 실행                  │    │
│  │              compensate(completedSteps.reversed())        │    │
│  │              return SagaResult.COMPENSATED                │    │
│  │          }                                                │    │
│  │      }                                                    │    │
│  │                                                          │    │
│  │      private fun compensate(steps: List<SagaStep>) {      │    │
│  │          for (step in steps) {                            │    │
│  │              try {                                        │    │
│  │                  when (step.name) {                       │    │
│  │                      "ORDER" ->                           │    │
│  │                          orderService.cancel(step.id)     │    │
│  │                      "PAYMENT" ->                         │    │
│  │                          paymentService.refund(step.id)   │    │
│  │                      "INVENTORY" ->                       │    │
│  │                          inventoryService                 │    │
│  │                              .restore(step.id)            │    │
│  │                  }                                        │    │
│  │              } catch (e: Exception) {                     │    │
│  │                  // 보상 실패 시 로그 + 재시도 큐         │    │
│  │                  logger.error("보상 실패: ${step}", e)     │    │
│  │                  retryQueue.enqueue(step)                 │    │
│  │              }                                            │    │
│  │          }                                                │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  동작 흐름:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  성공 시:                                                │    │
│  │  ORDER(생성) → PAYMENT(결제) → INVENTORY(차감)           │    │
│  │  → SagaResult.SUCCESS                                    │    │
│  │                                                          │    │
│  │  INVENTORY 실패 시:                                      │    │
│  │  ORDER(생성) → PAYMENT(결제) → INVENTORY(실패!)          │    │
│  │  → PAYMENT(환불) → ORDER(취소)                           │    │
│  │  → SagaResult.COMPENSATED                                │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.2 S3 + DB 보상 트랜잭션 (실전 시나리오)

┌─────────────────────────────────────────────────────────────────┐
│         S3 + DB 연동 시 보상 트랜잭션 구현                      │
│                                                                   │
│  시나리오: 파일 업로드 서비스                                   │
│  ├── DB에 파일 메타데이터 저장 (파일명, 크기, 경로)             │
│  ├── S3에 실제 파일 업로드                                      │
│  └── 둘 다 성공해야 "진짜 업로드 완료"                          │
│                                                                   │
│  문제 상황:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Case 1: S3 성공 → DB 실패                               │    │
│  │  → S3에 파일은 있는데 DB에 기록이 없음                   │    │
│  │  → "유령 파일" (S3에만 존재하는 미아 파일)               │    │
│  │  → 보상: S3 파일 삭제                                    │    │
│  │                                                          │    │
│  │  Case 2: DB 성공 → S3 실패                               │    │
│  │  → DB에 기록은 있는데 S3에 파일이 없음                   │    │
│  │  → "깨진 링크" (DB가 가리키는 파일이 없음)              │    │
│  │  → 보상: DB 기록 삭제                                    │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  구현 코드 (Kotlin):                                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  class FileUploadService(                                │    │
│  │      private val s3Client: S3Client,                     │    │
│  │      private val fileRepository: FileRepository          │    │
│  │  ) {                                                      │    │
│  │      fun upload(file: MultipartFile): FileMetadata {      │    │
│  │          val s3Key = generateKey(file)                    │    │
│  │                                                          │    │
│  │          // Step 1: S3에 파일 업로드                      │    │
│  │          try {                                            │    │
│  │              s3Client.putObject(                          │    │
│  │                  bucket, s3Key, file.bytes                │    │
│  │              )                                            │    │
│  │          } catch (e: Exception) {                         │    │
│  │              throw UploadException(                       │    │
│  │                  "S3 업로드 실패", e                      │    │
│  │              )                                            │    │
│  │          }                                                │    │
│  │                                                          │    │
│  │          // Step 2: DB에 메타데이터 저장                  │    │
│  │          try {                                            │    │
│  │              return fileRepository.save(                  │    │
│  │                  FileMetadata(                            │    │
│  │                      name = file.originalFilename,        │    │
│  │                      s3Key = s3Key,                       │    │
│  │                      size = file.size                     │    │
│  │                  )                                        │    │
│  │              )                                            │    │
│  │          } catch (e: Exception) {                         │    │
│  │              // DB 실패! → S3 파일 삭제 (보상)           │    │
│  │              s3Client.deleteObject(bucket, s3Key)         │    │
│  │              throw UploadException(                       │    │
│  │                  "DB 저장 실패, S3 파일 삭제 완료", e     │    │
│  │              )                                            │    │
│  │          }                                                │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  주의: S3 삭제(보상)도 실패할 수 있음!                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  개선된 보상 코드:                                       │    │
│  │                                                          │    │
│  │  } catch (e: Exception) {                                │    │
│  │      try {                                                │    │
│  │          s3Client.deleteObject(bucket, s3Key)             │    │
│  │      } catch (s3Error: Exception) {                      │    │
│  │          // 보상도 실패! → DLQ에 기록                    │    │
│  │          deadLetterQueue.send(                            │    │
│  │              OrphanedFile(bucket, s3Key)                  │    │
│  │          )                                                │    │
│  │          logger.error("S3 보상 실패: $s3Key")            │    │
│  │      }                                                    │    │
│  │      throw UploadException("업로드 실패", e)              │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  전략: "덜 위험한 것 먼저" 원칙                                 │
│  ├── S3 업로드를 먼저 (보상=삭제, 비교적 쉬움)                  │
│  ├── DB 저장을 나중에 (보상=삭제, 더 쉬움)                      │
│  └── 실패 가능성이 높은 작업을 먼저 실행하는 것이 유리          │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.3 멱등성 보장 (Idempotency)

┌─────────────────────────────────────────────────────────────────┐
│               멱등성 = 여러 번 해도 결과가 같음                 │
│                                                                   │
│  멱등성(Idempotency)이란?                                       │
│  = "같은 작업을 1번 하든 100번 하든 결과가 동일한 성질"         │
│                                                                   │
│  일상 비유:                                                     │
│  ├── 멱등한 것:                                                  │
│  │   ├── 엘리베이터 버튼: 10번 눌러도 1번 누른 것과 같음       │
│  │   ├── 전등 스위치 ON: 이미 켜져있으면 아무 일 없음           │
│  │   └── 도어락 잠금: 이미 잠겨있어도 다시 잠그면 그대로        │
│  │                                                               │
│  ├── 멱등하지 않은 것:                                           │
│  │   ├── 은행 입금: 1만원 입금을 2번 하면 2만원이 됨!           │
│  │   ├── 총 발사: 2번 쏘면 총알이 2발 나감!                     │
│  │   └── 이메일 발송: 2번 보내면 2통 도착!                      │
│  │                                                               │
│  └── 보상 트랜잭션에서 멱등성이 중요한 이유:                    │
│      네트워크 장애로 보상이 2번 호출될 수 있기 때문!            │
│                                                                   │
│  멱등성 없을 때의 참사:                                         │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ❌ 멱등성 없는 환불                                     │    │
│  │                                                          │    │
│  │  1차 환불 호출 → 성공 (10만원 환불)                      │    │
│  │  → 네트워크 타임아웃! 응답 못 받음                       │    │
│  │  → "환불 안 된 줄 알고" 재호출                           │    │
│  │  2차 환불 호출 → 또 성공 (10만원 또 환불)                │    │
│  │                                                          │    │
│  │  결과: 20만원 환불됨! (10만원 손해!)                     │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  멱등성 구현 방법: 멱등키(Idempotency Key)                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ✅ 멱등키 적용                                          │    │
│  │                                                          │    │
│  │  // 보상 작업에 고유 ID(멱등키)를 부여                   │    │
│  │  fun refund(paymentId: String,                           │    │
│  │             idempotencyKey: String) {                     │    │
│  │                                                          │    │
│  │      // 이미 처리된 키인지 확인                          │    │
│  │      if (processedKeys.contains(idempotencyKey)) {       │    │
│  │          return  // 이미 처리됨 → 무시!                  │    │
│  │      }                                                    │    │
│  │                                                          │    │
│  │      // 환불 실행                                        │    │
│  │      pgApi.refund(paymentId)                             │    │
│  │                                                          │    │
│  │      // 처리 완료 기록                                   │    │
│  │      processedKeys.add(idempotencyKey)                   │    │
│  │  }                                                        │    │
│  │                                                          │    │
│  │  // 사용:                                                │    │
│  │  refund("PAY-001", "REFUND-PAY-001-20260226")           │    │
│  │  refund("PAY-001", "REFUND-PAY-001-20260226") // 무시!  │    │
│  │  refund("PAY-001", "REFUND-PAY-001-20260226") // 무시!  │    │
│  │                                                          │    │
│  │  → 3번 호출해도 환불은 1번만 실행!                       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  멱등키 저장 방식:                                              │
│  ├── DB 테이블에 저장 (가장 일반적)                              │
│  ├── Redis에 저장 (TTL 설정으로 자동 만료)                      │
│  └── 분산 락(Lock)과 함께 사용                                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7. 보상이 불가능한 경우

7.1 보상 불가능한 작업들

┌─────────────────────────────────────────────────────────────────┐
│               보상할 수 없는 작업이 존재한다                    │
│                                                                   │
│  보상이 불가능한 작업들:                                        │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  이메일/SMS 발송                                         │    │
│  │  ├── 이미 보낸 메시지는 회수 불가                        │    │
│  │  └── 받은 사람의 기억을 지울 수 없음                     │    │
│  │                                                          │    │
│  │  물리적 작업                                             │    │
│  │  ├── 택배가 이미 출발함                                  │    │
│  │  ├── 3D 프린터가 이미 출력 시작                          │    │
│  │  └── 음식이 이미 조리됨                                  │    │
│  │                                                          │    │
│  │  시간 경과 효과                                          │    │
│  │  ├── "30분 무료 체험" → 이미 30분 지남                   │    │
│  │  └── 시간을 되돌릴 수 없음                               │    │
│  │                                                          │    │
│  │  외부 시스템 통지                                        │    │
│  │  ├── 신용정보 기관에 조회 기록 → 삭제 불가               │    │
│  │  └── 제3자 API에 전달된 정보 → 회수 불가                 │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  비유: "엎질러진 물은 다시 담을 수 없다"                        │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7.2 대안: 보상 불가능한 작업 다루기

┌─────────────────────────────────────────────────────────────────┐
│            보상 불가능한 작업의 대안들                           │
│                                                                   │
│  대안 1: 의미적 보상 (Semantic Compensation)                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  원래 작업을 "물리적으로" 되돌리는 건 불가능             │    │
│  │  하지만 "의미적으로" 보정하는 새 작업을 수행             │    │
│  │                                                          │    │
│  │  예시:                                                   │    │
│  │  ├── 주문 확인 이메일 발송 → 주문 취소 이메일 발송      │    │
│  │  ├── 결제 완료 SMS → 환불 안내 SMS                      │    │
│  │  └── 발송 완료 알림 → 반품 접수 안내 알림               │    │
│  │                                                          │    │
│  │  비유: 잘못된 기사 발행 → 정정 기사 발행                 │    │
│  │        (원래 기사를 없앨 순 없지만, 바로잡을 순 있음)    │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  대안 2: 전략적 순서 배치 (가장 중요!)                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "보상 불가능한 작업을 가장 마지막에 배치"               │    │
│  │                                                          │    │
│  │  ❌ 나쁜 순서:                                           │    │
│  │  ① 이메일 발송 (보상 불가!)                             │    │
│  │  ② 결제 처리                                            │    │
│  │  ③ 재고 차감 ← 여기서 실패하면 이메일 회수 불가!       │    │
│  │                                                          │    │
│  │  ✅ 좋은 순서:                                           │    │
│  │  ① 재고 차감 (보상 가능: +1)                            │    │
│  │  ② 결제 처리 (보상 가능: 환불)                          │    │
│  │  ③ 이메일 발송 (보상 불가 → 마지막!)                    │    │
│  │  → 이메일 전에 다른 모든 것이 확정되었으므로 안전!       │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  대안 3: 예약 후 확정 패턴                                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  보상 불가능한 작업을 "예약"과 "확정"으로 분리           │    │
│  │                                                          │    │
│  │  ① 이메일 "예약" (아직 안 보냄, 큐에 넣어둠)            │    │
│  │  ② 결제 처리                                            │    │
│  │  ③ 재고 차감                                            │    │
│  │  ④ 모든 것 성공!                                        │    │
│  │  ⑤ 이메일 "확정" (이제 진짜 보냄!)                      │    │
│  │                                                          │    │
│  │  중간에 실패하면?                                        │    │
│  │  → 이메일 예약만 취소하면 됨 (아직 안 보냈으니!)         │    │
│  │                                                          │    │
│  │  비유: 편지를 우체통에 넣기 전에 봉투에만 넣어두기       │    │
│  │        모든 확인이 끝나면 그때 우체통에 넣기              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  핵심 원칙:                                                     │
│  "편지를 보내기 전에 모든 확인을 끝내자!"                       │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8. 실전에서 자주 쓰이는 Saga 프레임워크/도구

┌─────────────────────────────────────────────────────────────────┐
│              Saga 구현을 도와주는 도구들                         │
│                                                                   │
│  프레임워크/도구란?                                             │
│  ├── Saga 패턴을 직접 처음부터 코딩하면 복잡하고 버그 위험     │
│  ├── 검증된 도구를 사용하면 안전하고 빠르게 구현 가능           │
│  └── 비유: 집을 지을 때 벽돌부터 만들지 않고 기성품을 쓰는 것  │
│                                                                   │
│  1. Axon Framework (Java/Kotlin)                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ├── Saga + Event Sourcing + CQRS 지원                  │    │
│  │  ├── 어노테이션 기반으로 Saga 정의 가능                  │    │
│  │  ├── @SagaEventHandler로 이벤트 매핑                    │    │
│  │  └── Spring Boot와 자연스러운 통합                       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  2. Temporal.io                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ├── 워크플로우 오케스트레이션 플랫폼                    │    │
│  │  ├── 자동 재시도, 타임아웃, 보상을 플랫폼이 관리         │    │
│  │  ├── Java, Go, TypeScript, Python 등 다양한 SDK          │    │
│  │  └── Netflix, Uber, Stripe 등에서 사용                   │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  3. AWS Step Functions                                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ├── AWS 서버리스 환경에서 Saga 구현                     │    │
│  │  ├── 상태 기계(State Machine)로 흐름 정의                │    │
│  │  ├── 시각적 워크플로우 디자이너 제공                     │    │
│  │  └── Lambda 함수와 연결하여 각 단계 실행                 │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  4. Kafka + 이벤트 기반 Choreography                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ├── Kafka 토픽으로 이벤트 발행/구독                     │    │
│  │  ├── 프레임워크 없이 직접 구현하는 방식                  │    │
│  │  ├── 자유도가 높지만 복잡도도 높음                       │    │
│  │  └── Choreography Saga에 적합                            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  5. MicroProfile LRA (Long Running Actions)                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ├── Jakarta EE 표준 기반의 Saga 구현                    │    │
│  │  ├── 어노테이션으로 참여자와 보상 정의                   │    │
│  │  ├── @Compensate로 보상 메서드 지정                     │    │
│  │  └── Quarkus, WildFly 등에서 지원                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  선택 가이드:                                                   │
│  ├── Spring + Kotlin → Axon Framework                          │
│  ├── 복잡한 워크플로우 → Temporal.io                           │
│  ├── AWS 환경 → Step Functions                                 │
│  ├── 이벤트 기반 → Kafka 직접 구현                             │
│  └── Jakarta EE → MicroProfile LRA                             │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9. 실전 설계 체크리스트

┌─────────────────────────────────────────────────────────────────┐
│           Saga 설계 시 반드시 확인할 체크리스트                  │
│                                                                   │
│  □ 1. 각 단계의 보상 트랜잭션이 정의되어 있는가?                │
│     ├── 모든 정방향 작업에 대해 역방향 보상이 있어야 함         │
│     └── 보상 불가능한 작업은 식별하고 대안 마련                  │
│                                                                   │
│  □ 2. 보상 트랜잭션에 멱등성이 보장되는가?                      │
│     ├── 같은 보상이 여러 번 실행되어도 안전한가?                 │
│     └── 멱등키를 사용하고 있는가?                                │
│                                                                   │
│  □ 3. 보상 실패에 대한 대비가 되어 있는가?                      │
│     ├── 재시도 로직 (Exponential Backoff)                        │
│     ├── Dead Letter Queue                                       │
│     └── 수동 개입 알림                                          │
│                                                                   │
│  □ 4. 작업 순서가 최적화되어 있는가?                            │
│     ├── 실패 가능성 높은 것 → 먼저                              │
│     ├── 보상 가능한 것 → 먼저                                   │
│     └── 보상 불가능한 것 → 마지막                               │
│                                                                   │
│  □ 5. 중간 상태가 사용자에게 보이는가?                          │
│     ├── Saga 진행 중일 때 "처리 중" 상태 표시                   │
│     ├── 최종 결과를 비동기로 알림 (이메일, 푸시 등)             │
│     └── 타임아웃 처리 (너무 오래 걸리면 자동 취소?)             │
│                                                                   │
│  □ 6. 모니터링과 추적이 가능한가?                               │
│     ├── 각 Saga 인스턴스의 상태를 추적할 수 있는가?             │
│     ├── 어떤 단계에서 실패했는지 로그가 있는가?                 │
│     └── 보상 진행 상황을 모니터링할 수 있는가?                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

10. 정리

┌─────────────────────────────────────────────────────────────────┐
│                        전체 핵심 정리                            │
│                                                                   │
│  보상 트랜잭션이란?                                             │
│  = "이미 완료된 작업을 되돌리는 역방향 작업"                     │
│  = DB ROLLBACK을 수동으로 구현하는 것                            │
│  = 비유: "이미 보낸 편지를 회수할 수 없으니 정정 편지를 보냄"   │
│                                                                   │
│  Saga 패턴이란?                                                 │
│  = "보상 트랜잭션을 체계적으로 관리하는 패턴"                    │
│  = 긴 트랜잭션을 작은 단계로 쪼개고, 실패 시 역순 보상          │
│  = 비유: 여행 계획 - 각 예약의 "취소 방법"이 미리 정해져 있음  │
│                                                                   │
│  왜 필요한가?                                                   │
│  = 분산 시스템(마이크로서비스)에서 자동 ROLLBACK이 불가능       │
│  = 여러 DB, 외부 API, 메시지 큐 등을 하나로 묶을 수 없기 때문  │
│                                                                   │
│  두 가지 방식:                                                  │
│  ├── Choreography (안무) = 이벤트 기반, 자율적, 느슨한 결합    │
│  └── Orchestration (지휘) = 중앙 조정자, 명확한 흐름, 디버깅    │
│                                                                   │
│  핵심 원칙 5가지:                                               │
│  ├── ① 멱등성: 같은 보상을 여러 번 해도 결과가 동일            │
│  ├── ② 역순 보상: 실패 시 완료된 단계를 역순으로 되돌림        │
│  ├── ③ 보상 불가 → 마지막: 이메일 등은 가장 마지막에 실행     │
│  ├── ④ 재시도: 보상 실패 시 자동 재시도 + DLQ                  │
│  └── ⑤ 감사 추적: DELETE가 아닌 상태 변경으로 이력 보존        │
│                                                                   │
│  시대별 흐름:                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  모놀리스     → @Transactional 하나로 충분                │    │
│  │  2PC 시도     → 느리고 확장 안 됨 (실패)                  │    │
│  │  마이크로서비스 → Saga + 보상 트랜잭션이 표준!             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실무 팁:                                                       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  1. 단순한 흐름(2-3단계) → Choreography                  │    │
│  │  2. 복잡한 흐름(4단계+) → Orchestration                  │    │
│  │  3. 모든 단계에 보상을 미리 정의                          │    │
│  │  4. 멱등키로 중복 실행 방지                               │    │
│  │  5. 보상 실패 대비: 재시도 + DLQ + 수동 개입              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

관련 키워드

보상 트랜잭션, Compensating Transaction, Saga 패턴, 분산 트랜잭션, 2PC, Two-Phase Commit, Eventual Consistency, 최종 일관성, Orchestration, Choreography, 롤백, 멱등성, Idempotency, Dead Letter Queue, 마이크로서비스, ACID