TL;DR

  • 이 글은 CQRS와 Event Sourcing 완전 가이드의 핵심 개념과 실제 적용 포인트를 빠르게 정리한다.
  • 왜 이 패턴/기법이 등장했는지 배경과 도입 이유를 함께 설명한다.
  • 실무에서 바로 쓰기 위한 특징과 상세 내용을 원문 기반으로 정리한다.

1. 개념

CQRS와 Event Sourcing 완전 가이드의 정의와 핵심 원리를 먼저 이해하면 뒤의 구현 전략을 훨씬 정확하게 판단할 수 있다.

2. 배경

기존 방식의 한계와 운영상의 문제를 해결하기 위해 이 접근이 발전했다.

3. 이유

확장성, 안정성, 유지보수성, 보안을 함께 높이기 위해 이 설계가 필요하다.

4. 특징

핵심 특징은 표준화된 구조, 명확한 책임 분리, 그리고 운영 관점에서의 예측 가능성이다.

5. 상세 내용

CQRS와 Event Sourcing 완전 가이드

작성일: 2026-03-16 카테고리: Backend / Distributed Systems / Architecture Patterns 키워드: CQRS, Command Query Responsibility Segregation, Event Sourcing, Event Store, Projection, Aggregate, DDD, Eventual Consistency, Snapshot, Rehydration, Axon Framework, EventStoreDB, Marten


1. CQRS와 Event Sourcing이란?

1.1 전통적 CRUD 모델의 한계

┌─────────────────────────────────────────────────────────────────┐
│              전통적 CRUD 모델의 구조적 한계                      │
│                                                                 │
│  [전통 CRUD]                                                    │
│                                                                 │
│  Client ──── 읽기/쓰기 ────► ┌──────────────┐                  │
│                               │  Application │                  │
│                               │   (단일 모델) │                  │
│                               └──────┬───────┘                  │
│                                      │                          │
│                               ┌──────▼───────┐                  │
│                               │   Database   │                  │
│                               │ (동일 테이블) │                  │
│                               └──────────────┘                  │
│                                                                 │
│  한계 1: 읽기/쓰기 비율 불균형                                  │
│  ├── 대부분의 시스템: 읽기 90%+ / 쓰기 10% 이하                │
│  ├── 읽기 최적화 = 쓰기 비효율, 쓰기 최적화 = 읽기 비효율      │
│  └── 단일 모델로 양쪽 모두 최적화 불가                          │
│                                                                 │
│  한계 2: "왜 이 상태인가?" 대답 불가                            │
│  ├── 현재 상태만 저장 (UPDATE로 덮어씀)                         │
│  ├── 주문 금액이 왜 변경됐는지 알 수 없음                      │
│  └── 감사 추적(audit trail) 부재                                │
│                                                                 │
│  한계 3: 독립적 확장 불가                                       │
│  ├── 읽기 트래픽 폭증 → 전체 시스템 스케일업 필요              │
│  └── 쓰기 최적화(인덱스 제거) → 읽기 성능 저하                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.2 CQS에서 CQRS로: 개념의 진화

┌─────────────────────────────────────────────────────────────────┐
│              CQS → CQRS: 메서드에서 아키텍처로                  │
│                                                                 │
│  [CQS] Command Query Separation (1988, Bertrand Meyer)          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  "모든 메서드는 Command 또는 Query 중 하나여야 한다"     │    │
│  │                                                          │    │
│  │  Command (명령)                Query (질의)              │    │
│  │  ├── 상태를 변경한다           ├── 상태를 반환한다       │    │
│  │  ├── 아무것도 반환하지 않음    ├── 상태를 변경하지 않음  │    │
│  │  └── 예: setName("Kim")       └── 예: getName()         │    │
│  │                                                          │    │
│  │  범위: 하나의 객체, 메서드 레벨                          │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  [CQRS] Command Query Responsibility Segregation                │
│         (2010, Greg Young)                                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  CQS의 원칙을 아키텍처 수준으로 확장                     │    │
│  │                                                          │    │
│  │  핵심 변화:                                              │    │
│  │  ├── Separation → Segregation (물리적 격리)             │    │
│  │  ├── Responsibility 추가 (SRP에서 차용)                 │    │
│  │  └── 메서드 → 모델/서비스/DB 수준 분리                  │    │
│  │                                                          │    │
│  │  Command Side          Query Side                        │    │
│  │  ┌────────────┐       ┌────────────┐                    │    │
│  │  │ Write Model │       │ Read Model  │                    │    │
│  │  │ (행위 중심) │       │ (표현 중심) │                    │    │
│  │  └──────┬─────┘       └──────┬─────┘                    │    │
│  │         │                    │                            │    │
│  │    ┌────▼────┐         ┌────▼────┐                      │    │
│  │    │Write DB │────────►│Read DB  │                      │    │
│  │    └─────────┘  동기화  └─────────┘                      │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.3 Event Sourcing: 회계 장부에서 소프트웨어로

┌─────────────────────────────────────────────────────────────────┐
│              Event Sourcing = 디지털 복식부기                    │
│                                                                 │
│  [복식부기] (1494, Luca Pacioli)                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  날짜      차변(Debit)    대변(Credit)    잔액           │    │
│  │  3/1       +1,000,000                    1,000,000      │    │
│  │  3/5                     -200,000        800,000        │    │
│  │  3/10      +500,000                      1,300,000      │    │
│  │                                                          │    │
│  │  규칙: 절대 수정하지 않는다. 잘못 기록하면 반대 분개.    │    │
│  │  현재 잔액 = 모든 거래 내역의 합                         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  [Event Sourcing]                                               │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  전통 방식: 현재 상태만 저장                             │    │
│  │  ┌─────────────────────┐                                │    │
│  │  │ Account             │                                │    │
│  │  │ balance: 1,300,000  │  ← 왜 이 금액인지 모름        │    │
│  │  └─────────────────────┘                                │    │
│  │                                                          │    │
│  │  Event Sourcing: 모든 변경을 이벤트로 저장               │    │
│  │  ┌─────────────────────────────────────────────┐        │    │
│  │  │ Event #1: AccountOpened   { amount: 1000000 }│        │    │
│  │  │ Event #2: MoneyWithdrawn  { amount: 200000  }│        │    │
│  │  │ Event #3: MoneyDeposited  { amount: 500000  }│        │    │
│  │  └─────────────────────────────────────────────┘        │    │
│  │  현재 잔액 = 이벤트 순차 적용 결과 = 1,300,000          │    │
│  │                                                          │    │
│  │  Git도 Event Sourcing이다!                               │    │
│  │  ├── commit = event (변경 이력)                          │    │
│  │  ├── working directory = 현재 상태 (projection)          │    │
│  │  └── checkout = rehydration (특정 시점 복원)             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.4 CQRS + Event Sourcing 조합의 의미

