⏱️ 시간 여행 — 과거 상태 재생 Replay
슬라이더로 이벤트 번호를 고르면, 0번부터 그 지점까지 이벤트를 순서대로 다시 적용해
"그 순간의 잔액"을 처음부터 재계산합니다. DB에 스냅샷이 없어도 로그만 있으면 언제든 재구성됩니다.
이벤트 0/0
재생된 시점의 잔액
₩ 0
// 슬라이더를 움직여 보세요
🔀 CQRS 데이터 흐름
Command
→
Event Store
⇉
잔액 뷰
통계 뷰
쓰기 모델(Command)과 읽기 모델(Query)이 분리되어 있어, 읽기 뷰 갱신에는 약간의 지연(eventual consistency)이
생길 수 있습니다 — 이것이 CQRS의 대표적인 트레이드오프입니다.
🆚 전통적 CRUD vs 이벤트 소싱
| CRUD (UPDATE) | 이벤트 소싱 |
| 저장 방식 | 현재 상태만 덮어씀 | 모든 변경을 사실로 추가 |
| 과거 이력 | 유실됨 | 전부 보존 (감사 로그) |
| "왜 이 값?" 설명 | 알 수 없음 | 이벤트 재생으로 재현 가능 |
| 새 통계 뷰 추가 | 기존 테이블 구조 변경 | 이벤트 재생으로 새 프로젝션만 추가 |
| 동시 쓰기 충돌 | Lost Update 위험 | 이벤트 append로 충돌 최소화 |
🐍 개념 코드
# 커맨드 → 이벤트 기록 (쓰기 모델)
def handle_withdraw(store, account_id, amount):
events = store.load(account_id)
balance = replay(events) # 현재 상태 재구성
if balance < amount:
raise ValueError("잔액 부족")
store.append(account_id,
{"type": "MoneyWithdrawn", "amount": amount})
# 이벤트 재생 → 읽기 모델 (프로젝션)
def replay(events):
balance = 0
for e in events:
if e["type"] == "MoneyDeposited":
balance += e["amount"]
elif e["type"] == "MoneyWithdrawn":
balance -= e["amount"]
return balance