TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

AI 서비스 개발, 운영, 성능개선을 하나의 루프로 연결합니다

aidevops.kr에서 LLMOps, RAG, AI Agent, 관측성, 평가, 비용-성능 최적화를 실전 운영 관점으로 정리합니다.

CQRS + Event Sourcing 실전 구현 가이드

· 수정 4월 22일
CQRS + Event Sourcing 실전 구현 가이드 다이어그램
이 글에서 다루는 핵심 흐름, 아키텍처 구조, 주요 판단 포인트를 한눈에 이해할 수 있도록 정리한 그림입니다.

CQRS와 Event Sourcing은 아키텍처 과시용 패턴이 아닙니다. CRUD 모델이 감추고 있던 비즈니스 규칙, 상태 전이, 조회 요구사항을 더 명시적으로 다루기 위한 선택입니다.

이 패턴의 진짜 가치는 기술적 멋이 아니라 다음 질문에 답할 수 있게 해주는 데 있습니다.

  • 왜 이 변경이 거절되었는가?
  • 사고 직전에는 정확히 어떤 일이 순서대로 발생했는가?
  • 서로 다른 조회 화면을 만들기 위해 왜 쓰기 모델까지 복잡하게 만들어야 하는가?
  • 버그 수정이나 스키마 변경 뒤에 파생 상태를 다시 만들 수 있는가?

문제는 이 장점이 공짜가 아니라는 점입니다. 도메인이 충분히 복잡하지 않다면, CQRS와 Event Sourcing은 해결보다 부담을 더 많이 남길 수 있습니다.

CQRS가 실제로 바꾸는 것

CQRS는 단순히 클래스를 나누는 기법이 아니라, 쓰기와 읽기가 서로 다른 최적화 목표를 가진다는 사실을 설계에 반영하는 방식입니다.

  • 쓰기 모델은 비즈니스 규칙과 불변식을 지키는 데 집중합니다.
  • 읽기 모델은 응답 속도와 화면별 데이터 형태에 집중합니다.
  • 두 모델은 같은 데이터를 다루더라도 다른 구조를 가질 수 있습니다.

중요한 점은 CQRS가 곧바로 마이크로서비스나 별도 데이터베이스를 뜻하지는 않는다는 것입니다. 실무에서는 다음 정도만 분리해도 효과가 큽니다.

  • command와 query를 다른 핸들러로 관리
  • aggregate는 쓰기 모델에서만 사용
  • 조회용 모델은 화면 또는 API 목적에 맞게 비정규화

Event Sourcing이 실제로 바꾸는 것

Event Sourcing은 현재 상태만 저장하지 않고, 상태를 만든 사건의 흐름 자체를 저장합니다.

예를 들어 계좌 시스템에서 balance = 150만 저장하는 대신 다음과 같은 사실을 남깁니다.

  • AccountOpened
  • FundsDeposited
  • FundsWithdrawn
  • OverdraftLimitRaised

현재 상태는 이 이벤트를 재생해서 만들거나, snapshot 이후 이벤트만 재생해서 복원합니다.

이 접근은 다음과 같은 장점을 줍니다.

  • 비즈니스 히스토리가 그대로 남음
  • Projection과 Read Model 생성이 자연스러움
  • 코드 수정 후에도 읽기 모델을 재구축할 수 있음
  • 상태 변화의 원인을 추적하기 쉬움

반대로 다음 운영 부담도 반드시 따라옵니다.

  • 이벤트 스키마 진화 관리
  • 재생 성능 관리
  • projection idempotency 보장
  • write/read 간 지연 모니터링
  • 재처리와 백필 도구 준비

이런 경우에 도입할 가치가 큽니다

  • 쓰기 규칙이 복잡하고 읽기 요구사항이 크게 다름
  • 변경 이력을 비즈니스 자산으로 보존해야 함
  • 같은 상태를 여러 조회 모델로 투영해야 함
  • 시간 순서가 중요한 도메인 판단이 많음
  • 사후 재처리, 리플레이, 감사를 실제로 자주 활용함

