이 글에서 얻는 것
- Event Sourcing과 CQRS가 무엇인지 “정의”가 아니라 **왜 등장했는지(해결하려는 문제)**로 설명할 수 있습니다.
- 이 패턴이 가져오는 이점(감사/추적/확장)과 비용(복잡도/최종 일관성/운영)을 비교해, “언제 쓰면 좋은지” 판단할 수 있습니다.
- 이벤트 설계(불변성/버전), 리드 모델(프로젝션), 스냅샷/재생 같은 핵심 운영 포인트를 이해합니다.
0) Event Sourcing/CQRS는 ‘고급 기술’이 아니라 ‘복잡도 교환’이다
이 패턴은 기본값이 아닙니다. 대신 아래가 정말 중요할 때 선택할 가치가 생깁니다.
- 변경 이력/감사가 핵심 요구사항이다(누가 언제 무엇을 바꿨는지)
- 읽기/쓰기 요구가 크게 다르고, 읽기 모델을 다양하게 만들고 싶다
- 시간이 흐르며 “과거 시점의 상태”를 복원/재계산해야 한다
그 대가로 시스템은 복잡해집니다.
1) Event Sourcing: 상태 대신 ‘이벤트’를 저장한다
일반 CRUD는 “현재 상태”만 저장합니다. Event Sourcing은 **상태 변화의 로그(이벤트)**를 저장하고, 현재 상태는 이벤트를 재생(replay)해서 만듭니다.
핵심 특징:
- 이벤트는 append-only(추가만)이고, 원칙적으로 수정하지 않습니다(불변성)
- “현재 상태”는 이벤트 스트림의 결과물입니다
- 과거 시점으로 되돌려 재생하면 ‘당시 상태’를 만들 수도 있습니다
2) CQRS: Command와 Query를 분리한다
CQRS(Command Query Responsibility Segregation)는 쓰기(Command)와 읽기(Query)를 분리하는 접근입니다.
- Command(쓰기): 불변식/검증/도메인 규칙을 지키며 상태를 바꾸는 모델
- Query(읽기): 조회 성능/편의에 맞춘 모델(보통 비정규화/전용 인덱스/전용 스키마)
여기서 중요한 현실 포인트:
- 읽기 모델은 “쓰기 모델과 동일할 필요가 없습니다”.
- 읽기 모델은 보통 비동기로 갱신되고, **최종 일관성(eventual consistency)**를 받아들입니다.
Event Sourcing과 CQRS는 자주 함께 쓰이지만, 반드시 같이 써야 하는 것은 아닙니다.
3) 언제 쓰면 좋은가 / 피해야 하는가
3-1) 쓰기 좋은 경우(가치가 큰 경우)
- 감사/이력/추적이 1급 요구사항(금융/정산/권한 변경 등)
- 읽기 모델이 다양해야 한다(대시보드/검색/리포트/피드)
- 시간이 지난 뒤에도 “그때 기준으로 다시 계산”이 필요하다(정산 규칙 변경 등)
3-2) 피하는 편이 좋은 경우
- 단순 CRUD(이력은 트리거/감사 테이블로 충분한 경우가 많음)
- 팀이 작고 운영 여력이 부족하다(프로젝션/재처리/버전 관리가 운영 부담)
- 강한 일관성이 핵심 요구사항인데, 최종 일관성을 받아들이기 어렵다
4) 설계 포인트(실무에서 꼭 부딪히는 것들)
4-1) 이벤트는 “사실”로 설계한다
- 이벤트는 과거 사실을 나타내야 하고, 뒤집기 어렵습니다.
- “명령”이 아니라 “발생한 일”에 가깝게 이름을 짓는 편이 좋습니다.
- 예:
WithdrawRequested(명령 느낌)보다MoneyWithdrawn(사실) 같은 형태
- 예:
4-2) 이벤트 버전/스키마 진화
시간이 지나면 이벤트 구조가 바뀝니다.
- 버전 필드, upcasting(구버전 이벤트를 읽을 때 변환)
- “새 필드 추가”처럼 호환 가능한 변경부터
- 과거 이벤트를 전부 다시 쓰는(migration) 선택은 비용이 큽니다
4-3) 순서/동시성(경쟁) 처리
쓰기 모델에서는 동시성 제어가 필수입니다.
- 애그리게이트 버전 기반의 optimistic concurrency(기대 버전이 다르면 충돌)
- 이벤트 스트림 파티션 키(예: aggregateId)로 순서를 보장하는 전략
4-4) 프로젝션(리드 모델) 갱신과 지연
리드 모델은 결국 “비동기 갱신 파이프라인”입니다.
- 갱신 지연(lag)은 정상 상태에서도 존재할 수 있음
- 지연이 커지면 사용자 경험/비즈니스에 영향 → SLO/알람이 필요
- 재처리(replay) 가능하게 만들어야 함(오프셋/체크포인트/멱등 처리)
4-5) 멱등성(idempotency)과 중복 처리
이벤트/메시지는 중복될 수 있습니다.
- 이벤트 id(또는 message id)로 중복 처리 방지
- 프로젝션 업데이트는 “같은 이벤트가 두 번 와도 결과가 같게” 설계
4-6) 스냅샷(Snapshot)
이벤트가 너무 길어지면 재생 비용이 커집니다.
- 일정 간격(이벤트 N개마다) 또는 시간 기준으로 스냅샷 저장
- 스냅샷은 “최적화”일 뿐, 진실은 이벤트 로그라는 점을 유지
5) 간단 예시(감각): 계좌 이벤트와 잔액 조회
이벤트 스트림(예):
AccountOpened(accountId, ownerId)MoneyDeposited(accountId, amount)MoneyWithdrawn(accountId, amount)
현재 잔액(balance)은:
AccountOpened이후Deposited는 더하고Withdrawn은 빼서 계산합니다.
CQRS를 적용하면:
- Command 모델은 “출금 시 잔액이 음수가 되면 안 된다” 같은 불변식을 지키고,
- Query 모델은
account_balance같은 조회 전용 테이블/캐시로 빠르게 응답합니다.
6) 운영 관점에서 반드시 보는 것
- 이벤트 저장소 append 지연/에러율
- 프로젝션 lag(현재 오프셋 vs 최신), DLQ 유입량
- 재처리(replay) 시간(리드 모델 rebuild가 가능해야 함)
- 이벤트 스키마 변경 시 호환성 테스트
연습(추천)
- 간단한 도메인(주문/재고/정산 중 하나)을 골라 “이벤트 10개”를 설계해보고, 사실(immutable)로 표현되는지 점검해보기
- 리드 모델을 2개 만들어보기: (1) 조회 API 최적화 테이블 (2) 대시보드용 집계 테이블, 그리고 갱신 지연이 사용자 경험에 미치는 영향을 적어보기
- 동일 이벤트 중복 처리/순서 뒤집힘을 가정하고, 어떤 키/버전으로 멱등성을 만들지 설계해보기
💬 댓글