Event Sourcing & CQRS Mechanics
상태가 아닌 변화의 궤적 자체를 물리 저장하는 이벤트 소싱과, 읽기와 쓰기의 모델을 분리하여 성능을 극대화하는 CQRS의 결합 물리학을 다루는 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
system-architecture-distributed-systemssystem-architecturedistributed-systemsevent-drivenreactive-systemsevent-sourcingcqrs-mechanicscqrs10 min read
1. Overview
이벤트 소싱과 CQRS 역학(Event Sourcing & CQRS Mechanics)은 데이터를 수정(UPDATE)하고 삭제(DELETE)하던 전통적인 RDBMS의 낡은 철학을 완전히 박살 내고, 모든 데이터의 변경을 "불변의 사건(Event)"으로 영구 저장하여 시스템의 무한한 복원력과 쿼리 성능을 동시에 확보하는 MSA 최상위 아키텍처 패턴을 해부합니다.
학습자는 현재의 상태값(예: 잔액 5만 원)만 달랑 저장하는 방식의 취약점을 인지하고, 계좌 개설부터 입출금 내역을 모두 Append-only로 쌓아 과거의 모든 순간으로 타임머신을 탈 수 있는 **이벤트 소싱(Event Sourcing)**의 철학을 뜯어봅니다. 나아가 이벤트들이 수백만 개 쌓였을 때 조회가 불가능해지는 치명적 단점을 해결하기 위해, 쓰기 모델(Command)과 읽기 모델(Query)을 완전히 두 쪽으로 찢어발기는 CQRS(Command Query Responsibility Segregation) 패턴의 필연적 결합을 해부합니다. 마지막으로, 읽기 DB와 쓰기 DB 사이의 지연(Eventual Consistency) 속에서 유저 UX를 방어해 내는 백엔드의 극한 타협 역량을 확보합니다.
2. Scope & Boundaries
In-Scope
- Event Sourcing: Update/Delete의 제거, 불변(Immutable) 이벤트 스트림, Append-only 데이터 모델, Replay (상태 복원).
- Snapshotting: 이벤트 재생 시간을 단축하기 위한 주기적인 상태 스냅샷.
- CQRS: 쓰기(Command - 도메인 무결성)와 읽기(Query - Materialized View) 모델의 물리적/논리적 분리.
- Eventual Consistency: CQRS 간 데이터 동기화 지연과 이를 극복하는 UX 처리(Optimistic UI).
Out-of-Scope
- Apache Kafka의 클러스터 튜닝: 이벤트를 저장하는 인프라 구축 07-04-01 Message Brokers 영역.
- DDD (Domain-Driven Design) 깊은 설계: 애그리거트(Aggregate)와 바운디드 컨텍스트 도출 06-02-04 Domain-Driven Data 영역으로 위임.
Boundaries
- CRUD vs Event Sourcing: CRUD는 장바구니에 아이폰을 넣었다가 뺀 사실을 '삭제(DELETE)'해 버리기 때문에 회사가 "유저가 무엇을 고민하다 안 샀는지"에 대한 황금 같은 데이터를 잃게 됩니다. 이벤트 소싱은
ItemAdded,ItemRemoved라는 이벤트를 모두 로깅하여 비즈니스 히스토리를 100% 보존합니다. 하지만 단순한 게시판이나 어드민 페이지조차 이벤트 소싱과 CQRS로 짜는 것은 "파리 잡는 데 바주카포를 쏘는 격"인 극악의 오버엔지니어링(Complexity 폭발)임을 명확히 경계 짓습니다.
3. Counterexample
- CQRS 없는 이벤트 소싱의 재앙: 은행 시스템을 이벤트 소싱으로 멋지게 만들었습니다. 10년간 A 유저에게 1억 건의 입출금 이벤트가 쌓였습니다. A 유저가 앱을 켜서 "현재 잔액"을 조회합니다. 시스템은 1억 건의 이벤트를 0원부터 더하고 빼는 재생(Replay) 연산을 시작합니다. 유저 화면은 3분 동안 스피너만 돌아가다 타임아웃 납니다. 이벤트를 쌓는 것만 생각하고, 이를 빠르게 조회하기 위해 별도의 읽기용 DB(Read Model)를 만들어두는 CQRS를 결합하지 않은 치명적 설계 실패입니다.
- 동기화 지연(Eventual Consistency)에 대한 유저 UX 무시: CQRS 아키텍처입니다. 유저가 '내 정보 수정'에서 이름을 '홍길동'으로 바꿨습니다(쓰기 DB 성공). 그리고 화면이 새로고침 되면서 '읽기 DB'를 찔렀습니다. 쓰기 DB에서 읽기 DB로 데이터가 넘어가는데(동기화 지연) 0.5초가 걸렸습니다. 화면에는 여전히 옛날 이름이 뜹니다. 빡친 유저는 정보 수정을 3번 연속 누르며 버그 리포트를 올립니다. 클라이언트 측에 캐시를 주거나 웹소켓으로 갱신을 알려주는 UI 방어막 없이 CQRS를 도입한 UX 파괴입니다.
4. Prerequisites
- Pub/Sub 및 EDA (Basic): 이벤트의 발행과 구독 흐름. (07-04-02 Event-driven Flows)
- 분산 로그 기반 (Basic): Append-only의 성능적 이점. (07-02-02 Consensus & Log)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: CRUD의 종말, 이벤트 소싱 (The End of CRUD)
- Why to Learn: 현재 상태(State)만 덮어쓰는 RDBMS의 방식이 비즈니스의 소중한 "과거의 의사결정 맥락(Context)"을 모조리 날려버리는 치명적 결함을 극복하기 위함입니다.
- What to Learn:
- Concepts: Event Sourcing, Append-only Log, Immutable Event, Domain Event.
- Skills: 상태 기반 엔티티(Entity) 모델링을 이벤트 스트림 기반 모델링으로 리팩터링.
- How to Learn:
- 1단계: 기억 상실증(CRUD): 잔액이 5만 원입니다. 3만 원이 입금되어
UPDATE를 쳐서 8만 원이 되었습니다. 5만 원이라는 과거 데이터는 디스크에서 영원히 삭제되었습니다. 해커가 3만 원을 조작해서 넣은 건지, 정상 입금인지 알 방법이 없는 상태를 해부합니다. - 2단계: 절대 기억(Event Sourcing):
[AccountCreated(0원)],[Deposited(5만 원)],[Withdrawn(2만 원)]. 데이터를 절대 수정하거나 지우지 않고 시간순으로 쌓기만(Append-only) 합니다. 현재 상태(잔액 3만 원)는 이 배열을reduce()연산으로 싹 더하면 언제든 100% 도출되는 마법을 뜯어봅니다.
- 1단계: 기억 상실증(CRUD): 잔액이 5만 원입니다. 3만 원이 입금되어
- Implement: 파이썬
BankAccount클래스 이벤트 소싱 렌더링.balance변수를 직접 수정하지 않고,events배열에{'type': 'DEPOSIT', 'amount': 50}만append()함.get_balance()호출 시events배열을 순회하며 합산을 리턴하여 데이터 불변성(Immutability)을 터미널로 증명.
Recommended
Core Topic 02: 타임머신과 스냅샷 (Replay & Snapshotting)
- Why to Learn: 1년 동안 쌓인 100만 건의 이벤트를 매번 처음부터 계산(Replay)하는 것은 CPU의 자살 행위임을 깨닫고, 시스템을 최적화하는 방어 기제를 설계하기 위함입니다.
- What to Learn:
- Concepts: State Replay, Point-in-Time Recovery (시점 복구), Snapshotting (스냅샷), Event Store.
- Skills: N개의 이벤트 주기로 상태를 미리 압축 저장해 두는 스냅샷 생성 로직 작성.
- How to Learn:
- 1단계: 타임머신(Point-in-Time): "지난주 수요일 오후 3시에 이 유저의 잔액은 얼마였지?" 이벤트 소싱은 특정 Timestamp(수요일 오후 3시)까지만 이벤트를 싹 긁어모아 재생(Replay)하면, 과거의 어떤 시점이든 완벽하게 복원할 수 있는 궁극의 디버깅 능력을 해부합니다.
- 2단계: 스냅샷(Snapshot): 이벤트가 100만 개면 조회 한 번에 100만 번의 연산이 필요합니다. 이를 막기 위해 이벤트 1만 개가 쌓일 때마다 당시의 결과값(잔액: 300만 원)을 '스냅샷'으로 저장합니다. 다음 조회 때는 1만 번부터 재생하지 않고, 스냅샷 값을 가져와 10,001번 이벤트부터만 재생하여 연산을 로 축소하는 기법을 뜯어봅니다.
- Implement: 시점 복구(Replay) 및 스냅샷 최적화 데모. 배열에 1,000개의 주식 거래 이벤트 존재.
get_stock_count(target_time)함수를 통해 타임머신 복원 렌더링. 또한 100번째 이벤트마다snapshot_dict에 상태를 기록해두고 조회 시 스냅샷 지점부터만 루프를 도는 성능 최적화(소요 시간 감소) 콘솔 출력.
Practical
Core Topic 03: 읽기와 쓰기의 분리, CQRS (Read/Write Split)
- Why to Learn: 이벤트 소싱만으로 읽기(Query)를 수행하면 아무리 스냅샷이 있어도 "잔액이 5만 원 이상인 20대 유저 목록" 같은 복잡한 검색이 물리적으로 불가능해지는 한계를, 두 개의 DB로 찢어서 해결하기 위함입니다.
- What to Learn:
- Concepts: CQRS (Command Query Responsibility Segregation), Command Model(쓰기용), Query Model (읽기용 View), Materialized View, Sync/Async Projection.
- Skills: 쓰기 모델(Event Store)의 이벤트를 구독하여 읽기 모델(Elasticsearch, Redis) 전용 뷰(View) 테이블을 갱신하는 프로젝션(Projection) 파이프라인 구축.
- How to Learn:
- 1단계: 딜레마의 폭발: 쓰기(Command)는 100% 무결성이 중요하니 관계형 DB에 락(Lock)을 걸어 저장하고 싶습니다. 반면 읽기(Query)는 검색 조건이 수십 개라 텍스트 검색 엔진(Elasticsearch)이 필요합니다. 하나의 완벽한 만능 DB는 세상에 존재하지 않음을 해부합니다.
- 2단계: DB를 찢다: 쓰기 전용 DB(Kafka/RDBMS)는 '이벤트'만 빠르게
INSERT하고 끝납니다. 이 이벤트가 뒤로 흘러가면, 프로젝터(Projector)라는 워커 스레드가 이벤트를 주워 먹고 Elasticsearch나 MongoDB(읽기 전용 DB)에 검색하기 딱 좋은 JSON 형태로 예쁘게 다듬어서(Materialized View) 저장해 두는 완벽한 분업(Segregation)을 뜯어봅니다.
- Implement: CQRS 파이프라인 시뮬레이션. 1) Command API가
Event_Store(List)에 데이터 Append. 2) 백그라운드Projector함수가 새 이벤트를 감지하고Read_DB(Dict)에 '조회하기 쉬운' 집계 데이터를 미리 만들어 둠. 3) Query API는 원본 이벤트를 건드리지 않고 오직 0.001초 만에Read_DB에서 값을 쓱 꺼내가는 3단 분리 로직 렌더링.
Advanced
Core Topic 04: 시간차의 공포, 최종적 일관성과 UX (The Eventual Consistency Trap)
- Why to Learn: CQRS 아키텍처의 필연적 부작용인 "방금 내가 바꾼 데이터가 왜 화면에 바로 안 보이지?"라는 0.5초의 동기화 지연(Lag)을 방치하여 비즈니스를 망치는 끔찍한 UX를 아키텍처로 방어하기 위함입니다.
- What to Learn:
- Concepts: Eventual Consistency(최종적 일관성), Read-Your-Writes(내가 쓴 건 읽기), Optimistic UI (낙관적 UI 렌더링), Polling vs WebSockets.
- Skills: 백엔드의 일관성 지연을 클라이언트(프론트엔드)와의 협약(Contract)이나 폴링 전략으로 숨기는 엔드투엔드 트레이드오프 설계.
- How to Learn:
- 1단계: 0.5초의 배신: 유저가 주문 취소를 눌렀습니다(Command 성공). 0.5초 뒤 뷰 DB(Query)가 갱신됩니다. 그런데 유저는 취소 버튼을 누르자마자 0.1초 만에 주문 목록을 새로고침 했습니다. 화면에는 '배송 준비 중'이 뜹니다. 유저는 취소가 안 된 줄 알고 분노의 광클을 연타하는 재앙을 해부합니다.
- 2단계: UX 방어선 (Optimistic UI & RYW): 이 한계를 백엔드(DB)에서 해결하려 하면 CQRS를 버리고 동기식 모놀리스로 돌아가야 합니다. 대신 클라이언트(React/App) 측에서 "성공 떨어지면, 서버 읽기(Query) 안 기다리고 그냥 내 폰 화면에서 '취소됨'으로 강제로 글씨를 바꿔버린다(Optimistic UI)"는 영리한 타협안과, 버전에 따른 Read-Your-Writes 라우팅 튜닝을 뜯어봅니다.
- Implement: 지연(Lag) 상황 UX 시뮬레이터 로직. Python 콘솔. 유저가
UPDATE커맨드를 날리면 서버는 비동기 처리(2초 뒤 Read DB 반영) 후 즉시200 OK리턴. 만약 클라이언트가 서버에 바로GET을 치면 낡은 데이터가 나옴. 이를 막기 위해 클라이언트 변수(local_state)를UPDATE즉시 갱신하고, 서버 조회는 생략하는 '낙관적 렌더링' 시나리오 데모.
7. Terminology
8. References
Primary
- [P1] CS2023 - Software Engineering (SE) - Software Architecture (Event Sourcing & CQRS)
- [P5] SFIA - Enterprise IT Architecture (ARCH) - Advanced Architectural Patterns
Secondary
- [Domain-Driven Design] Eric Evans - Repositories and Event Driven Architecture
- [Microservices Patterns] Chris Richardson - Event Sourcing and CQRS Pattern
Industry
- [Microsoft Azure Architecture Center] - CQRS pattern & Event Sourcing pattern
- [MartinFowler.com] - CQRS (Command Query Responsibility Segregation)
9. Final Checklist
Primary
- 전통적인 CRUD 방식이 데이터의 '최종 상태'만 남기고 과거의 수정/삭제 이력을 모두 날려버리는 단점을, 이벤트 소싱(Event Sourcing)이 Append-only 이력 보존으로 어떻게 완벽히 극복하는지 설명할 수 있는가?
- 시스템에 100만 건의 이벤트가 쌓였을 때 단일 유저의 현재 잔액을 구하기 위한 Replay(재생) 과정의 O(N) 연산 부하를, 스냅샷(Snapshotting) 전략을 통해 어떻게 O(1)에 가깝게 최적화할지 논증할 수 있는가?
Secondary
- 복잡한 마이크로서비스 환경에서 RDBMS 하나만으로 쓰기(ACID 트랜잭션)와 읽기(다차원 검색) 요구사항을 동시에 만족시키는 것이 왜 불가능한지, 그리고 CQRS가 이를 어떻게 찢어(Segregation) 구원하는지 물리적으로 해부할 수 있는가?
- 쓰기 DB(Command Store)에 저장된 이벤트를 카프카(Kafka)를 통해 구독하여, 읽기 전용 DB(Elasticsearch/Redis)에 검색하기 가장 훌륭한 형태(Materialized View)로 가공하여 찔러넣는 프로젝션(Projection) 파이프라인을 설계할 수 있는가?
Industry
- CQRS 적용 후, 유저가 데이터를 수정하자마자(Command 완료) 화면을 새로고침 했을 때 변경 전의 낡은 데이터가 보이는 '최종적 일관성(Eventual Consistency)'의 딜레마를 식별할 수 있는가?
- 백엔드의 동기화 지연(Lag)을 감추기 위해, 클라이언트(프론트엔드) 측에서 서버 응답을 기다리지 않고 로컬 UI를 먼저 업데이트해버리는 '낙관적 렌더링(Optimistic UI)'이나 버전 기반 라우팅(Read-Your-Writes) 방어 기제를 튜닝할 수 있는가?