대표적인 사례는 다음과 같습니다.

  • 결제, 정산, 원장 시스템
  • 주문, 배송, 보상 흐름이 긴 커머스 도메인
  • 예약, 할당, 해제 규칙이 복잡한 재고 시스템
  • 규제와 감사가 중요한 백오피스 영역

반대로 다음과 같은 경우에는 과한 선택이 될 가능성이 큽니다.

  • 단순 관리자 CRUD
  • 내부 운영툴 수준의 낮은 리스크 서비스
  • eventual consistency를 UX에서 수용하기 어려운 시스템
  • 보고서 몇 개만 추가하면 해결되는 단순 조회 문제

핵심 구조

Command -> Aggregate -> Event Store -> Projection -> Read Model

중요한 판단

  • 이벤트는 로그가 아니라 도메인 사실이어야 함
  • Aggregate는 규칙 경계여야 함
  • Projection은 지연과 재처리를 감당하는 별도 파이프라인으로 봐야 함
  • eventual consistency를 수용할 수 있어야 함

이 네 가지는 구현 디테일을 크게 바꿉니다.

이벤트는 도메인 언어로 설계해야 합니다

좋은 이벤트는 비즈니스가 이해할 수 있는 사실입니다.

좋은 예:

  • OrderPlaced
  • PaymentAuthorized
  • ShipmentDispatched

좋지 않은 예:

  • OrderUpdated
  • StatusChanged
  • RowModified

이벤트가 너무 일반적이면 의미가 코드 여러 곳으로 흩어집니다. 그러면 리플레이도 어렵고, 다른 소비자가 안전하게 활용하기도 어려워집니다.

실무 기준으로는 다음 원칙이 유효합니다.

  • 이름은 도메인 용어를 사용
  • payload는 나중에 보더라도 사실을 이해할 수 있게 구성
  • 한 번 발행한 이벤트는 immutable하게 취급

Aggregate는 크게 만들수록 좋아지지 않습니다

Aggregate는 UI 탐색 모델이 아니라 불변식을 지키는 트랜잭션 경계입니다.

좋은 Aggregate는 보통 다음 특징을 가집니다.

  • 명령 검증에 필요한 정보만 로딩
  • 규칙 검증을 동기적으로 완료
  • 외부 조회 모델에 의존하지 않고 핵심 판단을 내림

반대로 Aggregate가 화면 요구를 다 떠안기 시작하면 CQRS의 장점이 사라집니다. 조회는 Projection과 Read Model로 보내고, 규칙만 Aggregate가 담당하도록 경계를 유지하는 편이 좋습니다.

Projection은 재생 가능한 파이프라인이어야 합니다

Projection을 단순 백그라운드 소비자로 보면 운영 단계에서 바로 막힙니다. Projection은 언제든 다시 만들 수 있어야 하는 파이프라인이어야 합니다.

실전에서는 보통 다음이 필요합니다.

  • idempotent 처리
  • 순서 보장 전략
  • checkpoint 관리
  • 전체 재빌드 도구
  • lag 관측 지표

재생이 어렵거나 위험하다면 Event Sourcing의 핵심 가치가 크게 줄어듭니다.

정합성은 기술 문제가 아니라 제품 결정입니다

CQRS에서 write model과 read model 사이의 eventual consistency는 흔한 전제입니다. 하지만 이것은 단순한 인프라 문제가 아니라 제품 경험을 바꾸는 결정입니다.

팀은 최소한 아래를 명확히 해야 합니다.

  • 읽기 모델이 얼마나 늦어져도 되는가?
  • 명령 성공 직후 사용자에게 무엇을 보여줄 것인가?
  • read-your-own-write가 필요한 화면은 어디인가?
  • 운영자가 projection lag를 어떻게 확인할 것인가?

실무 패턴으로는 다음이 자주 쓰입니다.

  • write 결과를 즉시 응답에 포함
  • read model 버전이 따라올 때까지 polling
  • WebSocket 또는 SSE로 projection 완료 알림 전달
  • UI에 처리 중 상태를 명시