┌─────────────────────────────────────────────────────────────────┐
│              CQRS + Event Sourcing = 자연스러운 조합             │
│                                                                 │
│  Greg Young:                                                    │
│  "Event Sourcing 없이 CQRS는 가능하다.                         │
│   CQRS 없이 Event Sourcing은 사실상 불가능하다."               │
│                                                                 │
│  이유:                                                          │
│  ├── ES의 Event Store → 쓰기에 최적화 (append-only)            │
│  ├── 읽기가 느림 (매번 이벤트 재생 필요)                       │
│  └── 별도 Read Model(Projection)이 필수 → 자연스럽게 CQRS     │
│                                                                 │
│  ┌──────────┐  Command  ┌──────────┐  Event   ┌──────────┐    │
│  │  Client  │─────────►│ Command  │────────►│  Event   │    │
│  │          │           │ Handler  │          │  Store   │    │
│  └────┬─────┘           └──────────┘          └────┬─────┘    │
│       │                                            │           │
│       │  Query   ┌──────────┐  Subscribe  ┌───────▼────┐     │
│       └────────►│  Query   │◄────────────│ Projection │     │
│                  │ Handler  │              │  (동기화)   │     │
│                  └────┬─────┘              └────────────┘     │
│                       │                                        │
│                  ┌────▼─────┐                                  │
│                  │ Read DB  │                                  │
│                  │(최적화됨)│                                  │
│                  └──────────┘                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2. 용어 사전

용어 풀네임 / 어원 의미
CQRS Command Query Responsibility Segregation 명령(쓰기)과 질의(읽기)의 책임을 물리적으로 분리하는 아키텍처 패턴
CQS Command Query Separation (Bertrand Meyer, 1988) 메서드 레벨에서 명령과 질의를 분리하는 객체지향 원칙. CQRS의 이론적 기원
Event Sourcing “Sourcing” = Source of Truth에서 유래 상태가 아닌 상태 변경 이벤트를 저장하고, 이벤트를 재생하여 현재 상태를 도출
Event Store Event + Store(저장소) 이벤트를 시간순으로 저장하는 append-only 저장소. Source of Truth
Projection 라틴어 proicere(“앞으로 던지다”), 수학의 투영 이벤트 스트림을 특정 관점의 Read Model로 변환(투영)하는 과정
Read Model - Query Side에서 사용하는 비정규화된 데이터 모델. 논리적 개념
Materialized View Materialize = “실체화하다” Read Model의 물리적 구현. 사전 계산된 뷰를 실제 저장소에 보관
Aggregate 라틴어 aggregare(“모으다”). DDD 용어 일관성 경계를 이루는 도메인 객체 클러스터. 트랜잭션의 단위
Command 라틴어 commandare(“명령하다”) 시스템 상태 변경을 요청하는 의도. 거부될 수 있음
Event 라틴어 eventus(“결과, 발생한 것”) 이미 발생한 사실. 과거형으로 명명. 불변(immutable)
Query 라틴어 quaerere(“묻다, 찾다”) 상태를 조회하는 요청. 부작용 없음
Snapshot 사진 용어 “스냅샷” 특정 시점의 Aggregate 상태를 통째로 저장. Rehydration 성능 최적화
Rehydration 의학 “재수화(re-hydrate)” 저장된 이벤트로부터 Aggregate의 현재 상태를 복원하는 과정
Replay 녹화 “재생” 이벤트를 처음부터 다시 적용하여 상태나 Projection을 재구축
Eventual Consistency Werner Vogels (2007/2008) “결국에는” 모든 노드가 일관된 상태에 도달. 즉각적 일관성의 대안
Optimistic Concurrency “낙관적” 동시성 제어 충돌이 드물다고 가정. expectedVersion으로 쓰기 시점에 충돌 감지
Upcasting 타입 캐스팅에서 유래 오래된 이벤트를 새 스키마로 변환하는 기법
Correlation ID Correlate = “상관시키다” 하나의 비즈니스 흐름에 속한 모든 이벤트를 추적하는 ID
Causation ID Cause = “원인” 이 이벤트를 직접 유발한 이전 이벤트/커맨드의 ID
Fat Event - 소비자가 필요한 모든 데이터를 포함하는 이벤트. 서비스 간 통합용
Thin Event - 최소한의 식별 정보만 포함하는 이벤트. 서비스 내부용
Dead Letter Queue “배달 불능 편지함” 처리 실패한 이벤트를 격리하는 대기열

3. 역사와 학술적 배경

3.1 연대표

┌─────────────────────────────────────────────────────────────────┐
│              CQRS / Event Sourcing 진화 연대표                  │
│                                                                 │
│  1494  Luca Pacioli — 복식부기 체계화                           │
│    │   (Event Sourcing의 원형: append-only 거래 장부)           │
│    │                                                            │
│  1988  Bertrand Meyer — CQS 원칙 (OOSC 초판)                   │
│    │   "메서드는 Command 또는 Query 중 하나여야 한다"           │
│    │                                                            │
│  2000  Eric Brewer — CAP Theorem 추측 (PODC 기조강연)           │
│  2002  Gilbert & Lynch — CAP 정리 증명 (MIT)                    │
│    │                                                            │
│  2003  Eric Evans — "Domain-Driven Design" 출간                 │
│    │   Aggregate, Bounded Context, Repository 패턴 정의         │
│    │                                                            │
│  2005  Martin Fowler — Event Sourcing 패턴 문서화 (12/12)       │
│    │                                                            │
│  2006  Greg Young — CQRS 첫 언급 (ALT.NET 커뮤니티)            │
│    │                                                            │
│  2007  Pat Helland — "Life beyond Distributed Transactions"     │
│    │   (CIDR 2007). 대규모 분산 시스템 트랜잭션 한계 논의       │
│  2007  Werner Vogels — "Eventually Consistent" 초고             │
│  2008  Werner Vogels — ACM Queue 정식 게재                      │
│    │                                                            │
│  2009  Udi Dahan — "Clarified CQRS" (12/9)                      │
│    │   "CQRS ≠ Event Sourcing. 반드시 결합할 필요 없다"         │
│  2009  Axon Framework 탄생 (Allard Buijze, Java)                │
│    │                                                            │
│  2010  Greg Young — "CQRS Documents" 공식 발표 (11월, 56p PDF)  │
│    │                                                            │
│  2012  EventStoreDB 출시 (9/17, Greg Young 직접 개발)           │
│    │                                                            │
│  2013  Reactive Manifesto v1.0 (Jonas Boner 외)                 │
│    │                                                            │
│  2015  Marten 프로젝트 시작 (.NET + PostgreSQL)                 │
│    │   마이크로서비스 아키텍처 대중화                            │
│    │                                                            │
│  2017  "Designing Data-Intensive Applications" 출간 (Kleppmann) │
│  2017  AxonIQ 회사 설립 (Axon Framework 상용화)                 │
│    │                                                            │
│  2018+ Kafka Streams/ES 대중화, Temporal 등장 (2019)            │
│    │                                                            │
│  2022+ 프레임워크 성숙기, 서버리스 ES, AI 에이전트 아키텍처     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.2 학술적 기반

