상태 vs 이벤트 이벤트 소싱 이점 · 비용 스냅샷 CQRS 언제 쓰나 📝 문제풀이
◀ 심화 CH 16 📋 목차 심화 CH 18 ▶
📜 심화 CHAPTER 17 · 5년차 트랙

현재 상태만 저장할까, 일어난 일 전부를 저장할까?

CRUD로 UPDATE 한 방이면 과거는 사라져요. 이벤트 소싱은 "무슨 일이 있었는가"를 불변 로그로 쌓고, 현재 상태는 그걸 재생해 계산합니다. 여기에 읽기/쓰기 모델을 가르는 CQRS를 더하면 감사·확장·시간여행이 가능해져요 — 대신 복잡도라는 청구서가 따라옵니다.

🎯 이 장을 끝내면
🗃️
상태 vs 이벤트
"지금 얼마"만 남길까, "무슨 일이 있었나"를 남길까
은행 잔고를 예로 보면 차이가 선명해요.

전통적인 CRUD는 테이블에 현재 상태 한 줄만 유지해요. 계좌 잔고가 100 → 130 → 80으로 바뀌면 컬럼 값을 UPDATE로 덮어씁니다. 편하지만, 이전 값과 그 이유는 그 순간 사라져요. "왜 80이 됐지?"라는 질문에 답할 근거가 DB에 남지 않습니다.

⚖️ 두 저장 방식의 근본 차이 상태 저장(CRUD) — 테이블은 현재 스냅샷. balance = 80 한 값만 남고 과거는 소실.
이벤트 저장(Event Sourcing)일어난 일들을 순서대로 append. Opened(100) → Deposited(30) → Withdrawn(50). 현재 잔고 80은 이 이벤트들을 접어서(fold) 계산. 핵심 전환: "상태는 데이터가 아니라, 이벤트들을 재생한 결과"로 본다는 것. 진실의 원천(source of truth)이 테이블의 값에서 이벤트 로그로 바뀝니다.
🟢
Opened(100)
계좌 개설, 잔고 100
💰
Deposited(30)
입금 +30 → 130
💸
Withdrawn(50)
출금 −50 → 80
🖼️ 그림으로 보기 — 상태 저장(CRUD) vs 이벤트 소싱
상태 저장 (CRUD) 현재 값만 유지 · UPDATE로 덮어씀 100 → 130 → … balance = 80 ↺ UPDATE — 덮어쓰기 과거 값과 그 이유는 사라져요 이벤트 소싱 불변 이벤트를 append + 재생 Opened(100) Deposited(+30) Withdrawn(−50) ▼ replay (재생) balance = 80
🧭
회계 장부가 원조예요. 회계는 절대 기존 항목을 지우지 않아요. 틀렸으면 반대 분개(수정 이벤트)를 새로 추가합니다. 이벤트 소싱은 이 append-only 원장(ledger) 개념을 그대로 소프트웨어에 옮긴 것이에요.
📜
이벤트 소싱
불변 이벤트의 연속으로 저장하고, 재생으로 복원한다
append-only 로그 + replay = 현재 상태

이벤트 소싱(Event Sourcing)은 상태 변경 하나하나를 불변(immutable) 이벤트로 만들어 append-only 로그(이벤트 스토어)에 순서대로 기록해요. 이벤트는 과거형 사실(MoneyDeposited)이고, 한번 쓰이면 수정·삭제하지 않아요. 현재 상태가 필요하면 처음부터 이벤트를 재생(replay)해 다시 계산합니다.

// 이벤트는 과거형 사실 — 불변
sealed interface AccountEvent {}
record AccountOpened(String id, long initial) implements AccountEvent {}
record MoneyDeposited(long amount)          implements AccountEvent {}
record MoneyWithdrawn(long amount)          implements AccountEvent {}

