CRUD로 UPDATE 한 방이면 과거는 사라져요. 이벤트 소싱은 "무슨 일이 있었는가"를 불변 로그로 쌓고, 현재 상태는 그걸 재생해 계산합니다. 여기에 읽기/쓰기 모델을 가르는 CQRS를 더하면 감사·확장·시간여행이 가능해져요 — 대신 복잡도라는 청구서가 따라옵니다.
🎯 이 장을 끝내면
상태 저장(CRUD)과 이벤트 저장(Event Sourcing)의 근본 차이를 설명할 수 있어요.
이벤트를 append-only 로그로 쌓고 재생(replay)으로 현재 상태를 만드는 원리를 이해해요.
이벤트 소싱의 이점(감사·시간여행)과 비용(스키마 진화·조회·복잡도)을 저울질할 수 있어요.
스냅샷이 왜 필요하고 무엇을 최적화하는지 알아요.
CQRS로 명령/조회를 나누고 프로젝션(읽기 모델)을 최종 일관성으로 구축하는 그림을 그려요.
이벤트 소싱과 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 이벤트 소싱
🧭
회계 장부가 원조예요. 회계는 절대 기존 항목을 지우지 않아요. 틀렸으면 반대 분개(수정 이벤트)를 새로 추가합니다. 이벤트 소싱은 이 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하는 게 아니라 이벤트를 덧붙이는 것이 유일한 쓰기예요. 동시성 충돌은 보통 이벤트 버전(낙관적 락)으로 방어합니다.
과거 이벤트는 영원히 남아 못 지움. 필드 추가·의미 변경 시 업캐스팅(upcasting)·버전 관리 필요
조회의 어려움
"현재 잔고 top 10" 같은 질의를 이벤트 로그에 직접 못 함 → 별도 읽기 모델을 만들어야 함(→ CQRS)
복잡도 · 학습곡선
이벤트 설계·최종 일관성·재처리·멱등성 등 운영·인지 부담이 CRUD보다 훨씬 큼
⚠️
"이벤트는 못 지운다"가 축복이자 저주. 감사 관점에선 최고지만, 개인정보(GDPR 삭제권)·잘못 설계한 이벤트가 영원히 따라와요. 그래서 실무에선 암호화 후 키 파기(crypto-shredding), 이벤트 버전/업캐스터, 신중한 이벤트 스키마 설계가 필수 역량이 됩니다.
📸
스냅샷
매번 처음부터 재생하기엔 이벤트가 너무 많다
재생 비용을 잘라주는 중간 체크포인트.
한 애그리거트에 이벤트가 수만·수십만 개 쌓이면, 상태 하나 복원하려고 매번 전부 재생하는 건 느려요. 스냅샷(Snapshot)은 특정 버전까지 재생한 결과 상태를 미리 저장해 두는 최적화예요. 이후에는 가장 최근 스냅샷을 불러온 뒤 그 이후 이벤트만 재생하면 됩니다.
📜
이벤트 1~10000
전부 재생하면 느림
→
📸
스냅샷 @9990
그 시점 상태를 저장해 둠
→
⚡
+ 이벤트 9991~10000
10개만 재생 → 현재 상태
🖼️ 그림으로 보기 — 스냅샷으로 재생 비용 줄이기
💡
스냅샷은 최적화지 진실의 원천이 아니에요. 진실의 원천은 여전히 이벤트 로그입니다. 스냅샷은 언제든 버리고 재생으로 다시 만들 수 있는 캐시예요. 그래서 스냅샷이 손상돼도 이벤트만 온전하면 복구 가능하고, 보통 N개 이벤트마다 또는 주기적으로 갱신합니다.
🔀
CQRS
쓰기 모델과 읽기 모델을 갈라놓기
Command Query Responsibility Segregation
CQRS는 명령(Command, 상태를 바꾸는 쓰기)과 조회(Query, 상태를 읽는 읽기)를 서로 다른 모델로 분리하는 패턴이에요. 하나의 도메인 모델이 쓰기와 읽기를 다 감당하려다 비대해지는 문제를, 두 축으로 나눠 각자 최적화합니다.
✍️
Command (쓰기 모델)
규칙 검증 후 이벤트 append
→
📜
이벤트 스토어
진실의 원천
→
🔎
Query (읽기 모델)
프로젝션으로 조회 최적화
🖼️ 그림으로 보기 — CQRS 전체 흐름 (쓰기/읽기 분리)
이벤트 소싱과 결합하면, 쓰기 쪽이 남긴 이벤트를 구독해 읽기 모델(프로젝션, Projection)을 만들어요. 프로젝션은 조회에 딱 맞게 비정규화된 읽기 전용 뷰(예: RDB 조회 테이블, Elasticsearch 인덱스, Redis 캐시)입니다. 화면·질의마다 서로 다른 프로젝션 여러 개를 둘 수도 있어요.
⏳ 프로젝션은 "최종 일관성(Eventual Consistency)"
쓰기(이벤트 저장)와 읽기 모델 갱신 사이에는 시간차가 있어요. 이벤트가 저장된 뒤 프로젝션이 비동기로 따라잡기 때문에, 방금 쓴 내용이 조회에는 잠깐 안 보일 수 있습니다.
그래서 "저장 직후 바로 그 값을 다시 조회해 보여줘야 하는" UX는 주의가 필요해요 — 명령의 결과를 낙관적으로 화면에 반영하거나, 특정 조회만 쓰기 모델에서 강한 일관성으로 읽는 식으로 보완합니다.
🧭
읽기/쓰기를 나누면 확장도 나뉘어요. 대부분 시스템은 읽기가 쓰기보다 훨씬 많아요. CQRS면 읽기 모델만 독립적으로 스케일아웃·복제·캐싱하고, 쓰기 모델은 일관성·규칙에 집중할 수 있어요. 조회용으로 다른 저장소 기술(검색엔진 등)을 자유롭게 붙일 수 있는 것도 큰 장점이에요.
🎯
언제 쓰나
멋있다고 아무 데나 쓰면 후회한다
두 패턴은 별개이고, 대부분의 CRUD엔 과합니다.
⚠️
이벤트 소싱 ≠ CQRS. 둘은 독립된 별개 개념이에요. 이벤트 소싱 없이 CQRS만(그냥 읽기/쓰기 모델 분리) 쓸 수도 있고, CQRS 없이 이벤트 소싱만 쓸 수도 있어요. 다만 이벤트 소싱은 조회가 어렵기 때문에 CQRS와 자주 짝을 이룰 뿐, 서로를 강제하지 않습니다.
🟢 도입이 정당한 신호감사·규제가 필수 — 금융·의료·회계처럼 "모든 변경 이력"이 요구사항일 때. 도메인이 본질적으로 이벤트 중심 — 주문 처리, 물류 추적, 예약 등 "일어난 일"이 곧 도메인일 때. 복잡한 비즈니스 로직 · 시간여행 필요 — 과거 상태 재현, 다양한 읽기 뷰가 반복해서 필요할 때.
이때는 감사·유연성이 복잡도 비용을 충분히 상쇄합니다.
🔴 과한 신호 (그냥 CRUD 써라)단순 CRUD 도메인 — 게시판, 관리자 설정처럼 이력이 중요치 않은 곳. 강한 즉시 일관성이 핵심이고 팀이 최종 일관성을 감당할 준비가 안 됐을 때. 작은 규모 · 짧은 납기 — 이벤트 설계·재처리·프로젝션 운영 비용을 감당할 여력이 없을 때.
"대부분의 애플리케이션에는 이벤트 소싱/CQRS가 과잉 설계"라는 게 업계의 일반적 조언이에요. 도메인이 복잡하거나 감사가 필수일 때 비로소 가치가 나옵니다.
💡
부분 적용이 현실적이에요. 시스템 전체가 아니라 핵심 애그리거트 하나(예: 주문·결제)에만 이벤트 소싱을 쓰고, 나머지는 평범한 CRUD로 두는 하이브리드가 흔해요. "전부 아니면 전무"가 아니라 비용 대비 가치가 있는 경계에만 적용하세요.
🧠 이 장 핵심 요약
CRUD는 현재 상태만 저장(과거 소실). 이벤트 소싱은 변경을 불변 이벤트의 append-only 로그로 저장.
현재 상태 = 이벤트를 재생(replay)해 계산한 결과. 진실의 원천은 이벤트 로그.
이점: 완전한 감사·시간여행·재생으로 새 뷰 / 비용: 스키마(이벤트) 진화·조회 어려움·복잡도↑.
스냅샷 = 매번 전체 재생하는 비용을 줄이는 최적화(캐시). 진실의 원천은 여전히 이벤트.
CQRS = 명령(쓰기)/조회(읽기) 모델 분리. 프로젝션(읽기 모델)은 이벤트로부터 구축, 최종 일관성.
이벤트 소싱과 CQRS는 별개 개념(자주 함께 쓸 뿐 필수 아님). 대부분 CRUD엔 과함 — 복잡·감사 필수일 때 가치.
📝
문제풀이 · 점검
시니어 관점으로 점검
면접·설계 리뷰에서 실제로 묻는 결의 문제예요. 틀려도 바로 해설이 나옵니다.
🧪
개념을 "안다"와 "설명할 수 있다"는 달라요. 네 유형(객관식 · O/X · 빈칸 · 코드결과)으로 점검해보세요.