┌─────────────────────────────────────────────────────────────────┐
│              핵심 이론적 토대                                    │
│                                                                 │
│  CAP Theorem (Brewer 2000, Gilbert & Lynch 2002)                │
│  ├── Consistency, Availability, Partition Tolerance              │
│  ├── 네트워크 분할 시 C와 A 중 선택해야 함                     │
│  └── CQRS/ES는 AP 선택 후 Eventual Consistency로 C 보완        │
│                                                                 │
│  BASE (Basically Available, Soft state, Eventually consistent)  │
│  ├── ACID의 대안                                                │
│  └── CQRS Read Model은 Soft State의 전형적 예시                │
│                                                                 │
│  Pat Helland — "Life beyond Distributed Transactions"           │
│  ├── 대규모 시스템에서 분산 트랜잭션은 비현실적                 │
│  ├── "Activity" 단위 + 보상으로 일관성 확보                    │
│  └── CQRS + ES의 이론적 정당성을 제공                          │
│                                                                 │
│  DDD (Eric Evans 2003)                                          │
│  ├── Aggregate = 일관성 경계 = ES의 이벤트 스트림 단위          │
│  ├── Bounded Context = CQRS 적용 범위                           │
│  └── Ubiquitous Language = 이벤트 이름의 근거                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.3 핵심 인물과 기여

인물 기여 핵심 저작
Bertrand Meyer CQS 원칙 창시 Object-Oriented Software Construction (1988/1997)
Eric Evans DDD, Aggregate, Bounded Context Domain-Driven Design (2003)
Greg Young CQRS 공식화, EventStoreDB 개발 CQRS Documents (2010)
Udi Dahan CQRS 명확화, NServiceBus Clarified CQRS (2009)
Martin Fowler Event Sourcing 패턴 문서화 martinfowler.com (2005)
Pat Helland 분산 트랜잭션 한계 이론화 Life beyond Distributed Transactions (2007)
Werner Vogels Eventual Consistency 정립 ACM Queue (2008)
Martin Kleppmann 데이터 중심 아키텍처 종합 Designing Data-Intensive Applications (2017)

4. CQRS 구현 3단계

4.1 Level 1: Simple CQRS (단일 DB, 모델 분리)

┌─────────────────────────────────────────────────────────────────┐
│              Level 1: Simple CQRS                               │
│                                                                 │
│  코드 수준에서 읽기/쓰기 모델 분리. DB는 하나.                 │
│                                                                 │
│           ┌──────────────┐      ┌──────────────┐               │
│  Command ►│ Write Model  │      │  Read Model  │◄ Query        │
│           │ (OrderAggregate)    │ (OrderSummaryDTO)             │
│           └──────┬───────┘      └──────┬───────┘               │
│                  │                     │                        │
│                  └────────┬────────────┘                        │
│                           │                                     │
│                    ┌──────▼──────┐                              │
│                    │  Single DB  │                              │
│                    │ (PostgreSQL)│                              │
│                    └─────────────┘                              │
│                                                                 │
│  장점: 낮은 복잡도, 인프라 변경 없음, 코드 정리 효과           │
│  단점: 독립 스케일링 불가, 성능 개선 제한적                     │
│  적합: CQRS 첫 도입, 복잡한 도메인 모델 정리                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.2 Level 2: Separate Storage (읽기/쓰기 DB 분리)

┌─────────────────────────────────────────────────────────────────┐
│              Level 2: Separate Storage CQRS                     │
│                                                                 │
│  읽기/쓰기 DB를 물리적으로 분리. 동기화 필요.                  │
│                                                                 │
│           ┌──────────────┐      ┌──────────────┐               │
│  Command ►│ Write Model  │      │  Read Model  │◄ Query        │
│           └──────┬───────┘      └──────┬───────┘               │
│                  │                     │                        │
│           ┌──────▼───────┐      ┌──────▼───────┐               │
│           │  Write DB    │      │   Read DB    │               │
│           │ (PostgreSQL) │      │ (Elasticsearch│               │
│           └──────┬───────┘      │  / Redis)    │               │
│                  │              └──────▲───────┘               │
│                  │    동기화          │                        │
│                  └──────────────────────┘                        │
│                  (CDC / Event / Message)                        │
│                                                                 │
│  장점: 독립 스케일링, 읽기 최적화 자유도, 기술 다양성          │
│  단점: Eventual Consistency, 동기화 인프라 필요                 │
│  적합: 읽기 트래픽이 압도적, 읽기에 특화 DB 필요한 경우        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.3 Level 3: CQRS + Event Sourcing (완전한 형태)

┌─────────────────────────────────────────────────────────────────┐
│              Level 3: CQRS + Event Sourcing                     │
│                                                                 │
│  Event Store가 Source of Truth. Projection이 Read Model 생성.  │
│                                                                 │
│  Command ──► ┌──────────────┐                                  │
│              │   Command    │                                  │
│              │   Handler    │                                  │
│              └──────┬───────┘                                  │
│                     │ validate + emit events                   │
│              ┌──────▼───────┐                                  │
│              │  Aggregate   │                                  │
│              │  (도메인)     │                                  │
│              └──────┬───────┘                                  │
│                     │ append                                   │
│              ┌──────▼───────┐                                  │
│              │  Event Store │ ◄── Source of Truth               │
│              │ (append-only)│                                  │
│              └──────┬───────┘                                  │
│                     │ subscribe                                │
│              ┌──────▼───────┐      ┌──────────────┐           │
│              │  Projection  │─────►│   Read DB    │◄── Query  │
│              │  (이벤트→뷰) │      │ (최적화된 뷰) │           │
│              └──────────────┘      └──────────────┘           │
│                                                                 │
│  장점: 완전한 감사 추적, 시간여행, 이벤트 기반 통합            │
│  단점: 높은 복잡도, Eventual Consistency, 학습 곡선            │
│  적합: 금융, 복잡한 도메인, 감사/규제 요구, 이벤트 기반 MSA   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5. Event Sourcing 핵심 메커니즘