이 지연을 제품이 감당할 수 없다면, CQRS 도입은 다시 검토하는 편이 맞습니다.

Snapshot과 버전 관리

이벤트가 쌓일수록 재생 비용이 커지므로 snapshot 전략은 성능 최적화 관점에서 미리 준비해야 합니다.

  • 특정 stream version 또는 이벤트 개수 기준으로 snapshot 생성
  • snapshot 자체의 schema version 명시
  • snapshot은 source of truth가 아니라 캐시로 취급
  • snapshot 없이도 재생이 가능한지 주기적으로 검증

이벤트 버전 관리도 초기에 방향을 정해야 합니다.

  • 새 이벤트 타입을 추가하는 append-only 전략
  • CustomerAddressChangedV2 같은 버전드 이벤트
  • 리플레이 시점에 과거 payload를 최신 구조로 변환하는 upcaster

소비자가 늘어난 뒤에 이벤트 계약을 급하게 바꾸면 비용이 훨씬 커집니다.

운영에서 자주 과소평가되는 문제

실무에서 어려운 부분은 Aggregate 구현보다 운영 지속성인 경우가 많습니다.

  • 재처리 시 중복 부작용이 발생하는 projection
  • 도메인 사실이 아니라 내부 구현 디테일이 섞인 이벤트
  • 운영 환경에서 검증되지 않은 read model backfill 절차
  • 경계가 잘못 잡혀 비대해진 stream
  • 지원팀이 event history, lag, dead-letter queue를 관측할 수 없는 상태

도입 전에는 최소한 다음 질문에 답할 수 있어야 합니다.

  • 특정 projection 하나만 안전하게 다시 돌릴 수 있는가?
  • 새 read model을 운영 환경에서 어떻게 백필할 것인가?
  • 장애 상황에서 event stream을 어떻게 조사할 것인가?
  • 이벤트 계약 변경을 어떻게 점진적으로 배포할 것인가?

Spring Boot 기준 구현 힌트

Spring Boot에서는 보통 다음처럼 역할을 나누는 편이 실용적입니다.

  • command handler가 트랜잭션과 aggregate 로딩을 조율
  • aggregate는 인프라 이벤트가 아니라 domain event를 생성
  • event store append가 write-side commit 경계가 됨
  • 외부 시스템 연동은 outbox 또는 별도 스트림 브리지로 분리
  • projection 소비자는 재시도와 관측 정책을 독립적으로 가짐

특히 중요한 것은 도메인 판단이 Aggregate 안에 남아 있어야 한다는 점입니다. Aggregate가 HTTP 호출, 조회용 repository, 외부 분석 시스템에 기대어 명령을 검증하기 시작하면 모델이 쉽게 흔들립니다.

도입 판단 체크리스트

다음이 대부분 참이면 도입 가치가 큽니다.

  • 히스토리가 장기적으로 의미 있음
  • 읽기와 쓰기 요구가 구조적으로 다름
  • 파생 모델 재생성이 실제로 유용함
  • 운영 도구에 투자할 여력이 있음
  • eventual consistency를 수용할 수 있음

다음이 대부분 참이면 보류하는 편이 좋습니다.

  • 도메인이 단순함
  • CRUD와 리포트 몇 개면 충분함
  • 운영 성숙도가 아직 낮음
  • 재생과 projection 운영이 부담스러움
  • 강한 트랜잭션 정합성이 항상 필요함

마무리

핵심은 패턴의 난이도가 아니라, 그 복잡성이 도메인 이해도와 운영 유연성을 실제로 높여주는가입니다. 효과가 있다면 강력한 무기가 되지만, 그렇지 않다면 잘 설계된 트랜잭션 모델이 훨씬 더 높은 생산성을 줍니다.

Continue Reading

다음으로 읽기 좋은 글

다음 탐색

이 주제를 시스템 관점으로 더 이어서 보기