이 글에서 얻는 것

  • 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) 대시보드용 집계 테이블, 그리고 갱신 지연이 사용자 경험에 미치는 영향을 적어보기
  • 동일 이벤트 중복 처리/순서 뒤집힘을 가정하고, 어떤 키/버전으로 멱등성을 만들지 설계해보기