5.1 Event Store 구조

┌─────────────────────────────────────────────────────────────────┐
│              Event Store 테이블 구조                             │
│                                                                 │
│  CREATE TABLE events (                                          │
│      event_id        UUID PRIMARY KEY,                          │
│      stream_id       VARCHAR(255) NOT NULL,   -- Aggregate ID  │
│      stream_type     VARCHAR(255) NOT NULL,   -- Aggregate 타입│
│      version         INTEGER NOT NULL,        -- 순서 보장     │
│      event_type      VARCHAR(255) NOT NULL,   -- 이벤트 종류   │
│      data            JSONB NOT NULL,          -- 이벤트 페이로드│
│      metadata        JSONB NOT NULL,          -- 부가 정보     │
│      created_at      TIMESTAMP NOT NULL,                        │
│      UNIQUE(stream_id, version)               -- OCC 핵심!     │
│  );                                                             │
│                                                                 │
│  metadata 예시:                                                 │
│  {                                                              │
│    "correlation_id": "order-flow-abc-123",  -- 비즈니스 흐름   │
│    "causation_id": "cmd-place-order-456",   -- 원인 추적       │
│    "user_id": "user-789",                   -- 누가 발생시켰나 │
│    "timestamp": "2026-03-16T10:30:00Z"                         │
│  }                                                              │
│                                                                 │
│  핵심 규칙:                                                     │
│  ├── Append-only: UPDATE, DELETE 절대 금지                      │
│  ├── stream_id = Aggregate 인스턴스 (예: "order-123")           │
│  ├── version = 같은 stream 내 순서 (1, 2, 3, ...)              │
│  └── UNIQUE(stream_id, version) = Optimistic Concurrency 보장  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.2 Aggregate Rehydration (이벤트 재생)

┌─────────────────────────────────────────────────────────────────┐
│              Rehydration: 이벤트로부터 상태 복원                 │
│                                                                 │
│  1. Event Store에서 이벤트 로드                                 │
│     SELECT * FROM events                                        │
│     WHERE stream_id = 'order-123'                               │
│     ORDER BY version ASC                                        │
│                                                                 │
│  2. 빈 Aggregate 생성 후 순차 적용                              │
│                                                                 │
│     OrderAggregate (empty)                                      │
│         │                                                       │
│         ▼ apply(OrderCreated { id: 123, items: [...] })         │
│     OrderAggregate { status: CREATED, items: [...] }            │
│         │                                                       │
│         ▼ apply(PaymentReceived { amount: 50000 })              │
│     OrderAggregate { status: PAID, amount: 50000 }              │
│         │                                                       │
│         ▼ apply(OrderShipped { tracking: "KR123" })             │
│     OrderAggregate { status: SHIPPED, tracking: "KR123" }       │
│         │                                                       │
│     = 현재 상태 (version 3)                                     │
│                                                                 │
│  의사코드:                                                      │
│  fun rehydrate(streamId):                                       │
│      events = eventStore.load(streamId)                         │
│      aggregate = new OrderAggregate()                           │
│      for event in events:                                       │
│          aggregate.apply(event)                                 │
│      return aggregate                                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.3 Snapshot 전략

┌─────────────────────────────────────────────────────────────────┐
│              Snapshot: Rehydration 성능 최적화                   │
│                                                                 │
│  문제: 이벤트가 수천 개 쌓이면 Rehydration이 느려짐            │
│                                                                 │
│  Snapshot 없이:                                                 │
│  Event #1 → #2 → #3 → ... → #9999 → #10000 → 현재 상태        │
│  (10,000개 이벤트 모두 재생 = 느림)                             │
│                                                                 │
│  Snapshot 적용:                                                 │
│  [Snapshot @v9900] → Event #9901 → ... → #10000 → 현재 상태    │
│  (스냅샷 + 100개만 재생 = 빠름)                                 │
│                                                                 │
│  전략 3가지:                                                    │
│  ├── Every N events: N개마다 자동 스냅샷 (예: 100개마다)       │
│  ├── Time-based: 주기적으로 스냅샷 (예: 매 시간)               │
│  └── On-demand: 특정 조건 시 수동 생성                         │
│                                                                 │
│  Greg Young의 조언:                                             │
│  "이벤트 1000개 이하면 Snapshot이 불필요하다.                   │
│   먼저 측정하라. 대부분은 필요 없다."                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.4 Projection 구축 방식

┌─────────────────────────────────────────────────────────────────┐
│              Projection: 이벤트 → Read Model 변환               │
│                                                                 │
│  [Synchronous Projection]                                       │
│  ├── 이벤트 저장과 동시에 Read Model 업데이트                  │
│  ├── Strong Consistency 가능                                    │
│  └── 쓰기 지연 증가, 장애 전파 위험                            │
│                                                                 │
│  [Asynchronous Projection] ← 권장                               │
│  ├── 이벤트 발행 후 별도 프로세스가 Read Model 업데이트        │
│  ├── Eventual Consistency                                       │
│  └── 쓰기 성능 영향 없음, 독립 장애 격리                       │
│                                                                 │
│  구독 방식:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │  Catch-up Subscription                                   │    │
│  │  ├── "처음부터 따라잡기"                                  │    │
│  │  ├── 마지막 처리 위치(checkpoint) 기억                    │    │
│  │  ├── 재시작 시 checkpoint부터 재개                        │    │
│  │  └── Projection 재구축(replay)에 적합                    │    │
│  │                                                          │    │
│  │  Persistent Subscription                                 │    │
│  │  ├── Event Store가 소비자별 위치 관리                    │    │
│  │  ├── 경쟁 소비자(competing consumers) 지원               │    │
│  │  └── 실시간 처리 + 수평 확장에 적합                      │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.5 Optimistic Concurrency Control

┌─────────────────────────────────────────────────────────────────┐
│              OCC: 낙관적 동시성 제어                             │
│                                                                 │
│  원리: "충돌은 드물다"고 가정하고, 쓰기 시점에 검증             │
│                                                                 │
│  User A: load(order-123) → version 5                            │
│  User B: load(order-123) → version 5                            │
│                                                                 │
│  User A: append(event, expectedVersion=5) → OK (version 6)     │
│  User B: append(event, expectedVersion=5) → CONFLICT!           │
│          (현재 version이 이미 6이므로 거부)                     │
│                                                                 │
│  SQL로 구현:                                                    │
│  INSERT INTO events (stream_id, version, ...)                   │
│  VALUES ('order-123', 6, ...)                                   │
│  -- UNIQUE(stream_id, version) 제약 조건이 충돌 감지!           │
│                                                                 │
│  충돌 시 처리:                                                  │
│  ├── 재시도: 최신 상태 reload → 재검증 → 재시도                │
│  ├── 머지: 두 변경이 호환 가능하면 병합                        │
│  └── 거부: 사용자에게 "다시 시도해주세요" 응답                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.6 Event Versioning

