ES(Event Sourcing) 와 CQRS(Command Query Responsibility Segregation) 는 시스템의 원본 데이터(source of truth) 를 어떤 형태로 둘 것인가, 그것으로부터 표현을 어떻게 만들 것인가의 결정이다.

ES 는 원본 데이터를 상태가 아닌 변화의 시퀀스로 둔다. 현재 상태는 그 시퀀스로부터 도출된다. CQRS 는 같은 원본 데이터로부터 서로 다른 표현을 분리한다. 두 패턴은 별개지만 함께 묶여 동작한다. 새 read model 이 필요하면 새 projection 으로 같은 이벤트 시퀀스를 다시 소비하면 된다.

flowchart LR
  A[Command] --> B[(Event Store
append-only)] B --> C[Projection] C --> D[Read Model
검색] C --> E[Read Model
통계] C --> F[Read Model
UI]

Event Sourcing

CRUD 는 흔히 한 가지를 원본 데이터로 둔다. 테이블 한 줄의 현재 값이다. 주문 테이블에 PAID 라고 적혀 있으면 그 주문은 결제됐다. 변화는 UPDATE 로 덮어써지므로 어떻게 그 상태가 되었는지는 사라진다.

ES 는 그 대신 변화의 시퀀스를 둔다. OrderCreated, PaymentRequested, PaymentCompleted, OrderShipped 처럼 일련의 이벤트가 append-only 로 쌓이고, 그 시퀀스가 원본 데이터다. “현재 주문이 PAID 인가” 는 이벤트들을 처음부터 재생(replay) 한 결과로 얻는다. 상태는 이벤트 시퀀스에서 도출된 결과다.

이 작은 차이가 시스템 설계의 전제를 바꾼다.

  • audit log 가 자연스럽게 따라온다. 모든 변화가 이벤트로 기록되어 있으므로 별도 audit 인프라가 불필요하다.
  • time travel debugging 이 가능해진다. 특정 시점까지 replay 하면 그 시점의 상태가 도출된다.
  • 새 view 를 추가하는 자유가 생긴다. 이벤트 시퀀스로부터 새로운 projection 을 만들면 새 read model 이 나온다.

대신 비용이 따라온다. 이벤트가 누적되면 매번 처음부터 replay 하는 게 비싸진다. 그래서 snapshot 이 도입된다. 특정 시점까지의 상태를 저장해두고 그 이후 이벤트만 replay 한다. schema 변경도 까다롭다. 과거에 발행된 이벤트도 여전히 시스템의 원본 데이터라 함부로 형태를 바꿀 수 없다.

이벤트 저장소(event store) 로 Kafka 가 자주 거론된다. append-only log 라는 점이 ES 와 부합한다. 단 Kafka 는 일정 기간 후 데이터를 삭제하는 보존 정책이 일반적이라, 모든 이벤트의 영구 보존을 전제하는 ES 의 전제와 어긋난다.

CQRS

CRUD 는 한 모델로 쓰기와 읽기를 모두 처리한다. 주문 도메인이라면 Order 라는 한 모델이 검증, 상태 변경, 검색, 통계를 다 떠안는다. 작은 시스템에서는 깔끔하지만 복잡해지면 한 모델로 두 요구를 모두 충족시키기 어려워진다. 쓰기는 비즈니스 규칙과 트랜잭션 무결성을 원하고, 읽기는 빠른 조회와 다양한 표현을 원하는데 한 모델이 두 요구를 모두 잘 충족시키는 경우는 드물다.

CQRS 는 이 비대칭을 그대로 받아들이고 쓰기 모델(Command 측) 과 읽기 모델(Query 측) 을 분리한다. 쓰기 모델은 도메인 규칙과 일관성에, 읽기 모델은 조회 효율과 표현에 각각 최적화한다. 둘은 같은 원본 데이터를 다른 형태로 다룬다.

읽기 모델은 여러 개일 수 있다. 검색용 인덱스, 통계용 집계 테이블, UI 화면용 뷰처럼 다른 형태의 read model 이 같은 원본 데이터로부터 도출된다. 새 화면이 필요하면 새 read model 을 추가하면 된다.

read model 갱신 시점은 세 갈래로 갈린다.

  • 동기 갱신 — 쓰기와 같은 트랜잭션에서 read model 도 함께 업데이트. 일관성은 즉시지만 쓰기 트랜잭션의 부담이 커진다.
  • 비동기 갱신 — 쓰기 후 이벤트로 알리고 read model 은 별도 흐름에서 업데이트. eventual consistency 를 받아들이는 가장 흔한 패턴.
  • on-demand 갱신 — 읽을 때 도출. 조회 빈도가 낮은 view 에 어울린다.