// 현재 상태 = 이벤트들을 fold(재생)한 결과
long balance = events.stream().reduce(0L, (bal, e) -> switch (e) {
    case AccountOpened o    -> o.initial();
    case MoneyDeposited d   -> bal + d.amount();
    case MoneyWithdrawn w   -> bal - w.amount();
}, Long::sum);
// events = [Opened(100), Deposited(30), Withdrawn(50)]  ->  balance == 80
💡
쓰기의 진짜 모습. 명령(Command)이 들어오면 ① 해당 애그리거트의 과거 이벤트를 재생해 현재 상태 복원 → ② 비즈니스 규칙 검증(예: 잔고보다 큰 출금 거부) → ③ 통과하면 새 이벤트를 append. 상태를 UPDATE하는 게 아니라 이벤트를 덧붙이는 것이 유일한 쓰기예요. 동시성 충돌은 보통 이벤트 버전(낙관적 락)으로 방어합니다.
Append-only Log Immutable Event Replay Event Store Aggregate Past Tense
⚖️
이점 · 비용
공짜 점심은 없다
강력하지만 대가가 분명한 패턴이에요.
이점 ✅왜 좋은가
완전한 감사 로그모든 변경이 지워지지 않는 사실로 남아, "누가·언제·왜 바꿨나"를 항상 답할 수 있음
시간여행 · 디버깅특정 시점까지만 재생하면 과거 어느 순간의 상태를 그대로 재현. 장애 재현·감사에 유리
재생으로 새 뷰 생성새 읽기 모델이 필요하면 기존 이벤트를 다시 재생해 언제든 새 프로젝션을 만들 수 있음
비용 ⚠️무엇이 어려운가
스키마(이벤트) 진화과거 이벤트는 영원히 남아 못 지움. 필드 추가·의미 변경 시 업캐스팅(upcasting)·버전 관리 필요
조회의 어려움"현재 잔고 top 10" 같은 질의를 이벤트 로그에 직접 못 함 → 별도 읽기 모델을 만들어야 함(→ CQRS)
복잡도 · 학습곡선이벤트 설계·최종 일관성·재처리·멱등성 등 운영·인지 부담이 CRUD보다 훨씬 큼
⚠️
"이벤트는 못 지운다"가 축복이자 저주. 감사 관점에선 최고지만, 개인정보(GDPR 삭제권)·잘못 설계한 이벤트가 영원히 따라와요. 그래서 실무에선 암호화 후 키 파기(crypto-shredding), 이벤트 버전/업캐스터, 신중한 이벤트 스키마 설계가 필수 역량이 됩니다.
📸
스냅샷
매번 처음부터 재생하기엔 이벤트가 너무 많다
재생 비용을 잘라주는 중간 체크포인트.

한 애그리거트에 이벤트가 수만·수십만 개 쌓이면, 상태 하나 복원하려고 매번 전부 재생하는 건 느려요. 스냅샷(Snapshot)특정 버전까지 재생한 결과 상태를 미리 저장해 두는 최적화예요. 이후에는 가장 최근 스냅샷을 불러온 뒤 그 이후 이벤트만 재생하면 됩니다.

📜
이벤트 1~10000
전부 재생하면 느림
📸
스냅샷 @9990
그 시점 상태를 저장해 둠
+ 이벤트 9991~10000
10개만 재생 → 현재 상태
🖼️ 그림으로 보기 — 스냅샷으로 재생 비용 줄이기
전체 재생 스냅샷 없이 이벤트 1~10000 모두 재생 (느림) 스냅샷 활용 스냅샷 + 최근 이벤트 재생 안 함 (스냅샷이 대체) 10개 📸 스냅샷 @9990 스냅샷은 최적화(캐시) — 진실의 원천은 여전히 이벤트 로그
💡
스냅샷은 최적화지 진실의 원천이 아니에요. 진실의 원천은 여전히 이벤트 로그입니다. 스냅샷은 언제든 버리고 재생으로 다시 만들 수 있는 캐시예요. 그래서 스냅샷이 손상돼도 이벤트만 온전하면 복구 가능하고, 보통 N개 이벤트마다 또는 주기적으로 갱신합니다.
🔀
CQRS
쓰기 모델과 읽기 모델을 갈라놓기
Command Query Responsibility Segregation

CQRS명령(Command, 상태를 바꾸는 쓰기)조회(Query, 상태를 읽는 읽기)서로 다른 모델로 분리하는 패턴이에요. 하나의 도메인 모델이 쓰기와 읽기를 다 감당하려다 비대해지는 문제를, 두 축으로 나눠 각자 최적화합니다.

✍️
Command (쓰기 모델)
규칙 검증 후 이벤트 append
📜
이벤트 스토어
진실의 원천
🔎
Query (읽기 모델)
프로젝션으로 조회 최적화
🖼️ 그림으로 보기 — CQRS 전체 흐름 (쓰기/읽기 분리)
쓰기 (Command side) Command 상태 변경 요청 쓰기 모델 규칙 검증 이벤트 스토어 이벤트 append · 진실의 원천 이벤트 구독 · 쓰기/읽기 분리 · 최종 일관성 이벤트 읽기 (Query side) 프로젝션 이벤트 → 읽기 뷰 투영 읽기 모델 조회 최적화 뷰 Query 상태 조회