┌─────────────────────────────────────────────────────────────────┐
│              Event Versioning: 스키마 진화 전략                  │
│                                                                 │
│  이벤트는 불변이므로, 스키마가 바뀌면 이전 이벤트와 공존 필요  │
│                                                                 │
│  전략 1: Weak Schema (기본값, 권장)                              │
│  ├── 새 필드 추가 시 기본값 사용                                │
│  ├── 소비자가 없는 필드는 무시                                  │
│  └── JSON의 유연성 활용                                         │
│                                                                 │
│  전략 2: Upcasting                                              │
│  ├── 읽기 시점에 오래된 이벤트를 새 형식으로 변환               │
│  ├── 원본 이벤트는 그대로 유지 (불변성 보장)                   │
│  └── 변환 로직을 파이프라인으로 구성                            │
│      v1 → Upcaster(v1→v2) → Upcaster(v2→v3) → v3             │
│                                                                 │
│  전략 3: Copy-and-Transform                                     │
│  ├── 새 스트림으로 변환된 이벤트를 복사                         │
│  ├── 대규모 마이그레이션에 적합                                 │
│  └── 다운타임 필요, 원본 보존                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6. 대안 비교 분석

6.1 CQRS vs 전통 CRUD

기준 전통 CRUD CQRS
복잡도 낮음 높음 (최소 2배 코드)
일관성 강한 일관성 (ACID) Eventual Consistency (Level 2+)
확장성 수직 확장 위주 읽기/쓰기 독립 수평 확장
읽기 최적화 제한적 (동일 모델) 자유도 높음 (용도별 Read Model)
온보딩 즉시 팀 학습 6-12개월
적합 시나리오 단순 CRUD, 관리 도구 읽기/쓰기 비율 불균형, 복잡 도메인

6.2 CQRS 단독 vs CQRS + Event Sourcing

기준 CQRS 단독 CQRS + ES
Source of Truth 상태 기반 DB Event Store
감사 추적 별도 구현 필요 내장
시간여행 불가 가능 (이벤트 replay)
복잡도 중간 높음
Projection 재구축 불가 언제든 가능
Greg Young 조언 “ES 없이 CQRS를 먼저 시도하라” 감사/시간여행 필요 시

6.3 Event Sourcing vs CDC (Change Data Capture)

기준 Event Sourcing CDC (예: Debezium)
이벤트 수준 비즈니스 도메인 이벤트 DB 물리적 변경 (row-level)
의미 “주문이 생성되었다” “orders 테이블에 INSERT 발생”
애플리케이션 변경 필수 (큰 변경) 최소 (DB 로그 활용)
기존 시스템 적용 어려움 (재설계) 쉬움 (비침투적)
Debezium 권고 - “대부분 CDC+Outbox가 ES보다 나은 대안”

6.4 Event Sourcing vs Audit Log 테이블

기준 Event Sourcing Audit Log 테이블
목적 Source of Truth + 이벤트 기반 아키텍처 법적 감사 기록
상태 복원 가능 (replay) 불가
이벤트 소비 Projection, 통합, 알림 등 조회 전용
복잡도 높음 낮음
선택 기준 이벤트 기반 통합 필요 시 법적 감사만 필요 시

6.5 프레임워크 비교표