비동기 갱신을 택하면 staleness 가 생긴다. 쓰기 직후 짧은 시간 동안 read model 이 옛 값을 반환할 수 있다. 이 staleness 를 비즈니스 측에서 받아들일 수 있느냐가 CQRS 채택 가능 여부를 결정한다.

ES 와 CQRS 의 결합

ES 와 CQRS 는 서로 독립이지만 잘 묶인다.

ES 가 원본 데이터를 이벤트 시퀀스로 두면 CQRS 의 read model 이 그 시퀀스를 입력으로 받는다. 이벤트가 append 되면 projection 이 소비해 read model 을 갱신한다. 이벤트가 그대로 read 측의 자료가 되므로 두 패턴 사이의 결합 비용이 작다. 새 read model 이 필요하면 새 projection 을 만들어 이벤트 처음부터 replay 하면 된다.

이 결합은 Saga 와 Outbox 패턴과도 이어진다. ES 의 이벤트가 그대로 Saga 트리거가 되고, Outbox 가 보장하는 “DB write 와 이벤트 발행의 원자성” 은 ES 환경에서 처음부터 같은 트랜잭션으로 묶인다. 이벤트 저장이 곧 비즈니스 데이터 저장이기 때문이다.

대신 원본 데이터(이벤트) 와 표현(read model) 이 다른 저장소에 있어서 read 와 write 일관성이 즉시가 아니다. 사용자가 자기 행동의 결과를 화면에서 즉시 확인하지 못하는 경우가 생기고, 이 점이 UX 결정에 직접 영향을 준다.

채택 기준

분해 기준 — 도메인 경계, 데이터 소유권, 스케일 패턴, 장애 격리 — 이 ES/CQRS 채택의 출발점이 된다.

  • 도메인 경계에서 audit/compliance 가 핵심 요구사항이라면 ES 가 잘 맞는다. 금융, 보험, 의료 같은 도메인.
  • 데이터 소유권 기준에서 한 도메인이 여러 형태의 read model 이 필요하다면 CQRS 가 따라온다. 검색, 통계, 실시간 대시보드가 같은 원본 데이터로부터 다른 표현을 필요로 할 때.
  • 스케일 패턴 기준에서 read 와 write 의 부하 패턴이 다르다면 CQRS 의 read model 분리가 적합하다. read replica 와는 성격이 다른 분리다. 모델 자체가 다르다.
  • 장애 격리 기준에서 read 측의 장애가 write 를 막아서는 안 된다면 read model 의 비동기 갱신이 격리를 제공한다.

모든 시스템이 ES/CQRS 를 필요로 하지는 않는다. 단일 CRUD 로 충분한 시스템에서 ES 를 도입하면 시스템 전반에 도출 비용이 더해진다. 분해 기준이 ES/CQRS 의 가치를 정하고, 그 가치가 비용을 정당화하는지를 보고 채택을 결정한다.

운영 비용

ES 는 audit log, time travel, 새 view 추가의 자유라는 가치를 가져오지만 그 가치를 받기 위한 비용이 시스템 전반에 분산된다.

  • replay 비용 — snapshot 이 없으면 매번 처음부터 재생. snapshot 이 있어도 운영 부담이 따른다.
  • projection 운영 — read model 별로 갱신 흐름을 따로 관리하고 실패 시 재처리 전략을 준비한다.
  • schema 변경의 어려움 — 과거 이벤트를 함부로 바꿀 수 없어 versioning, upcasting, weak/strong schema 같은 별도 운영 패턴이 필요하다. 별도 글로 다룬다.
  • 디버깅의 추상도 증가 — 이벤트 시퀀스와 projection 의 정합성 추적이 단일 상태 모델보다 어렵다.

audit, compliance, time travel debugging, 새 view 추가의 자유가 시스템에서 본질적 가치가 아니라면 단순 CRUD 가 답인 경우가 많다고 본다. 패턴이 도입의 목적이 되면 정상 흐름에서는 잘 동작하지만 ES 의 가치를 누리지 못한 채 비용만 쌓인다.

참고

  • Microservices Architecture — 분해 기준(도메인 경계, 데이터 소유권, 스케일, 장애)과 통신 결정. ES/CQRS 채택 가치 판단의 전제.
  • Distributed Transactions — 단일 트랜잭션의 분해/재조립과 Saga·Outbox. ES 의 이벤트가 그대로 Saga 트리거가 되는 결합 지점.
  • Kafka 기초와 KRaft 모드 — Kafka 의 동작과 데이터 보존 정책. ES 의 영구 보존 전제와의 전제 차이를 이해하는 배경.