이벤트 소싱과 결합하면, 쓰기 쪽이 남긴 이벤트를 구독해 읽기 모델(프로젝션, Projection)을 만들어요. 프로젝션은 조회에 딱 맞게 비정규화된 읽기 전용 뷰(예: RDB 조회 테이블, Elasticsearch 인덱스, Redis 캐시)입니다. 화면·질의마다 서로 다른 프로젝션 여러 개를 둘 수도 있어요.

⏳ 프로젝션은 "최종 일관성(Eventual Consistency)" 쓰기(이벤트 저장)와 읽기 모델 갱신 사이에는 시간차가 있어요. 이벤트가 저장된 뒤 프로젝션이 비동기로 따라잡기 때문에, 방금 쓴 내용이 조회에는 잠깐 안 보일 수 있습니다. 그래서 "저장 직후 바로 그 값을 다시 조회해 보여줘야 하는" UX는 주의가 필요해요 — 명령의 결과를 낙관적으로 화면에 반영하거나, 특정 조회만 쓰기 모델에서 강한 일관성으로 읽는 식으로 보완합니다.
🧭
읽기/쓰기를 나누면 확장도 나뉘어요. 대부분 시스템은 읽기가 쓰기보다 훨씬 많아요. CQRS면 읽기 모델만 독립적으로 스케일아웃·복제·캐싱하고, 쓰기 모델은 일관성·규칙에 집중할 수 있어요. 조회용으로 다른 저장소 기술(검색엔진 등)을 자유롭게 붙일 수 있는 것도 큰 장점이에요.
🎯
언제 쓰나
멋있다고 아무 데나 쓰면 후회한다
두 패턴은 별개이고, 대부분의 CRUD엔 과합니다.
⚠️
이벤트 소싱 ≠ CQRS. 둘은 독립된 별개 개념이에요. 이벤트 소싱 없이 CQRS만(그냥 읽기/쓰기 모델 분리) 쓸 수도 있고, CQRS 없이 이벤트 소싱만 쓸 수도 있어요. 다만 이벤트 소싱은 조회가 어렵기 때문에 CQRS와 자주 짝을 이룰 뿐, 서로를 강제하지 않습니다.
🟢 도입이 정당한 신호 감사·규제가 필수 — 금융·의료·회계처럼 "모든 변경 이력"이 요구사항일 때.
도메인이 본질적으로 이벤트 중심 — 주문 처리, 물류 추적, 예약 등 "일어난 일"이 곧 도메인일 때.
복잡한 비즈니스 로직 · 시간여행 필요 — 과거 상태 재현, 다양한 읽기 뷰가 반복해서 필요할 때. 이때는 감사·유연성이 복잡도 비용을 충분히 상쇄합니다.
🔴 과한 신호 (그냥 CRUD 써라) 단순 CRUD 도메인 — 게시판, 관리자 설정처럼 이력이 중요치 않은 곳.
강한 즉시 일관성이 핵심이고 팀이 최종 일관성을 감당할 준비가 안 됐을 때.
작은 규모 · 짧은 납기 — 이벤트 설계·재처리·프로젝션 운영 비용을 감당할 여력이 없을 때. "대부분의 애플리케이션에는 이벤트 소싱/CQRS가 과잉 설계"라는 게 업계의 일반적 조언이에요. 도메인이 복잡하거나 감사가 필수일 때 비로소 가치가 나옵니다.
💡
부분 적용이 현실적이에요. 시스템 전체가 아니라 핵심 애그리거트 하나(예: 주문·결제)에만 이벤트 소싱을 쓰고, 나머지는 평범한 CRUD로 두는 하이브리드가 흔해요. "전부 아니면 전무"가 아니라 비용 대비 가치가 있는 경계에만 적용하세요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
시니어 관점으로 점검
면접·설계 리뷰에서 실제로 묻는 결의 문제예요. 틀려도 바로 해설이 나옵니다.
🧪
개념을 "안다"와 "설명할 수 있다"는 달라요. 네 유형(객관식 · O/X · 빈칸 · 코드결과)으로 점검해보세요.
심화 CH 18 🗄️ 데이터베이스 심화 — 인덱스, 트랜잭션 격리수준, 락, 실행계획 (제작 예정)