프레임워크 언어 특징 성능 (writes/sec)
EventStoreDB 다중 (gRPC) 전용 ES DB, Greg Young 개발, Persistent Subscription ~15K
Axon Framework Java/Spring Annotation 기반, Saga 내장, Axon Server 프레임워크 의존
Marten .NET (C#) PostgreSQL JSONB 활용, 인프라 추가 불필요 조건부 4~15x (vs ESDB)
Akka Persistence Scala/Java Actor 모델, Journal Plugin 아키텍처 Actor 수에 비례
직접 구현 아무 언어 PostgreSQL events 테이블 + JSONB DB 성능에 의존
Kafka (Event Store용) - 안티패턴! Aggregate 단위 조회 불가, OCC 없음 높은 throughput

주의: Kafka는 Event Store가 아니라 Event Bus(전송 계층)로 사용해야 한다. Aggregate 단위 이벤트 조회와 Optimistic Concurrency를 지원하지 않는다.


7. 상황별 최적 선택

7.1 판단 트리

┌─────────────────────────────────────────────────────────────────┐
│              의사결정 플로우차트                                 │
│                                                                 │
│  시작: 새로운 시스템 설계                                       │
│    │                                                            │
│    ▼                                                            │
│  도메인이 단순한가? (CRUD 위주, 비즈니스 규칙 적음)             │
│    ├── YES → 전통 CRUD (CQRS 쓰지 마라)                        │
│    └── NO ──▼                                                   │
│             읽기/쓰기 비율이 극단적인가? (읽기 95%+)            │
│               ├── YES → CQRS 단독 + Read Cache                  │
│               └── NO ──▼                                        │
│                        감사추적/시간여행이 필요한가?             │
│                          ├── NO ──▼                              │
│                          │        복잡한 도메인 로직이 있는가?  │
│                          │          ├── NO → CQRS 단독          │
│                          │          └── YES → CQRS + DDD        │
│                          └── YES ──▼                             │
│                                   기존 시스템인가?              │
│                                     ├── YES → CDC (Debezium)    │
│                                     │         + 점진적 전환     │
│                                     └── NO ──▼                  │
│                                             CQRS + ES           │
│                                             (+ DDD 권장)        │
│                                                                 │
│  부가 판단:                                                     │
│  ├── 스타트업 MVP → CRUD 먼저. 검증 후 전환                    │
│  ├── 법적 감사만 필요 → Audit Log 테이블로 충분                │
│  └── 이벤트 기반 MSA → CQRS + ES + Kafka (전송 계층)           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.2 상황별 가이드

상황 권장 패턴 이유
단순 CRUD 앱 / 관리 도구 전통 CRUD 불필요한 복잡도 회피
읽기 95%+ / 대시보드 CQRS 단독 (Level 1~2) 읽기 최적화만 필요
금융 / 의료 / 법률 CQRS + ES 감사추적 필수, 규제 준수
복잡한 도메인 (보험, 물류) CQRS + ES + DDD 도메인 복잡성 관리
이벤트 기반 MSA CQRS + ES + Kafka 서비스 간 이벤트 통합
기존 시스템 현대화 CDC (Debezium) 비침투적, 점진적 전환
스타트업 MVP CRUD → 추후 전환 빠른 검증 우선

7.3 실제 사례

┌─────────────────────────────────────────────────────────────────┐
│              실제 적용 사례                                      │
│                                                                 │
│  LMAX Exchange (금융 거래소)                                    │
│  ├── 단일 JVM 스레드로 초당 600만 주문 처리                    │
│  ├── Command Sourcing (커맨드 자체를 저장)                     │
│  └── Event Sourcing + 메모리 기반 Projection                   │
│                                                                 │
│  이커머스 (주문 파이프라인)                                     │
│  ├── OrderCreated → PaymentProcessed → OrderShipped            │
│  ├── 각 이벤트가 다음 단계를 트리거                            │
│  └── 다중 Projection: 주문 현황, 매출 통계, 재고 현황          │
│                                                                 │
│  게임 (리플레이/안티치트)                                       │
│  ├── 플레이어 액션을 이벤트로 저장                             │
│  ├── 리플레이: 이벤트 재생으로 게임 재현                       │
│  └── 안티치트: 이벤트 시퀀스 분석으로 부정 행위 탐지           │
│                                                                 │
│  IoT (센서 데이터)                                              │
│  ├── 센서 → Event Store (고속 append)                          │
│  ├── Projection 1: 실시간 대시보드                             │
│  ├── Projection 2: 이상 탐지                                   │
│  └── Projection 3: 일간/월간 집계                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.4 성능 특성

작업 성능 특성 비고
쓰기 append-only (잠금 없음) → 매우 빠름 CRUD의 UPDATE (잠금 필요)보다 유리
읽기 (Projection) pre-computed → 매우 빠름 비정규화된 Read Model 직접 조회
읽기 (Rehydration) 이벤트 수에 비례 10K 이벤트 replay: ~50ms (PostgreSQL)
Snapshot 없이 1M 이벤트 수 초 ~ 수십 초 비실용적. Snapshot 필수
Projection 재구축 전체 이벤트 수에 비례 Blue-Green 방식 권장

8. 베스트 프랙티스

8.1 Event 설계 원칙

┌─────────────────────────────────────────────────────────────────┐
│              Event 설계 원칙                                     │
│                                                                 │
│  1. 과거형으로 명명 (이미 발생한 사실)                          │
│     ├── OK:  OrderPlaced, PaymentReceived, ItemShipped          │
│     └── BAD: PlaceOrder, ReceivePayment, ShipItem               │
│                                                                 │
│  2. 도메인 언어(Ubiquitous Language) 사용                       │
│     ├── OK:  CartCheckedOut (비즈니스 의미 명확)                │
│     └── BAD: CartDataUpdated (기술 용어, 의미 불명)             │
│                                                                 │
│  3. Thin Event (내부) + Fat Integration Event (외부)            │
│     ├── 내부 이벤트: OrderPlaced { orderId: "123" }             │
│     │   → 같은 서비스 내에서 조회 가능하므로 ID만               │
│     └── 통합 이벤트: OrderPlaced { orderId, items, total, ... }│
│         → 외부 서비스가 추가 조회 없이 처리 가능하도록          │
│                                                                 │
│  4. Correlation ID + Causation ID 필수                          │
│     ├── Correlation ID: 전체 비즈니스 흐름 추적                │
│     └── Causation ID: 직접적 원인 추적 (디버깅)                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

8.2 Aggregate 설계 (Vaughn Vernon 규칙)

┌─────────────────────────────────────────────────────────────────┐
│              Aggregate 설계 4원칙                                │
│                                                                 │
│  규칙 1: 1 트랜잭션 = 1 Aggregate                               │
│  ├── 하나의 Command는 하나의 Aggregate만 수정                  │
│  └── 여러 Aggregate 변경 필요 → Saga/Process Manager 사용      │
│                                                                 │
│  규칙 2: Aggregate를 작게 유지                                  │
│  ├── Order { items, payments, shipments } → 너무 큼!           │
│  ├── Order { items } + Payment { ... } + Shipment { ... }      │
│  └── 이벤트 수가 적어져 Rehydration도 빨라짐                  │
│                                                                 │
│  규칙 3: ID로만 참조                                            │
│  ├── 다른 Aggregate를 직접 참조하지 않음                       │
│  └── Order.customerId (ID만) vs Order.customer (객체 참조 X)   │
│                                                                 │
│  규칙 4: Eventual Consistency 수용                              │
│  ├── Aggregate 간에는 즉각 일관성을 강제하지 않음              │
│  └── 이벤트를 통해 "결국" 일관성 달성                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

8.3 Projection 전략

┌─────────────────────────────────────────────────────────────────┐
│              Projection 베스트 프랙티스                          │
│                                                                 │
│  1. 용도별 Read Model 분리                                      │
│     ├── 주문 목록 조회용 Projection                             │
│     ├── 매출 통계용 Projection                                  │
│     └── 고객 대시보드용 Projection (각각 다른 스키마)           │
│                                                                 │
│  2. 항상 재구축 가능하도록 설계                                 │
│     ├── Projection = 파생 데이터 (삭제 후 재생성 가능)         │
│     └── Event Store만 있으면 언제든 새 Projection 추가 가능    │
│                                                                 │
│  3. Event Handler는 Idempotent하게                              │
│     ├── 같은 이벤트를 두 번 처리해도 결과 동일                 │
│     └── checkpoint/position 기반 중복 방지                      │
│                                                                 │
│  4. Eventual Consistency 경계 명시                              │
│     ├── UI에 "정보가 반영되기까지 수 초 걸릴 수 있습니다"      │
│     └── 쓰기 직후 자신의 데이터 읽기 = Read-Your-Writes 보장   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

8.4 Event Versioning 전략

단계 전략 언제 사용
1단계 Weak Schema (기본값) 필드 추가/제거. JSON 유연성 활용
2단계 Upcasting 필드 이름 변경, 구조 변경. 읽기 시점 변환
3단계 Copy-and-Transform 대규모 마이그레이션. 새 스트림으로 복사

9. 안티패턴과 흔한 함정

9.1 “CRUD 이벤트” (Property Sourcing)

┌─────────────────────────────────────────────────────────────────┐
│              안티패턴 #1: Property Sourcing                      │
│                                                                 │
│  BAD: "CRUD 이벤트" — 도메인 의미 없는 기술적 이벤트           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  UserUpdated { field: "email", value: "new@mail.com" }  │    │
│  │  OrderUpdated { field: "status", value: "shipped" }     │    │
│  │                                                          │    │
│  │  → 이벤트에서 "왜" 변경됐는지 알 수 없음                │    │
│  │  → Event Sourcing의 핵심 가치를 상실                     │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  GOOD: 도메인 이벤트 — 비즈니스 의미를 담은 이벤트             │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  UserEmailChanged { oldEmail, newEmail, reason }         │    │
│  │  OrderShipped { trackingNumber, carrier, shippedAt }     │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.2 너무 큰 Aggregate

┌─────────────────────────────────────────────────────────────────┐
│              안티패턴 #2: 거대 Aggregate                        │
│                                                                 │
│  BAD: 하나의 Order Aggregate에 모든 것을 포함                  │
│  Order { items[], payments[], shipments[], reviews[], ... }     │
│  → 이벤트 폭발, Rehydration 느림, 동시성 충돌 빈번            │
│                                                                 │
│  GOOD: 작은 Aggregate로 분리                                   │
│  Order { items[] }                                              │
│  Payment { orderId, amount, status }                            │
│  Shipment { orderId, tracking, carrier }                        │
│  → 각각 독립적 이벤트 스트림, 빠른 Rehydration                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.3 서비스 경계를 넘는 Event Sourcing

┌─────────────────────────────────────────────────────────────────┐
│              안티패턴 #3: 서비스 간 내부 이벤트 공유             │
│                                                                 │
│  BAD: 내부 도메인 이벤트를 그대로 외부에 발행                  │
│  → 내부 스키마 변경이 다른 서비스를 깨뜨림 (강한 결합)        │
│                                                                 │
│  GOOD: 내부 이벤트와 통합 이벤트를 분리                        │
│  ┌──────────────┐     변환     ┌──────────────┐               │
│  │ 내부 이벤트   │────────────►│ 통합 이벤트   │               │
│  │ (도메인 상세) │             │ (공개 계약)   │               │
│  └──────────────┘             └──────────────┘               │
│                                                                 │
│  내부: OrderItemQuantityAdjusted { itemId, oldQty, newQty }    │
│  통합: OrderModified { orderId, totalAmount, itemCount }        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.4 Projection에서 외부 서비스 호출

┌─────────────────────────────────────────────────────────────────┐
│              안티패턴 #4: Projection에서 외부 호출               │
│                                                                 │
│  BAD: Projection Handler 내에서 API/DB 호출                    │
│  → 외부 서비스 장애 시 Projection 전체 중단                    │
│  → Replay 시 외부 서비스에 과부하                               │
│  → 비결정적 (같은 이벤트, 다른 결과)                           │
│                                                                 │
│  GOOD: 필요한 데이터를 이벤트에 포함하거나                     │
│        별도 Enrichment 단계를 분리                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.5 모든 곳에 CQRS 적용

┌─────────────────────────────────────────────────────────────────┐
│              안티패턴 #5: CQRS Everywhere                       │
│                                                                 │
│  Greg Young: "CQRS is not a top-level architecture"             │
│  → Bounded Context 내에서만 적용                                │
│  → 시스템 전체가 아니라, 필요한 부분에만                       │
│                                                                 │
│  Martin Fowler: "CQRS는 minority case이다.                     │
│   대부분의 시스템에서는 위험한 복잡도를 추가할 뿐이다."        │
│                                                                 │
│  Udi Dahan: "대부분의 사람들은 CQRS를 쓰면 안 된다."           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.6 Idempotency 미보장

┌─────────────────────────────────────────────────────────────────┐
│              안티패턴 #6: Idempotency 미보장                    │
│                                                                 │
│  분산 시스템에서 메시지 중복 전달은 불가피                      │
│  → Command Handler, Event Handler 모두 멱등성 필수             │
│                                                                 │
│  전략:                                                          │
│  ├── Command: Idempotency Key로 중복 Command 감지              │
│  ├── Projection: checkpoint/position 기반 중복 건너뛰기        │
│  └── 통합 이벤트: 소비자가 event_id로 중복 처리 방지           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

10. 마이그레이션 가이드

10.1 CRUD → CQRS 점진적 전환 (Strangler Fig)

┌─────────────────────────────────────────────────────────────────┐
│              CRUD → CQRS 단계적 전환                            │
│                                                                 │
│  Phase 1: Read Model 분리                                       │
│  ├── 기존 CRUD 유지                                            │
│  ├── 읽기 전용 모델/DTO 분리                                   │
│  └── 같은 DB, 코드 수준 분리만 (Level 1)                       │
│                                                                 │
│  Phase 2: 이벤트 발행 추가                                     │
│  ├── 쓰기 작업 후 도메인 이벤트 발행                           │
│  ├── Transactional Outbox 패턴 사용                            │
│  └── 기존 읽기 로직은 그대로 동작                              │
│                                                                 │
│  Phase 3: 별도 Read DB 도입                                    │
│  ├── 이벤트를 구독하여 Read DB 구축                            │
│  ├── 읽기 트래픽을 점진적으로 Read DB로 전환                   │
│  └── Strangler Fig: 기존 → 신규로 점진 교체                   │
│                                                                 │
│  Phase 4 (선택): Event Sourcing 전환                            │
│  ├── 쓰기 측을 Event Store 기반으로 전환                       │
│  ├── 초기 스냅샷으로 기존 상태 마이그레이션                    │
│  └── 이 단계는 감사추적이 반드시 필요할 때만                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

10.2 State-based → Event Sourcing 전환

┌─────────────────────────────────────────────────────────────────┐
│              State → ES 전환 전략                                │
│                                                                 │
│  방법 1: 초기 스냅샷 (Big Bang)                                 │
│  ├── 현재 상태를 "InitialSnapshot" 이벤트로 저장               │
│  ├── 이후 변경부터 이벤트로 기록                               │
│  └── 단점: 과거 이력 없음                                      │
│                                                                 │
│  방법 2: Hybrid Repository (점진적)                             │
│  ├── 기존 상태 DB + 새 Event Store 병행                        │
│  ├── 쓰기: 둘 다에 기록                                       │
│  ├── 읽기: 먼저 ES, 없으면 상태 DB 폴백                       │
│  └── 점진적으로 모든 Aggregate를 ES로 전환                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

10.3 조직적 고려사항

┌─────────────────────────────────────────────────────────────────┐
│              조직적 고려사항                                     │
│                                                                 │
│  Conway's Law:                                                  │
│  "시스템 구조는 조직 구조를 반영한다"                           │
│  → CQRS/ES 도입 시 팀 구조도 함께 고려해야 함                 │
│  → Command Side 팀 / Query Side 팀 분리 가능                   │
│                                                                 │
│  학습 비용:                                                     │
│  ├── 팀 전체 온보딩: 6-12개월                                  │
│  ├── 이벤트 사고방식 전환이 가장 어려움                        │
│  ├── "상태를 어떻게 바꿀까" → "무슨 일이 일어났는가"          │
│  └── Pair Programming + 내부 스터디 권장                       │
│                                                                 │
│  점진적 도입 전략:                                              │
│  ├── 한 Bounded Context에서 시작                               │
│  ├── 성공 사례 만들고 확산                                     │
│  └── 전체 시스템에 강제 적용하지 않음                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

11. 운영 가이드

11.1 모니터링 필수 지표

지표 설명 임계값 예시
Projection Lag 이벤트 발생 ~ Read Model 반영 지연 > 5초 경고, > 30초 위험
Append Latency Event Store 쓰기 지연 > 50ms 경고
DLQ Size Dead Letter Queue 적재량 > 0 즉시 확인
Event Throughput 초당 이벤트 처리량 기준선 대비 20% 하락 시 경고
Snapshot Hit Ratio Snapshot 활용률 < 80% 시 Snapshot 전략 재검토
Rehydration P99 Aggregate 복원 99퍼센타일 지연 > 100ms 시 Snapshot 필요

11.2 GDPR/개인정보 준수

┌─────────────────────────────────────────────────────────────────┐
│              GDPR과 Event Sourcing의 충돌                        │
│                                                                 │
│  문제: 이벤트는 불변(immutable)인데, "잊힐 권리"는 삭제 요구   │
│                                                                 │
│  해법 1: Crypto-Shredding                                       │
│  ├── 개인정보를 사용자별 암호화 키로 암호화하여 저장           │
│  ├── 삭제 요청 시 암호화 키만 폐기                             │
│  ├── 이벤트 자체는 유지되지만 개인정보 복호화 불가             │
│  └── 이벤트 불변성을 유지하면서 GDPR 준수                     │
│                                                                 │
│  해법 2: Forgettable Payload (권장)                              │
│  ├── 이벤트 = 참조(reference) + 별도 저장소(개인정보)          │
│  ├── 이벤트: { userId: "user-123", personalDataRef: "pd-456" } │
│  ├── 별도 저장소: { "pd-456": { name: "Kim", email: "..." } }  │
│  ├── 삭제 요청 시 별도 저장소에서만 삭제                       │
│  └── 이벤트 구조가 깔끔하고, Replay 시 자연스럽게 처리        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

11.3 프로덕션 투입 전 체크리스트

┌─────────────────────────────────────────────────────────────────┐
│              프로덕션 체크리스트                                 │
│                                                                 │
│  [ ] Event Store                                                │
│      [ ] Append-only 정책 (UPDATE/DELETE 차단)                  │
│      [ ] Optimistic Concurrency 동작 확인                       │
│      [ ] 백업/복구 전략 수립 및 테스트                          │
│      [ ] 스토리지 용량 계획 (이벤트는 계속 쌓임)               │
│                                                                 │
│  [ ] Projection                                                 │
│      [ ] 재구축 절차 문서화 및 테스트                           │
│      [ ] Idempotent Handler 검증                                │
│      [ ] Projection Lag 모니터링 설정                           │
│      [ ] Dead Letter Queue 설정 및 알림                         │
│                                                                 │
│  [ ] Event 설계                                                 │
│      [ ] 과거형 네이밍 통일                                    │
│      [ ] Correlation/Causation ID 포함                          │
│      [ ] Event Versioning 전략 수립                             │
│      [ ] 내부/통합 이벤트 분리                                  │
│                                                                 │
│  [ ] 운영                                                       │
│      [ ] Snapshot 전략 (필요 시)                                │
│      [ ] GDPR 대응 방안 (Crypto-Shredding 등)                  │
│      [ ] 모니터링 대시보드 구축                                │
│      [ ] 장애 시 Projection 재구축 런북                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

12. 전문가 핵심 조언

┌─────────────────────────────────────────────────────────────────┐
│              전문가 인용                                         │
│                                                                 │
│  Greg Young:                                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "CQRS is not a top-level architecture.                  │    │
│  │   It should be applied within a Bounded Context."        │    │
│  │                                                          │    │
│  │  "Try CQRS without Event Sourcing first."                │    │
│  │                                                          │    │
│  │  "If you have fewer than 1000 events, you don't need     │    │
│  │   snapshots. Measure first."                             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  Martin Fowler:                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "CQRS is a useful pattern, but it's a minority case.   │    │
│  │   For most systems, adding CQRS increases risk           │    │
│  │   because of the additional complexity."                 │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  Udi Dahan:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "Most people shouldn't be using CQRS."                  │    │
│  │                                                          │    │
│  │  "CQRS and Event Sourcing are two separate things.       │    │
│  │   You don't have to use them together."                  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

13. 참고 자료

핵심 문헌

자료 저자 연도 유형
Object-Oriented Software Construction Bertrand Meyer 1988/1997 서적
Domain-Driven Design Eric Evans 2003 서적
Event Sourcing 패턴 Martin Fowler 2005 블로그
CQRS Documents Greg Young 2010 논문 (56p PDF)
Clarified CQRS Udi Dahan 2009 블로그
Life beyond Distributed Transactions Pat Helland 2007 논문 (CIDR)
Eventually Consistent Werner Vogels 2008 ACM Queue
Designing Data-Intensive Applications Martin Kleppmann 2017 서적
Implementing DDD Vaughn Vernon 2013 서적

프레임워크 공식 문서

프레임워크 URL
EventStoreDB https://www.eventstore.com/docs
Axon Framework https://docs.axoniq.io
Marten https://martendb.io/docs
Akka Persistence https://doc.akka.io/docs/akka/current/persistence.html

추가 자료

자료 저자/출처 비고
CAP Theorem Brewer (2000), Gilbert & Lynch (2002) 분산 시스템 이론 토대
Reactive Manifesto Jonas Boner 외 (2013) 반응형 시스템 원칙
Enterprise Integration Patterns Hohpe, Woolf (2003) 메시징 패턴 기초
CQRS Journey (MS Patterns & Practices) Microsoft 실전 구현 가이드
Versioning in an Event Sourced System Greg Young 이벤트 버전관리 심화