CQRS (Command Query Responsibility Segregation)
CQRS 는 애플리케이션의 쓰기 (Command) 와 읽기 (Query) 를 별도 모델·경로로 분리해 각 측면을 독립적으로 최적화·확장하는 패턴이다.
쓰기 모델은 도메인 규칙과 상태 변경을 책임지고, 읽기 모델은 조회 성능에 맞춰 전용 뷰 스키마/저장소를 쓴다.
필요에 따라 Event Sourcing 을 결합해 이벤트로 읽기 모델을 갱신할 수 있으나 필수는 아니다.
마이크로서비스·복잡 도메인에서 성능·확장성·보안을 높일 수 있지만, 구현·운영 복잡성과 일관성 관리 비용이 증가하므로 신중한 적용이 요구된다.
핵심 개념
CQRS 는 “쓰기 전용 모델” 과 “읽기 전용 모델” 을 완전히 분리해 각자에 맞게 설계·확장하는 방식이다.
쓰기는 도메인 규칙/트랜잭션을 지키고, 읽기는 질의 성능만 바라본다. 둘 사이 데이터는 주로 이벤트로 흘러가며, 이 과정에서 최종일관성을 받아들인다. 이벤트 전달의 원자성은 Outbox로, 다 단계 쓰기 조율은 Saga로 보완한다.
개념 | 무엇 | 왜 중요한가 |
---|---|---|
CQRS | 읽기/쓰기 책임·모델·저장소 분리 | 독립 최적화/확장, 복잡성 분리, 경합 감소. |
CQS | 명령/조회 역할 분리 원칙 (메서드 레벨) | CQRS 의 이론 기반, 부작용 최소화. |
Write Model | 도메인 규칙·트랜잭션을 담는 쓰기 전용 모델 | 무결성 보장·비즈니스 규칙 초점. |
Read Model | 조회 전용 프로젝션/뷰/캐시 | 고성능 조회·저비용 스케일아웃. |
Event Sourcing | 상태 변경을 이벤트 로그로 저장 | 이력/감사·재생·프로젝션 생성 용이. |
최종일관성 | 읽기 모델 반영이 지연될 수 있음 | 성능·확장성 대가를 인지하고 UX 보정 필요. |
Outbox | DB 변경과 이벤트 발행의 원자성 보장 | 듀얼 라이트/유실 방지·멱등 처리 토대. |
Saga | 분산 쓰기 단계의 조율/보상 트랜잭션 | 다 애그리게이트/서비스 쓰기 안전화. |
- CQRS 는 “분리로 얻는 최적화” 가 본질이고, 최종일관성을 Outbox·Saga로 실무급으로 다룬다. 이벤트 소싱은 선택이지만 읽기 모델 생성엔 강력한 도우미다.
실무 구현과의 연관성
핵심 개념 | 왜 연관? | 어떻게 구현? (대표 수단) | 품질 지표/체크 |
---|---|---|---|
CQRS | 읽기/쓰기 성격·튜닝 포인트가 다름 | API/핸들러·스키마 분리, 서비스/DB 분리 | R/W 비율, 읽기 p95, 쓰기 p95. |
Read Model | 조회 부하 대응·개별 최적화 | 머티리얼라이즈드 뷰, 캐시, NoSQL, 인덱스 | 뷰 갱신 지연 (ms), 캐시 히트율. |
Write Model | 무결성·도메인 규칙 중심 | 애그리게이트/도메인 서비스·강한 트랜잭션 | 도메인 불변식 위반률, 트랜잭션 재시도율. |
최종일관성 | 분리 전파 지연의 필연 | UX 보정 (스낵바, 로컬 캐시), R-Y-W 전략 | 읽기 최신성 SLA, 간극 분포. |
Event Sourcing | 읽기 모델 생성·감사 요구 | 이벤트→프로젝션 파이프라인 | 재생 시간, 이벤트 멱등률. |
Outbox | DB 변경 + 이벤트 발행 원자화 | 트랜잭션 내 Outbox 기록→릴레이 전송 | 미전송 이벤트 수, 중복 전송률. |
Saga | 분산 쓰기 조율 | 오케스트레이션/코레오그래피·보상 트랜잭션 | 보상률, 타임아웃률. |
- 읽기=뷰 튜닝, 쓰기=도메인 무결성, 간극=최종일관성 관리. 신뢰성은 Outbox, 분산 일관성은 Saga로 덧댄다.
기초 이해 (Foundation Understanding)
개념 정의 및 본질
CQRS 는 프로그램에서 " 쓰기 (명령)" 와 " 읽기 (조회)" 를 완전히 다른 경로로 처리하는 설계 방법이다.
예를 들어, 주문을 넣을 때는 쓰기 모델이 처리하고, 주문 내역을 볼 때는 조회 모델이 처리한다.
이렇게 나누면 각 경로를 성능에 맞게 최적화할 수 있지만, 두 모델의 데이터가 맞춰져 있어야 해서 동기화가 필요하다. 작은 시스템보다는 복잡하고 읽기·쓰기 비율이 크게 다른 시스템에 유리하다.
구분 | 내용 |
---|---|
정의 | 데이터 변경 (명령) 과 조회 (쿼리) 의 책임을 별도의 모델로 분리하는 아키텍처 패턴 |
기원 | Robert C. Martin 의 CQS 원칙 확장 |
구성 요소 | Command Model(쓰기 전용), Query Model(읽기 전용), 별도 저장소, 이벤트 버스 |
목적 | 읽기/쓰기 요구사항을 독립적으로 최적화, 성능·확장성 향상 |
특징 | 모델·저장소·서비스 계층 분리, Event Sourcing 과 잘 결합 |
장점 | 읽기 성능 최적화, 확장성 확보, 유지보수 용이 |
단점 | 데이터 동기화 복잡성, 과도 설계 가능성 |
적용 사례 | 읽기 요청이 많은 시스템, 복잡한 도메인, DDD 환경 |
CQRS 는 읽기와 쓰기를 분리해 각 경로를 최적화하는 아키텍처 패턴으로, 확장성과 성능을 높이는 대신 동기화 복잡성이 따른다.
graph TB Client[클라이언트] --> CommandAPI[명령 API] Client --> QueryAPI[조회 API] CommandAPI --> CommandModel[명령 모델] QueryAPI --> QueryModel[조회 모델] CommandModel --> WriteDB[(쓰기 DB)] QueryModel --> ReadDB[(읽기 DB)] WriteDB --> EventBus[이벤트 버스] EventBus --> ReadDB
등장 배경 및 발전 과정
CQRS 는 **" 읽기와 쓰기 문제를 따로 푼다 “**는 생각에서 출발했다.
CQS(메서드 원칙) → CQRS(시스템 설계) 로 확장되며, 읽기는 빠르게·쓰기는 정확하게를 동시에 달성하도록 진화했다. 클라우드 시대엔 읽기를 마음껏 수평 확장하고, 쓰기는 도메인 규칙에 집중하면서 둘 사이를 이벤트/비동기로 연결하는 게 표준이 됐다.
등장 배경
- 전통 CRUD 단일 모델이 읽기/쓰기의 상충 요구(인덱스/스키마/잠금) 로 병목 → 대규모·다지역 서비스에서 가속.
- CQRS 는 모델/핸들러/저장소 분리로 각자 최적화하고, 비동기 전파로 전체 시스템 처리량과 확장성 향상을 노린다.
발전 과정
시기 | 사건/주요 인물 | 왜 등장했나 | 무엇이 개선됐나 |
---|---|---|---|
1988~90s | CQS·Bertrand Meyer | 부작용 분리·코드 추론성 향상 | 명령/조회 책임 경계 확립 |
2006~2010 | CQRS 정립·Greg Young | 읽기/쓰기 튜닝 포인트 상이 | R/W 모델·스토리지 분리로 독립 최적화 |
2009~2012 | Clarified CQRS, CQRS Journey | 오해 정리·실무 지침 필요 | ES 와의 관계 정리, 레퍼런스 아키텍처·가이드 제공 |
2010s~현재 | 클라우드·마이크로서비스·이벤트 기반 | 읽기 우세·분산·독립 확장 요구 | 읽기 수평 확장, 쓰기 무결성, 최종 일관성 운영 |
- CQS 가 이론적 분리를 제시했고, CQRS 가 이를 시스템 설계로 끌어올렸다. 2010 년대 이후 운영 지침과 사례가 축적되며 클라우드 환경에서 표준화됐다.
timeline title CQRS: 등장 배경 및 발전 과정 section 개념 형성 1988 : CQS 원칙 제안 (Bertrand Meyer) – 메서드 수준 분리 section 패턴 정립 2006-2010 : CQRS 명명·정립 (Greg Young) – 읽기/쓰기 분리 확립 2009 : Clarified CQRS (Udi Dahan) – 오해 정리 section 실무 확산 2012 : Microsoft "CQRS Journey" – 레퍼런스 구현·가이드 2010s-2025 : 클라우드·MSA·이벤트 기반 확산 – 수평 확장·최종 일관성
- CQS → CQRS로의 확장은 " 메서드 분리 " 를 " 시스템 분리 " 로 확장해 독립 최적화/확장을 가능하게 했다.
- 2010 년 전후 Young·Dahan의 정립·정리와 MS CQRS Journey가 실무 표준을 만들었고, 오늘날 읽기 우세·분산 환경에서 CQRS 는 사실상 기본 선택지가 되었다.
핵심 동기 및 가치 제안
CQRS 의 목적은 서로 다른 요구를 가진 읽기와 쓰기를 갈라서 잘하게 만들기다. 쓰기는 규칙·트랜잭션 일관성에 집중하고, 읽기는 빠른 응답과 다양한 뷰에 집중한다. 이렇게 나누면 각 부분을 따로 설계·배포·확장할 수 있어 성능과 유지보수성이 좋아진다. 필요하면 Event Sourcing 을 더해 이벤트로 읽기 모델을 채우고 이력/감사도 강화할 수 있지만, 반드시 결합해야 하는 것은 아니다.
범주 | 목적/가치 | 왜 필요한가 | 적용 포인트 |
---|---|---|---|
성능/확장성 | 읽기·쓰기 독립 최적화/스케일 | 경로 특성이 다름 (검증·트랜잭션 vs. 조회·캐시) | 쓰기: 도메인 모델·트랜잭션, 읽기: 뷰 DB/리플리카 |
복잡성 관리 | 변경 영향 축소·테스트 용이 | 규칙/조회 요구 동시 충족 시 결합도↑ | 코드·스키마·핸들러를 분리 배치 |
유지보수/팀 조직 | 병렬 개발·독립 배포 | 대규모 팀에서 충돌/대기 비용 큼 | 서비스/리포지토리 레벨 분리, CI/CD 분리 |
보안/권한 | 변경 경로 엄격, 조회 경로 안전 공개 | 쓰기 권한·검증이 더 중요 | 커맨드 핸들러 보호, 조회는 캐시/리드뷰로 확장 |
이벤트성 (옵션) | 이력/감사/재생, 비동기 업데이트 | 감사·타임머신 요구, 읽기 갱신 모델 필요 | ES 로 이벤트 발행→리드 모델 물질화 (필수 아님) |
- CQRS 는 이질적 요구 (쓰기 규칙 vs. 읽기 성능) 를 분리해 각각을 제대로 하도록 만든다.
- 독립 스케일·배포가 가능해지고, 팀·스키마 충돌을 줄여 변경 비용을 낮춘다.
- Event Sourcing 은 선택적으로 결합해 감사/재생/물질화 뷰를 강화한다.
주요 특징
CQRS 는 “쓰기 길(명령) 과 읽기 길(조회) 을 아예 다른 도로로 나누는 설계 " 다. 그래서 읽기 도로는 빠르게 조회하도록 깔고, 쓰기 도로는 규칙·검증을 두껍게 깐다. 바쁘면 읽기 도로만 폭 넓게 확장할 수 있고, 쓰기·읽기를 다른 DB로 운영할 수도 있다. 다만 두 도로 사이를 이벤트로 이어 주기 때문에, 읽기 쪽이 약간 늦게 반영되는 최종 일관성을 감수해야 한다.
특징 | 설명 | 왜 가능한가/근거 |
---|---|---|
모델 분리 | 명령과 조회를 다른 모델·인터페이스로 분리 | CQRS 정의 자체가 분리 원칙을 전제. |
독립 최적화/스케일 | 읽기·쓰기 각각 스키마·캐시·인덱싱·배포 단위 최적화·확장 | Azure 가 독립 스케일·스키마 최적화를 명시. |
저장소 분리/폴리글랏 | 읽기/쓰기 저장소를 분리하고 기술을 달리 쓸 수 있음 | " 서로 다른 데이터 스토어/기술 사용 " 을 공식적으로 제시. |
이벤트 기반 갱신 | 쓰기 이벤트를 읽기 모델의 프로젝션/뷰 갱신에 사용 | Materialized View·ES 문서의 조합 패턴. |
최종 일관성 | 분리된 저장소 간 비동기 동기화로 잠시 지연 허용 | Azure 가 ‘Eventual consistency’ 트레이드오프를 명시. |
보안/검증 경계 | 쓰기 측에 검증·권한 집중, 읽기 측은 읽기 전용 DTO | Azure 가 보안 이점과 역할 분리를 명시. |
CQS 기반 | " 질의는 상태를 바꾸지 않음 " 원칙을 시스템 레벨로 확장 | Fowler CQS 가 사상적 배경을 제공. |
- 분리가 출발점, 독립 최적화/스케일이 핵심 이점.
- 이벤트로 연결해 읽기 모델을 갱신하되 최종 일관성을 설계로 흡수.
- 필요하면 서로 다른 저장소/기술을 선택, 쓰기 경로에 보안·검증을 집중한다.
핵심 이론 (Core Theory)
핵심 설계 원칙
CQRS 는 도로 두 개를 까는 설계다. 한 도로 (쓰기) 는 신호·검문이 빡세고, 다른 도로 (읽기) 는 고속도로처럼 빠르다.
쓰기 도로에서 난 변화는 이벤트로 읽기 도로에 전달돼 안내표지 (프로젝션) 가 갱신된다. 다만 표지 갱신이 살짝 늦을 수 있음 (최종 일관성) 을 감수하면, 확장성과 성능을 크게 얻는다.
원칙 | 목적 | 필요한 이유 |
---|---|---|
책임 분리 (CQS) | 읽기/쓰기 부수효과·성능 요구 분리 | 한 모델로 두 요구 충족 시 상충·결합 ↑ |
단일 책임·관심사 분리 | 쓰기=규칙·검증, 읽기=조회 최적화 | 변경 이유 분리로 충돌·복잡도 ↓ |
독립 최적화·스케일 | 각 워크로드별 성능·비용 최적 | 읽기 편중/쓰기 편중을 개별 확장 |
이벤트 기반 갱신 | 읽기 모델 (프로젝션) 최신화 | 모델 분리로 직접 조인 불가→이벤트 전파 |
최종 일관성 | 성능·가용성 확보 | 비동기 동기화로 전파 지연 수용 |
폴리글랏 저장소 | 모델별 최적 DB 선택 | 데이터 특성별 강점 활용 |
보안·검증 경계 | 변경 경로에 강한 검증·인가 | 공격면 축소·감사 용이 |
단순함 우선 | 과설계 방지 | 복잡성·운영 비용 상쇄 |
핵심은 분리이고, 효과는 독립 최적화·확장이다. 읽기는 프로젝션으로 빠르게, 쓰기는 규칙·검증을 두껍게. 저장소는 필요하면 다르게, 일관성은 최종적으로 맞춘다. 단, 복잡성 비용이 정당화될 때만 쓴다.
기본 원리 및 동작 메커니즘
CQRS 는 “쓰기랑 읽기를 갈라서, 각자 잘하게 만든다” 가 전부다. 쓰기 경로는 규칙 검증과 트랜잭션에 집중하고, 읽기 경로는 빠른 응답과 맞춤형 뷰에 집중한다. 쓰기 후 이벤트를 흘려 보내 프로젝션이 읽기 DB 를 갱신하기 때문에, 조회 데이터는 약간 늦게 반영될 수 있다 (대신 속도·확장성이 좋아진다).
Event Sourcing 을 더하면 이벤트로 상태를 재구성·감사할 수 있지만 꼭 필요하진 않다.
기본 원리
원리 | 요지 | 구현 포인트 |
---|---|---|
책임 분리 | Command/Query 모델·핸들러·저장소 분리 | CommandHandler/QueryHandler, WriteDB/ReadDB 분리 |
CQS 기반 | Command=상태 변경, Query=부작용 없음 | 메서드/엔드포인트 계층에서 원칙 준수 |
물질화 뷰 | 이벤트로 ReadDB 갱신 (프로젝션) | 비정규화/캐시/리드뷰, 재빌드 가능 |
일관성 모델 | Eventual consistency 허용 | 읽기 지연·보상 로직·리드 모델 랙 모니터링 |
선택적 ES | ES 는 옵션(감사·재생 강화) | Event Store, 스냅샷·리플레이 전략 |
적용 판단 | 이득>복잡성일 때 채택 | 조회 패턴 다양/스케일 요구 뚜렷할 때 유리 |
- CQRS 는 구조 (모델·저장소) 까지 분리해야 효과가 크다.
- 읽기 측은 전용 뷰 DB로 단순·고속화하고, 필요하면 재구축한다.
- ES 결합은 선택이며, 도입 시 스냅샷·리플레이·이벤트 스키마 버저닝을 설계한다.
- 결국 성능·확장성 ↔ 복잡성의 트레이드오프를 판단하는 문제다.
동작 메커니즘
sequenceDiagram participant Client participant CmdAPI as Command API participant CmdHandler as Command Handler participant WriteDB as Write DB / Aggregate participant EvtBus as Event Bus (옵션: Event Store) participant Proj as Projection / ReadModel Updater participant ReadDB as Read DB (Materialized View) participant QryAPI as Query API participant QryHandler as Query Handler Client->>CmdAPI: Command 전송 CmdAPI->>CmdHandler: 검증/도메인 규칙 실행 CmdHandler->>WriteDB: 상태 변경(트랜잭션) CmdHandler-->>EvtBus: 이벤트 발행(필요 시) EvtBus-->>Proj: 이벤트 전달 Proj->>ReadDB: 읽기 모델 갱신(비동기) Client->>QryAPI: Query 전송 QryAPI->>QryHandler: 질의 처리 QryHandler->>ReadDB: 물질화 뷰 조회 ReadDB-->>QryHandler: 결과 QryHandler-->>Client: 응답
- 명령 경로는 규칙 검증→트랜잭션 저장→이벤트 발행까지 담당하고,
- 프로젝션이 이벤트를 소비해 ** 읽기 모델 (비정규화/캐시)** 을 갱신한다.
- 조회 경로는 읽기 모델만 조회하므로 빠르지만, 반영 지연(eventual consistency) 을 수반할 수 있다.
graph TD User[사용자] --> API API --> CommandHandler[명령 핸들러] CommandHandler --> WriteDB[쓰기DB] API --> QueryHandler[조회 핸들러] QueryHandler --> ReadDB[조회DB] WriteDB --> EventBus[이벤트 버스] EventBus --> ReadDB
- 사용자는 API 를 통해 명령 (Command) 과 조회 (Query) 를 분리하여 요청.
- Command 는 CommandHandler, WriteDB 를 통해 처리, 필요시 이벤트 (이벤트 소싱) 발행.
- Query 는 QueryHandler, ReadDB 를 통해 빠르게 조회.
아키텍처 및 구성 요소
CQRS 는 쓰기 (정확성) 와 읽기 (속도) 를 서로 다른 모델과 저장소로 나눠 각자 최적화하는 설계다.
쓰기에서 바뀐 내용은 이벤트/배치로 읽기 모델에 반영되어, 전체 시스템은 보통 최종 일관성으로 동작한다. 신뢰성·분산 일관성은 Outbox와 Saga로 보강한다.
graph TB %% 외부 인터페이스 U[Client/UI] -->|Commands| CMDAPI[Command API/Bus] U -->|Queries| QAPI[Query API/Bus] %% Command Side subgraph "Command Side (Write)" CH[Command Handler<br/>- 검증·도메인 규칙·트랜잭션] WM[Write Model] WDB[(Write DB)] CH --> WM --> WDB end CMDAPI --> CH %% 신뢰성 보완 subgraph Reliability OX[Outbox Table] RELAY[Outbox Relay] end WDB -. 동일 트랜잭션 .-> OX OX --> RELAY %% 이벤트/프로젝션 subgraph Eventing MB[(Event Bus/Broker)] ES[(Event Store)] PROJ[Projection/Read Model Updater] end RELAY --> MB MB --> ES MB --> PROJ %% Query Side subgraph "Query Side (Read)" RM[Read Model<br/>- 비정규화/인덱스/캐시] RDB[(Read DB/Cache)] QH[Query Handler] PROJ --> RM --> RDB QAPI --> QH --> RDB end %% 분산 트랜잭션 subgraph "Coordination (Optional)" SAGA[Saga Orchestrator/Choreography] end CH -. cross-service commands .-> SAGA SAGA -. emits .-> MB
- Command 경로는 무결성과 도메인 규칙에 집중 (트랜잭션). Outbox로 DB 변경 + 이벤트 발행의 원자성을 보장한다.
- Eventing 경로는 브로커를 통해 이벤트를 전달하고 Projection이 이를 Read Model로 반영 (머티리얼라이즈드 뷰).
- Query 경로는 읽기 전용 저장소만 조회 (부작용 없음). 읽기/쓰기의 독립 확장이 가능하다.
- Saga는 다 서비스 쓰기를 보상/오케스트레이션으로 조율한다.
구성 요소
구성 요소 | 핵심 역할 | 대표 기술/패턴 | 품질 지표 예시 |
---|---|---|---|
Command Handler | 명령 검증·도메인 규칙·트랜잭션 | 계층형/DDD 애그리게이트 | 쓰기 p95, 실패율 |
Write Model/DB | 무결성·동시성 관리 | RDBMS/도큐먼트 | Deadlock/Retry 율 |
Outbox/Relay | 이벤트 발행 원자화 | Outbox 테이블·릴레이 | 미전송 이벤트 수 |
Event Bus | 비동기 전달 | Kafka/RabbitMQ | 지연/중복률 |
Event Store | 이벤트 영속 | EventStoreDB 등 | 재생 시간 |
Projection | 읽기 모델 생성 | Stream processor | 뷰 갱신 지연 |
Read Model/DB | 조회 최적화 | NoSQL/캐시/뷰 | 읽기 p95/히트율 |
Query Handler | 질의 처리 | API/GraphQL/GRPC | 오류율 |
Saga | 분산 쓰기 조율 | Orchestrator/Choreo | 보상률/타임아웃 |
- 쓰기=무결성/도메인, 읽기=속도/스케일에 집중하도록 경로와 저장소를 분리한다.
- Outbox+Projection으로 최종 일관성을 통제 가능한 방식으로 운영하고, 분산 쓰기는 Saga로 안전하게 조율한다.
주요 기능과 역할
CQRS 는 프로그램에서 " 데이터 바꾸는 부분 " 과 " 데이터 보는 부분 " 을 완전히 다른 구조로 설계하는 방법이다.
- Command Side는 ’ 규칙대로 데이터를 바꾸는 ’ 역할을 한다.
- Query Side는 ’ 최대한 빠르고 보기 좋게 데이터를 보여주는 ’ 역할을 한다.
- 두 영역은 이벤트라는 메시지를 주고받으며 동기화된다.
이렇게 나누면 각 부분을 더 잘 최적화할 수 있고, 큰 시스템에서도 성능을 유지하기 쉽다.
영역 | 구성 요소 | 주요 기능 | 개선·해결 효과 |
---|---|---|---|
명령 처리 (Command Side) | Command Model, Command Handler | 비즈니스 규칙 검증, 상태 변경, 트랜잭션 처리, 이벤트 발행 | 데이터 무결성·일관성 확보, 도메인 규칙 강화 |
조회 처리 (Query Side) | Query Model, Query Handler | 성능 최적화 조회, 다양한 View 제공, 캐싱·비정규화 활용 | 응답 속도 향상, 읽기 전용 확장성 강화 |
이벤트 관리·프로젝션 | Event Bus, Projection, Event Store(선택) | 이벤트 발행·전달, 읽기 모델 업데이트, 감사 추적 | 최종 일관성 유지, 결합도 감소, 장애 복구 용이 |
CQRS 의 Command 는 무결성과 규칙 준수를, Query 는 조회 성능과 확장성을, Event 관리·프로젝션은 양측 간 연결과 일관성 유지를 담당한다.
특성 분석 (Characteristics Analysis)
장점 및 이점
CQRS 는 도로를 두 개로 가르는 설계다.
한쪽 (쓰기) 은 규칙·검증이 빡센 도로, 다른 쪽 (읽기) 은 고속도로다. 고속도로 표지판 (프로젝션/뷰) 은 쓰기에서 온 이벤트로 갱신된다. 그래서 읽기를 크게 늘려도 쓰기가 버티고, 필요하면 두 도로를 따로 확장하거나 다른 DB를 쓸 수 있다.
단, 표지판 갱신이 약간 늦는 최종 일관성은 설계·UX 로 흡수해야 한다.
구분 | 항목 | 설명 |
---|---|---|
성능 | 조회 성능 향상 | 읽기 모델을 프로젝션/머티리얼라이즈드 뷰로 구성해 복잡 조인 없이 고속 응답 |
성능 | 쓰기 성능·안정성 | 쓰기 모델이 도메인 규칙·트랜잭션에 집중하여 락 경합·조회 부담 감소 |
확장성 | 독립적 스케일링 | 읽기/쓰기 파이프라인을 별도로 수평 확장 |
확장성 | 폴리글랏 퍼시스턴스 | 읽기/쓰기에 다른 저장소·스키마 (예: SQL/NoSQL) 적용 |
유지보수 | 관심사 분리 | 쓰기=검증·규칙, 읽기=DTO/뷰로 분리되어 변경 충돌·복잡도 감소 |
보안 | 권한 경계 강화 | 쓰기 엔드포인트에만 변경 권한을 집중, 읽기는 최소 권한 조회 |
조직 | 병렬 개발·배포 | 읽기/쓰기 컴포넌트를 분리해서 팀 단위 병렬 개발/배포 |
운영 (조합) | 감사/재구성 (ES 결합) | 이벤트 로그 재생으로 읽기 모델 재생성·감사 추적 |
핵심은 분리다. 분리 덕에 읽기는 빠르게, 쓰기는 엄격하게 다듬을 수 있고, 각자 따로 확장·따로 DB를 선택한다. 감사·재구성은 ES 를 곁들였을 때 특히 강력해진다.
단점 및 제약사항과 해결방안
CQRS 의 단점은 한마디로 ” 복잡해지고, 읽기가 늦게 반영될 수 있다 “ 이다. 그래서 이득이 확실한 경우(읽기/쓰기 부하·요구가 매우 다를 때) 만 쓴다. 신뢰성은 Outbox(이중쓰기 제거) 와 멱등 소비자(중복 안전), Saga(분산 보상) 로 다지고, ES 를 쓰면 스냅샷/아카이빙으로 크기와 복구 시간을 관리한다.
범주 | 단점/문제 | 원인 | 영향 | 탐지·진단 | 예방 | 해결 기법/패턴 |
---|---|---|---|---|---|---|
복잡성/학습 | 구성요소·경로 증가 | 모델/DB/브로커 분리 | 개발·운영 비용↑ | 아키텍처 리뷰 | 단계적 도입·경계 명확화 | " 필요할 때만 " 채택 (가이드 준수) |
최종 일관성 | 읽기 지연/불일치 | 비동기 프로젝션 | UX 혼란 | 읽기 랙 모니터링 | 기대치·SLA 명시 | UI 지연 표시·보상 트랜잭션 (사가) |
인프라 비용 | 다중 DB·브로커 | 기술 스택 확대 | 장애 지점↑·비용↑ | 비용/장애 지표 | IaC·관리형 서비스 | 아키텍처 단순화·리소스 상한 |
테스트 복잡 | 경로별 검증 필요 | 흐름 분리 | 테스트 시간↑ | 계약/통합 테스트 | 표준 템플릿 | 리플레이/계약 테스트 체계화 |
메시징 실패/중복/순서 | 재시도·중복소비·오더링 | 분산·네트워크 특성 | 상태 불일치 | 중복률/오프셋 랙 | 파티셔닝·키 설계 | 멱등 소비자, 트랜잭션/EOS 활용 |
이중쓰기 | DB 쓰기↔이벤트 발행 사이 실패 | 분산 경계 | 데이터·이벤트 불일치 | 발행 실패율 | 로컬 트랜잭션 우선 | Transactional Outbox + CDC |
스키마 진화 | 이벤트/뷰 버전 불일치 | 장기 진화 | 역직렬화 오류 | 호환성 테스트 | 스키마 레지스트리 | 업/다운캐스터·버저닝 전략 |
ES 팽창/복구 지연 | 이벤트 무한 적재 | ES 도입 | 저장비용↑·리플레이 지연 | 스토리지/리플레이 시간 | 보존정책 | 스냅샷/아카이빙 |
- EOS(Exactly-once) 는 브로커·클라이언트 조합에서 " 범위 제한 " 으로 달성되며, 엔드투엔드에선 멱등성·Outbox 가 실효적이다.
핵심 리스크는 복잡성·EC·메시징 불확실성이고, 실무 기본값은 Outbox(이중쓰기 제거) + 멱등 소비자 (중복 안전) + Saga(보상) 다. ES 를 쓰면 스냅샷/아카이빙·스키마 버저닝이 생존 전략이다.
트레이드오프 관계 분석
CQRS 는 쓰기=정확성, 읽기=속도를 따로 최적화해 성능과 확장을 얻는 대신, 복잡도와 운영 부담을 치른다.
강한 즉시 일관성이 필요한 기능은 동기화·RPC 를, 대량 조회·피드·검색은 비동기·이벤트·폴리글랏으로 가져가고, 신뢰성은 Outbox, 분산 일관성은 Saga, UX 는 낙관적 UI로 메운다.
트레이드오프 축 | 선택지 | 장점 | 단점 | 적용 기준 | 관련 패턴/완화 |
---|---|---|---|---|---|
성능/확장성 ↔ 복잡도 | CRUD | 단순, 운영 쉬움 | 확장/튜닝 충돌 | 소규모, 즉시일관성 강함 | - |
CQRS | 독립 최적화·확장 | 구조·운영 복잡 | R≫W, 질의 다양 | 프로젝션/캐시 | |
일관성 ↔ 확장성 | 동기 반영 | 최신성 보장 | 지연·결합↑ | 결제·재고 등 | 트랜잭션/RPC |
비동기 반영 | 처리량·내결함성↑ | RYW 미보장 | 피드/리포트 | 낙관적 UI/세션 일관성 | |
신뢰성 ↔ 단순성 | 직접 발행 | 구현 간단 | 유실·중복 위험 | 프로토타입 | - |
Outbox | 원자성·재시도·멱등 | 릴레이 운영 부담 | 프로덕션 | 멱등 키/DLQ | |
일관성 ↔ 유연성 | 단일 트랜잭션 | ACID 롤백 | 경계 확장 불가 | 모놀리식 | - |
Saga | 분산 일관성·확장 | 보상·타임아웃 복잡 | 다 서비스 쓰기 | 오케스트레이션/코레오 | |
유연성 ↔ 운영 부담 | 단일 DB | 운영 단순 | 튜닝 충돌 | 단순 도메인 | - |
폴리글랏 | 목적별 최적화 | 거버넌스·비용↑ | 대규모/다양 질의 | 스키마 버전닝/백필 | |
결합도/지연 ↔ 복잡도 | 동기 RPC | 단순·예측가능 | 스파이크 취약 | 강한 상호의존 | Circuit Breaker |
이벤트 구독 | 느슨 결합·버퍼링 | 순서·중복 처리 | 비동기 허용 | 멱등성/리플레이 |
- 효과는 분명(읽기/쓰기 독립 최적화·확장) 하지만, 대가도 분명(복잡도·운영비용).
- 도입은 핵심 병목부터: 기본 CQRS → Outbox(신뢰성) → 프로젝션 SLA → Saga(분산 쓰기) 순으로 단계적 확대가 안전하다.
- UX 는 기술이 아닌 경험 설계로 보완 (낙관적 UI, 세션 일관성, 재시도·정정 플로우).
graph LR A[성능 향상] <--> B[복잡성 증가] C[확장성] <--> D[일관성 약화] E[유연성] <--> F[운영 부담] G[개발 자유도] <--> H[통합 복잡도]
구현 및 분류 (Implementation & Classification)
구현 기법 및 방법
- 단일 저장소로 " 코드는 분리하되 DB 는 하나 " 에서 시작 → 분리 저장소로 읽기/쓰기를 따로 확장 → 이벤트 소싱 결합으로 감사/재생까지.
- 동기화·정합성은 Outbox+CDC로 안전하게, 순서는 파티션 키로 지킨다.
- 단순 CRUD 면 굳이 복잡도를 올리지 않는다. 필요에 따라 성숙도를 올린다.
분류 | 정의 | 구성 요소 | 원리 | 목적 | 사용 상황 | 특징 |
---|---|---|---|---|---|---|
단일 저장소 CQRS | 한 DB 로 쓰기/읽기 로직·모델만 분리 | Command/Query Handler, Shared DB | 쓰기·읽기 모델 분리 (논리), 동일 저장소 | 구조적 분리 도입, 저비용 시작 | 초기 단계, 단일 서비스 | 단순·낮은 운영비, 이행 쉬움. 필요 시 분리 저장소로 확장 |
분리된 저장소 CQRS | 쓰기/읽기 DB 분리 | Write DB(RDB), Read DB(NoSQL/캐시), 동기화 | 쓰기 후 이벤트→읽기 저장소 갱신 | 독립 확장, 쿼리 성능 최적화 | 읽기 비율↑, 고성능 UI | 스키마 다변화·확장 용이, 최종 일관성 설계 필요 |
ES + CQRS | 이벤트 스토어가 진실원, 이벤트 재생으로 읽기 모델 | Event Store, Aggregate, Projection | 변경을 이벤트로 영속, 재생으로 상태/뷰 구성 | 감사/시간여행/재구성 | 규제/감사, 복잡한 도메인 | 이력 완전, 복잡도↑·스냅샷/재생 비용 고려 |
Outbox + CDC | DB 트랜잭션 안에 이벤트 기록, CDC 로 브로커 전송 | Outbox Table, CDC(Debezium 등), Broker | “DB 쓰기 = Outbox 기록 " 원자화 → CDC | 이중 쓰기/유실 방지 | Kafka·Debezium 환경 | 일관성↑, 지연 약간↑, 표준화 쉬움 |
파티션 키 설계 | 키별 파티션 고정으로 파티션 내 순서 보장 | Producer(Key), Topic/Partition | 동일 키→동일 파티션, 파티션 내 순서 | 상태 전이 순서 보장 | 주문/계정 등 엔티티별 이벤트 | 글로벌 순서 X, 키 선정이 관건 |
단일 DB 로 논리 분리부터 시작하고, 필요 시 저장소 분리로 확장한다. 이벤트 소싱은 감사/재생이 필요할 때 채택한다. 정합성은 Outbox+CDC 로, 순서는 파티션 키로 보장한다. 단순 도메인엔 과도 설계를 피한다.
단일 저장소 + Outbox
|
|
- 위 패턴이 “Outbox+CDC” 의 요지이다.
- DB 커밋=이벤트 기록이 원자적으로 보장되므로 이중 쓰기 문제가 사라진다.
- Debezium 이
outbox
테이블만 캡처해 브로커로 전달.
분리 저장소 + 파티션 키 + 프로젝션
|
|
- 같은 키 (
orderId
) 는 같은 파티션으로 가므로 순서가 보장된다. - 읽기 모델 (MongoDB) 은 비정규화된 뷰를 유지한다.
분류 기준에 따른 유형 구분
CQRS 를 레고 블록처럼 생각하자.
- 먼저 분리 수준 블록 (Type 1→2→3) 중 가능한 수준을 고른다.
- 다음 저장소 구성(단일↔이중↔폴리글랏) 을 고르고,
- 일관성 방식(동기↔비동기) 을 정한다.
- 필요하면 이벤트 블록(Outbox/ES) 까지 붙인다.
→ 이렇게 조합해 지금의 요구(트래픽, 팀, 규정) 에 맞춘다.
분류 축 | 유형 | 핵심 설명 | 언제 쓰나 | 트레이드오프/주의 |
---|---|---|---|---|
분리 수준 | Type 1: 클래스 분리 | 명령/조회 클래스를 분리 (같은 모델) | 초기 진입, 코드 충돌 완화 | 성능 이득 제한. |
Type 2: 모델 분리 | 명령/조회 도메인 모델 분리, 동일 DB | 읽기/쓰기 변경 이유가 뚜렷할 때 | 스키마/코드 중복 증가. | |
Type 3: 저장소 분리 | Write-Read 저장소 분리 (읽기는 프로젝션) | 대규모 조회/확장성 최우선 | 최종 일관성·동기화 파이프라인 필요. | |
저장소 구성 | 단일 DB CQRS | 같은 DB, 읽기용 뷰/테이블·인덱스 | 초기/운영 단순화 | 읽기 확장 한계. |
이중 DB CQRS | Write/Read DB 분리 | 읽기 폭증, 비용 최적화 | 복제/동기화 운영 필요. | |
폴리글랏 | 서로 다른 DB 기술 믹스 | 워크로드별 최적화 | 복잡한 운영·보안 거버넌스. | |
일관성 | 동기 | 요청 내 읽기 즉시 반영 | 강한 일관성·소규모 | 확장성 제한. |
비동기 (최종) | 이벤트로 프로젝션 갱신 | 대규모·고처리량 시스템 | 지연·재시도/멱등성 설계 필요. | |
이벤트 결합 | Outbox+CDC | 이중쓰기 방지, 신뢰성↑ | 다중 스토어 동기화 | CDC 인프라 운영. |
CQRS+ES | 이벤트 스토어 + 프로젝션 재생 | 감사/이력 필수 도메인 | 설계/운영 복잡성↑. | |
적용 범위 | 도메인 단위 | 특정 바운디드 컨텍스트만 적용 | 점진 도입 | 경계 설계 필요. |
시스템 전역 | 전 서비스로 확장 | 공통 거버넌스 용이 | 락인·조직 복잡성↑. |
핵심은 분리 수준을 적정선으로 잡고(Type 1→3), 그 위에 저장소 구성과 일관성 방식을 조합하는 것이다. 비동기 + 프로젝션이 대규모에 흔하고, Outbox/ES는 필요 시점에만 더해 운영 복잡성을 통제한다.
실무 적용 (Practical Application)
실습 예제 및 코드 구현
간단한 주문 시스템에서 CQRS 를 적용
시나리오: 간단한 주문 시스템에서 CQRS 를 적용해 쓰기/읽기 분리
시스템 구성:
- Command API (FastAPI)
- Query API (FastAPI)
- Kafka (이벤트 브로커)
- PostgreSQL (쓰기 DB)
- Redis (읽기 캐시)
시스템 구성 다이어그램:
graph TB C[Client] --> CA[Command API] CA --> CH[Command Handler] CH --> WDB[Write DB] CH --> EB[Event Bus] EB --> QH[Query Handler] QH --> RDB[Read DB] C --> QA[Query API] QA --> RDB
Workflow:
- Client 가 주문 생성 요청 (Command) 전송
- Command Handler 가 Write DB 저장 후 이벤트 발행
- Query Handler 가 이벤트 수신 후 Read DB 업데이트
- Client 가 Query API 로 조회 요청 → Read DB 응답
유무에 따른 차이점:
- 도입 전: 읽기와 쓰기 동일 DB, 쿼리 성능 저하
- 도입 후: 읽기 요청 성능 향상, 쓰기와 독립 확장 가능
구현 예시 (Python/FastAPI + Redis):
|
|
- Command API 는 쓰기와 이벤트 발행, Query API 는 읽기 전용 캐시 조회를 담당.
읽기 모델 (Projection) 을 Redis 와 OpenSearch 로 갱신
시나리오: " 주문 (Order) 생성→결제 승인→배송 시작 " 이벤트가 발생하면, 읽기 모델 (Projection) 을 Redis 와 OpenSearch 로 갱신하는 CQRS+Outbox 미니 시스템.
시스템 구성:
- FastAPI(Command/Query API)
- PostgreSQL(Write)
- Outbox 테이블
- Debezium CDC → Kafka(Event Bus), Projection Consumer(Python), Redis(OpenSearch 대체 가능)
시스템 구성 다이어그램:
graph TB C[Client] --> CA["Command API(FastAPI)"] CA --> WDB[(PostgreSQL)] CA --> OUTBOX[(outbox)] Deb[Debezium CDC] --> K[Kafka] K --> PC[Projection Consumer] PC --> R[Redis - Read Model] C --> QA["Query API(FastAPI)"] QA --> R
Workflow:
- Command API 가 쓰기 트랜잭션으로
orders
와outbox
에 기록 (이중 쓰기 방지) - Debezium 이 outbox 변화 감지 → Kafka 토픽 발행
- Projection Consumer 가 이벤트를 수신하여 Redis 의 읽기 모델 업데이트
- Query API 는 Redis 에서 빠른 조회 응답
유무에 따른 차이점:
- 도입 전: 동일 DB 에서 조인/인덱스 경쟁으로 조회 지연
- 도입 후: 읽기 경로 짧고 (캐시/키 조회), 쓰기 확장과 독립적 운영
구현 예시 (Python/SQL - 핵심 부분 주석 포함)
|
|
|
|
- Outbox+CDC 구성이 이중 쓰기 문제를 해결하는 정석 패턴.
|
|
- 파티션 키=주문 ID 로 설정하면 순서 보장(같은 파티션) → 상태 전이 안전.
|
|
게시글 등록 및 조회 서비스
시나리오: 게시글 등록 및 조회 서비스 (도메인별 CQRS 패턴 구현)
시스템 구성:
- Board Command Service (게시글 등록/수정/삭제)
- Board Query Service (게시글 조회)
- Message Broker (Kafka 등)
시스템 구성 다이어그램:
graph TB User[사용자] --> API API --> CommandService API --> QueryService CommandService --> WriteDB CommandService --> EventBus[Kafka/EventBus] EventBus --> QueryService QueryService --> ReadDB
Workflow:
- 사용자가 게시글 등록 요청 (Command)
- CommandService 가 처리 후 WriteDB 저장/이벤트 발행
- QueryService 가 이벤트를 감지, 게시글 정보 갱신
- 사용자는 실시간으로 게시글을 조회 (쿼리)
핵심 역할: 독립적 읽기/쓰기 구조로 실시간 데이터 일관성 보장
유무에 따른 차이점:
- 도입 전: DB 트랜잭션 충돌, 조회/쓰기 부하 관리 어려움
- 도입 후: 읽기와 쓰기 트래픽 분산, 데이터 확장 및 성능 개선
구현 예시 (Python)
|
|
전자상거래 주문 관리 시스템
시나리오: 전자상거래 주문 관리 시스템
시스템 구성:
- 명령 측: PostgreSQL (주문 데이터)
- 조회 측: Redis (주문 요약), Elasticsearch (검색)
- 이벤트 버스: Apache Kafka
시스템 구성 다이어그램:
graph TB Client[클라이언트] --> API[API 게이트웨이] API --> CMD[주문 명령 API] API --> QRY[주문 조회 API] CMD --> OrderService[주문 서비스] QRY --> QueryService[조회 서비스] OrderService --> PostgreSQL[(PostgreSQL)] OrderService --> Kafka[Apache Kafka] Kafka --> Projection[프로젝션 서비스] Projection --> Redis[(Redis)] Projection --> Elasticsearch[(Elasticsearch)] QueryService --> Redis QueryService --> Elasticsearch
Workflow:
- 클라이언트가 주문 생성 요청
- 주문 서비스가 비즈니스 로직 검증 후 PostgreSQL 에 저장
- 주문 생성 이벤트를 Kafka 로 발행
- 프로젝션 서비스가 이벤트를 수신하여 Redis 와 Elasticsearch 업데이트
- 조회 요청은 Redis/Elasticsearch 에서 처리
핵심 역할:
- CQRS 는 주문 생성 (명령) 과 주문 조회 (쿼리) 를 완전히 분리
- 각 저장소는 해당 워크로드에 최적화
- 이벤트를 통한 비동기 데이터 동기화
유무에 따른 차이점:
- 도입 전: 단일 데이터베이스에서 복잡한 조인 쿼리로 성능 저하
- 도입 후: 읽기는 비정규화된 뷰에서 빠른 조회, 쓰기는 정규화된 모델에서 일관성 보장
구현 예시:
|
|
CQRS + 이벤트 소싱 + 사가 패턴 통합 아키텍처 실전 예시
시나리오: 대형 이커머스에서 결제와 배송을 포함한 주문 처리 프로세스를 여러 서비스에 걸친 분산 트랜잭션으로 관리한다.
- 주문 생성, 결제 승인, 재고 차감, 배송 예약 등 각 서비스가 CQRS, 이벤트 소싱, 사가 패턴을 조합해 비동기 일관성을 보장한다.
시스템 구성:
- Order Command Service (주문 생성/변경)
- Payment Command Service (결제 처리)
- Inventory Command Service (재고 관리)
- Delivery Command Service (배송 예약)
- Event Bus (Kafka/RabbitMQ 등 이벤트 브로커)
- Saga Coordinator (트랜잭션 조정 및 보상 로직)
- 각 서비스별 Read DB (Elasticsearch/Redis 등)
graph LR User[사용자] --> APIGateway[API Gateway] APIGateway --> OrderCmdSvc[Order Command Service] OrderCmdSvc-->|OrderCreatedEvent|EventBus(Kafka) EventBus-->|OrderSagaStart|SagaCoordinator SagaCoordinator-->|PaymentCommand|PaymentCmdSvc[Payment Command Service] PaymentCmdSvc-->|PaymentApprovedEvent|EventBus SagaCoordinator-->|InventoryCommand|InventoryCmdSvc[Inventory Command Service] InventoryCmdSvc-->|InventoryReservedEvent|EventBus SagaCoordinator-->|DeliveryCommand|DeliveryCmdSvc[Delivery Command Service] DeliveryCmdSvc-->|DeliveryScheduledEvent|EventBus SagaCoordinator-->|OrderCompletedEvent|EventBus
Workflow:
- 사용자 주문 요청 (Command)
- Order Command 서비스가 주문 생성 후 OrderCreatedEvent(주문 생성 이벤트) 발행
- Saga Coordinator(사가 조정자) 가 이벤트 수신, 결제/재고/배송 등 하위 서비스 명령 분산
- 각 서비스가 처리 후 PaymentApprovedEvent(결제 승인 이벤트), InventoryReservedEvent(재고 예약 이벤트), DeliveryScheduledEvent(배송 예약 이벤트) 등을 이벤트 버스로 발행
- Saga Coordinator 가 이벤트 종합 후, 최종적으로 OrderCompletedEvent(주문 완료 이벤트) 발행 또는 실패 시 보상 트랜잭션 (Compensation) 수행
사가 패턴의 통합 처리 Event Flow 설명:
- OrderCreatedEvent가 발생하면
→ Saga Coordinator(사가 조정자) 가 ApprovePaymentCommand를 결제 서비스로 전송 - 결제 성공 (PaymentApprovedEvent)
→ Saga Coordinator 가 ReserveInventoryCommand를 재고 서비스로 전송 - 재고 성공 (InventoryReservedEvent)
→ Saga Coordinator 가 ScheduleDeliveryCommand를 배송 서비스로 전송 - 모든 단계가 Success 일 때
→ Saga Coordinator 가 OrderCompletedEvent를 발행
→ 모든 서비스 Read DB 에 최종 상태 반영 - 단계별 실패 시 (예: 결제 승인 실패)
→ Saga Coordinator 가 보상 명령 (PaymentCompensationCommand 등) 을 발행
→ 이전 단계 변경분을 Rollback, 전체 주문 취소 이벤트 (OrderCancelledEvent) 발행
핵심 역할:
- CQRS: 각 서비스에서 읽기/쓰기를 분리해 확장성 확보
- Event Sourcing (이벤트 소싱): 상태 변경을 이벤트로 저장, 장애 복구/이력 추적 가능
- Saga Pattern (사가 패턴): 다수 서비스 간 프로세스를 이벤트 기반으로 연결, 실패 시 보상 트랜잭션으로 데이터 일관성 유지
유무에 따른 차이점:
- 도입 전: 단일 트랜잭션 실패 시 전체 주문 취소, 장애 복원 어렵고 장애 시 타 서비스 일관성 깨짐
- 도입 후: 서비스별 상태·이력·보상 처리로 분산 트랜잭션 일관성 보장, 장애 복구 용이, 실시간 처리 성능 개선
구현 예시:
사가 코디네이터 보상 로직 예시 (Saga Coordinator Compensation Logic)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
# saga_coordinator.py class SagaCoordinator: """사가 패턴 중심 주문 트랜잭션 처리""" def __init__(self): self.state = {} def handle_order_created(self, event): # 결제 서비스 명령 전송 self.publish_command('payment', {'order_id': event.order_id, 'amount': event.amount}) def handle_payment_approved(self, event): # 재고 서비스 명령 전송 self.publish_command('inventory', {'order_id': event.order_id, 'items': event.items}) def handle_inventory_reserved(self, event): # 배송 서비스 명령 전송 self.publish_command('delivery', {'order_id': event.order_id, 'address': event.address}) def handle_delivery_scheduled(self, event): # 주문 완료 이벤트 발행 self.publish_event('order_completed', {'order_id': event.order_id}) def handle_failure(self, stage, event): # 실패 단계에 따라 보상(Compensation) 트랜잭션 실행 if stage == 'payment': # 결제 취소 로직 self.publish_command('payment_compensate', {'order_id': event.order_id}) elif stage == 'inventory': # 재고 복원 로직 self.publish_command('inventory_compensate', {'order_id': event.order_id}) elif stage == 'delivery': # 배송 예약 취소 로직 self.publish_command('delivery_compensate', {'order_id': event.order_id}) def publish_command(self, service, payload): # 각 서비스에 명령 메시지 전달 pass # 실제 구현은 Kafka/RabbitMQ 등 사용 def publish_event(self, event_type, payload): # 마무리 이벤트 송신 (Kafka 등) pass
결제 서비스 (Payment Command/Query)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# payment_command.py class ApprovePaymentCommand: """결제 승인 명령 - CQRS 명령 모델""" def __init__(self, order_id, amount): self.order_id = order_id self.amount = amount # payment_handler.py def handle_approve_payment(command: ApprovePaymentCommand): """결제 승인 처리 - 결제 DB 업데이트 + 이벤트 발행""" save_payment_to_db(command.order_id, command.amount) payment_event = {"order_id": command.order_id, "status": "APPROVED"} publish_event("payment_approved", payment_event) # payment_query.py class GetPaymentStatusQuery: """결제 상태 조회 쿼리 - CQRS 조회 모델""" def __init__(self, order_id): self.order_id = order_id def handle_get_payment_status(query: GetPaymentStatusQuery): """결제 상태 조회 실행""" return fetch_payment_status_from_read_db(query.order_id)
재고 서비스 (Inventory Command/Query)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# inventory_command.py class ReserveInventoryCommand: """재고 예약 명령 - CQRS 명령 모델""" def __init__(self, order_id, items): self.order_id = order_id self.items = items def handle_reserve_inventory(command: ReserveInventoryCommand): """재고 예약 처리 - 재고 DB 업데이트 + 이벤트 발행""" for item in command.items: update_inventory_count(item["product_id"], -item["quantity"]) inventory_event = {"order_id": command.order_id, "status": "RESERVED"} publish_event("inventory_reserved", inventory_event) # inventory_query.py class GetInventoryStatusQuery: """재고 상태 조회 쿼리""" def __init__(self, product_id): self.product_id = product_id def handle_get_inventory_status(query: GetInventoryStatusQuery): """조회 DB에서 재고 상태 반환""" return fetch_inventory_status_from_read_db(query.product_id)
배송 서비스 (Delivery Command/Query)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# delivery_command.py class ScheduleDeliveryCommand: """배송 예약 명령 모델""" def __init__(self, order_id, address): self.order_id = order_id self.address = address def handle_schedule_delivery(command: ScheduleDeliveryCommand): """배송 예약 처리 - 배송 DB 기록 + 이벤트 발행""" save_delivery_schedule_to_db(command.order_id, command.address) delivery_event = {"order_id": command.order_id, "status": "SCHEDULED"} publish_event("delivery_scheduled", delivery_event) # delivery_query.py class GetDeliveryStatusQuery: """배송 상태 조회 쿼리""" def __init__(self, order_id): self.order_id = order_id def handle_get_delivery_status(query: GetDeliveryStatusQuery): """조회 DB에서 배송 상태 반환""" return fetch_delivery_status_from_read_db(query.order_id)
이벤트 소비 및 상태 반영 예시 (Query Side)
1 2 3 4 5 6 7 8
# event_handler.py def handle_order_completed_event(event): """주문 완료 이벤트 수신 후 Read DB 갱신""" update_order_status_in_read_db(event['order_id'], status='COMPLETED') def handle_payment_approved_event(event): """결제 승인 이벤트 수신 후 Read DB 반영""" update_payment_status_in_read_db(event['order_id'], status='APPROVED')
- 각 핸들러마다 CQRS 분리, 이벤트 소비 후 Read DB(Materialized View, 비정규화 DB) 빠른 상태 반영
실제 도입 사례
CQRS 도입은 읽기와 쓰기의 요구가 크게 다를 때 힘을 발휘한다. 주문·거래처럼 규칙과 트랜잭션이 중요한 쓰기 경로는 도메인 규칙에 맞춰 설계하고, 상품 검색·대시보드처럼 속도가 중요한 읽기 경로는 비정규화된 물질화 뷰로 최적화한다. 이벤트를 흘려 프로젝션이 읽기 뷰를 갱신하므로, 조회는 빠르되 약간의 지연이 있을 수 있다. 처음엔 단순 분리로 시작하고, 필요해지면 브로커·ES를 더해 확장한다.
도메인/목적 | 전형 구성 (쓰기↔읽기) | 함께 쓰는 기술/패턴 | 핵심 효과 |
---|---|---|---|
이커머스 주문·검색 분리 | 주문/결제=RDB 트랜잭션 ↔ 카탈로그/검색=물질화 뷰 (Elasticsearch/캐시) | Kafka/Outbox, 프로젝션, 물질화 뷰 | 피크 트래픽에서도 조회 안정·지연 감소, 팀별 독립 확장. |
금융 원장/조회 | 거래=ES 로 이벤트 영속 ↔ 잔액/명세=리드뷰 | 파티셔닝·순서보장, 스냅샷, 사가 | 감사/복구 용이, 순서 일관성 유지, 규제 대응. |
실시간 분석/대시보드 | 인입 스트림 ↔ OLAP/검색용 여러 읽기 뷰 | Kafka, ClickHouse/OpenSearch, 다중 프로젝션 | 다양한 질의 패턴에 최적화된 뷰 제공, 확장 용이. |
소셜/콘텐츠 피드 | 포스팅/상호작용=쓰기 ↔ 피드/검색=읽기 | 캐시/검색 엔진, 물질화 뷰 | 짧은 응답시간, 읽기 부하 흡수. |
IoT/텔레메트리 | 시계열 쓰기 ↔ 집계/알람용 읽기 | 시계열 DB, 스트림 프로세싱, 물질화 뷰 | 실시간 모니터링·집계 분리, 비용 효율적 처리. |
서버리스 CQRS/ES | DynamoDB(Event store) ↔ OpenSearch/Redis(읽기) | Streams→Lambda 프로젝션, Outbox/CDC | 운영 자동화·탄력 확장, 관리 부담↓. |
단계적 도입 (레퍼런스) | 동일 DB 내 논리적 CQRS→필요 시 분리 | eShopOnContainers(간단 CQRS) | 리스크 낮은 시작·점진 확장. |
- 읽기/쓰기의 이질성이 큰 도메인 (주문 vs 검색, 거래 vs 잔액) 에선 CQRS 가 성능·확장·조직 분리를 동시에 준다.
- 프로젝션 + 물질화 뷰로 읽기를 빠르게 만들고, **ES(옵션)**를 결합하면 감사/복구가 쉬워진다.
- 현실적인 접근은 단순 CQRS→저장소 분리→브로커/ES 순으로 점진 도입이다.
실제 도입 사례의 코드 구현
Netflix 스타일의 개인화 추천 시스템
시나리오: Netflix 스타일의 개인화 추천 시스템
시스템 구성:
- 명령 측: 사용자 행동 데이터 수집 (Cassandra)
- 조회 측: 추천 결과 제공 (Redis, MongoDB)
- 이벤트 스트리밍: Apache Kafka
- 머신러닝 파이프라인: Apache Spark
시스템 구성 다이어그램:
graph TB User[사용자] --> API[추천 API] subgraph "명령 측면 (행동 수집)" API --> TrackingAPI[행동 추적 API] TrackingAPI --> Kafka[Apache Kafka] Kafka --> Cassandra[(Cassandra)] end subgraph "ML 파이프라인" Cassandra --> Spark[Apache Spark] Spark --> MLModel[ML 모델] end subgraph "조회 측면 (추천 제공)" API --> RecommendAPI[추천 조회 API] RecommendAPI --> Redis[(Redis)] RecommendAPI --> MongoDB[(MongoDB)] end MLModel --> Redis MLModel --> MongoDB
Workflow:
- 사용자 행동 데이터 실시간 수집 및 Kafka 로 스트리밍
- Cassandra 에 원시 행동 데이터 저장
- Spark 가 배치로 데이터를 처리하여 ML 모델 훈련
- 개인화된 추천 결과를 Redis/MongoDB 에 저장
- 사용자 요청 시 사전 계산된 추천 결과 즉시 반환
핵심 역할:
- CQRS 를 통해 데이터 수집과 추천 제공을 완전히 분리
- 실시간 수집과 배치 처리의 하이브리드 아키텍처
- 개인화 추천의 낮은 레이턴시 보장
유무에 따른 차이점:
- 도입 전: 사용자 요청 시 실시간 추천 계산으로 높은 지연시간
- 도입 후: 사전 계산된 추천 결과로 밀리초 단위 응답
구현 예시:
|
|
Serverless CQRS + Event Sourcing
시나리오: DynamoDB(Event Store) 에 이벤트를 Append 하고, DynamoDB Streams → AWS Lambda 로 OpenSearch(또는 Redis) Projection 을 생성.
효과: 서버 관리 최소화, 초당 이벤트 폭증에도 원활한 수평 확장, 비용 효율성.
시스템 구성:
- Write(API Gateway + Lambda):
PUT /orders/{id}
→ DynamoDBevents
테이블에 Append(타입/버전/페이로드) - Stream Processor(Lambda): Streams 레코드 → OpenSearch/Redis 업데이트
- Read(API Gateway + Lambda): Projection 에서 조회
시스템 구성 다이어그램:
graph TB C[Client] --> APIGW[API Gateway] APIGW --> W["Lambda(Command)"] W --> DDB[(DynamoDB Event Store)] DDB --> DS[DynamoDB Streams] DS --> P["Lambda(Projection)"] P --> OS[OpenSearch / Redis] C --> QAPIGW["API Gateway(Query)"] QAPIGW --> QR["Lambda(Query)"] QR --> OS
Workflow:
- Command Lambda 가
OrderCreated
이벤트를 이벤트 스토어 (DDB) 에 Append - Streams 가 이벤트를 트리거 → Projection Lambda 가 OpenSearch/Redis 업데이트
- Query Lambda 가 Projection 에서 조회 반환
유무에 따른 차이점:
- 전: 단일 RDB 조인 기반 조회로 지연/경쟁
- 후: Write/Read 경로 분리, 대량 조회 시 선형 확장
구현 예시 (Python Lambda 핵심)
Command Lambda
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Command Lambda: 이벤트 소싱 Append (CQRS 쓰기 측) import os, json, time, uuid, boto3 ddb = boto3.resource('dynamodb').Table(os.environ['EVENTS_TABLE']) def handler(event, context): order_id = event["pathParameters"]["id"] body = json.loads(event["body"]) evt = { "aggregate_id": order_id, "version": int(time.time()*1000), # 간단 버전(프로덕션은 정합 설계 필요) "type": "OrderCreated", "payload": body, "occurred_at": int(time.time()) } ddb.put_item(Item=evt) # append-only return {"statusCode": 200, "body": json.dumps({"ok": True, "id": order_id})}
Projection Lambda
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# Projection Lambda: Streams → OpenSearch/Redis 업데이트 (CQRS 읽기 측) import os, json, boto3, redis r = redis.Redis(host=os.environ['REDIS_HOST'], port=6379, decode_responses=True) def handler(event, context): for rec in event["Records"]: if rec["eventName"] in ("INSERT","MODIFY"): new = rec["dynamodb"]["NewImage"] agg = new["aggregate_id"]["S"] et = new["type"]["S"] data = json.loads(new["payload"]["S"]) if et == "OrderCreated": r.hset(f"order:{agg}", mapping={"status":"CREATED","total":data["total_amount"]}) elif et == "OrderPaid": r.hset(f"order:{agg}", mapping={"status":"PAID"}) elif et == "OrderShipped": r.hset(f"order:{agg}", mapping={"status":"SHIPPED"})
AWS 공식/레퍼런스 아키텍처에서 DynamoDB Streams + Lambda로 CQRS/Event Sourcing 구현을 권장.
대형 전자상거래 플랫폼에서 " 주문 (Order)” 과 " 재고 (Inventory)” 관리
시나리오: 대형 전자상거래 플랫폼에서 " 주문 (Order)” 과 " 재고 (Inventory)" 를 CQRS + Event Sourcing 방식으로 관리.
- 사용자가 주문을 생성하면 Command Service가 처리하고, 이벤트를 발행한다.
- Query Service는 이벤트를 기반으로 읽기용 DB(Materialized View) 를 업데이트하여, 실시간 재고 조회 성능을 높인다.
시스템 구성:
- Order Command Service: 주문 생성/취소 처리
- Order Query Service: 주문 내역 조회
- Inventory Command Service: 재고 조정 처리
- Inventory Query Service: 재고 현황 조회
- Event Store (Kafka, RabbitMQ): 이벤트 저장 및 전송
- Read DB (Elasticsearch, Redis): 빠른 조회 모델
시스템 구성 다이어그램:
graph TB User[사용자] --> API[API Gateway] API --> OrderCmdSvc[Order Command Service] API --> OrderQrySvc[Order Query Service] OrderCmdSvc --> WriteDB[(Order WriteDB)] OrderCmdSvc --> EventBus["Event Bus (Kafka)"] EventBus --> OrderQrySvc OrderQrySvc --> ReadDB[(Order ReadDB)]
Workflow:
- 사용자가 주문 생성 요청 (Command)
- Order Command Handler 가 Write Model 에 저장
- 주문 생성 이벤트 (OrderCreated) 를 Event Store 에 발행
- Order Query Handler 가 이벤트를 소비하여 Read Model 갱신
- 사용자가 주문 내역 또는 재고를 실시간 조회 (Query)
핵심 역할:
- CQRS: 읽기/쓰기 모델 분리로 대규모 요청 처리 최적화
- Event Sourcing: 주문 변경 이력 추적 및 재연 가능
- Materialized View: 실시간 조회 성능 향상
유무에 따른 차이점:
- 도입 전: 단일 DB 에서 CRUD 처리, 높은 조회 부하 발생 시 쓰기 지연
- 도입 후: 읽기 트래픽은 Read Model 이 처리, 쓰기 성능 안정화, 이벤트 기반 확장 용이
구현 예시 (Python + Kafka + Redis 등):
|
|
전자상거래 플랫폼
시스템 구성:
- 명령 측: 주문 생성, 재고 업데이트, 결제 처리
- 조회 측: 상품 검색, 주문 이력, 재고 현황
시스템 구성도:
graph TB subgraph "사용자 인터페이스" A[웹 애플리케이션] B[모바일 앱] end subgraph "명령 측 (Order Service)" C[Order API] D[Order Handler] E[Order DB] end subgraph "조회 측 (Catalog Service)" F[Catalog API] G[Search Handler] H[Read DB] end subgraph "인프라" I[Event Bus] J[Cache Layer] end A --> C B --> C A --> F B --> F C --> D D --> E D --> I I --> G G --> H G --> J F --> G
Workflow:
- 사용자가 주문 생성 요청
- Order Handler 가 비즈니스 로직 검증
- Order DB 에 주문 정보 저장
- OrderCreated 이벤트 발행
- Catalog Service 가 이벤트 수신
- Read DB 업데이트 (재고 감소)
- 캐시 갱신
CQRS 역할:
- 명령 측: 복잡한 주문 로직과 재고 관리
- 조회 측: 빠른 상품 검색과 추천
CQRS 유무에 따른 차이점:
- CQRS 적용 전:
- 단일 데이터베이스에서 모든 작업 처리
- 복잡한 조인 쿼리로 성능 저하
- 읽기/쓰기 경합으로 인한 대기 시간 증가
- CQRS 적용 후:
- 읽기 최적화된 별도 데이터베이스
- 검색 성능 10 배 향상
- 주문 처리와 독립적인 카탈로그 조회
구현 예시:
|
|
운영 및 최적화 (Operations & Optimization)
보안 및 거버넌스
CQRS 보안은 **" 두 개의 길 + 울타리 “**로 보면 쉽다.
- 쓰기 길 (명령) 은 높은 펜스(강한 권한·검증), 읽기 길 (조회) 은 얕은 펜스(최소권한·고속).
- 길 사이의 교차로 (이벤트 브로커) 는 암호화·인증·ACL로 통제.
- 표지판 (스키마·프로젝션) 은 버전·호환성 규칙으로 바뀌어도 안전.
- 모든 움직임은 감사 로그로 남기고, 규정(NIST/GDPR) 에 맞춰 보관한다.
영역 | 핵심 위험 | 보안 요구/정책 | 구현 방안 | 모니터링/감사 |
---|---|---|---|---|
인증/인가 (API) | 권한 상승, 토큰 오용 | Command/Query 권한 분리, 최소권한 (RBAC/ABAC) | OAuth 2.0 BCP(권장 플로우, PKCE/PAR), JWT 서명·클레임 검증(exp/iss/aud 등) | 접근 로그, 토큰 실패율/이상 패턴 탐지 |
통신 보안 | 도청·중간자 공격 | 외부 TLS 1.3, 내부 mTLS 강제 | L7 게이트웨이 TLS1.3, 서비스 메시 인증정책 (mTLS) | 인증서 만료/회전, 암호 스위트 준수 |
이벤트/브로커 | 위·변조, 무단 구독 | 브로커 인증·인가, 토픽 분리 | Kafka SASL_SSL + ACL, 프로듀서/컨슈머 자격 분리 | 토픽 접근 실패·비정상 소비량 경보 |
동기화/일관성 | 이중쓰기, 유실 | Outbox+CDC, 멱등 소비 | 트랜잭셔널 아웃박스 + Debezium, 재처리·DLQ | 오프셋 지연·누락 이벤트 대시보드 |
데이터 보호 | PII 누출 | 전송·저장 암호화, 키관리·회전 | 필드 레벨 암호화, 키 볼트/KMS, 로테이션 | 키 만료·교체 감사, 민감필드 접근 추적 |
스키마 거버넌스 | 소비자 장애 | 호환성 모드(Backward 등) 규정 | Confluent Schema Registry + CI 검증 | 스키마 변경 이력·호환성 실패 알림 |
규정 준수 | 책임소재 불분명 | 최소권한 (AC-6), 감사 (AU-2) 매핑 | 역할·권한 증적, 이벤트/관리행위 로그 보존 | 정기 리뷰·침해 대응 (72h 규정 등 도메인별) |
제품·UX | 최종 일관성 혼동 | 지연 알림·리프레시 UX | " 처리 중/곧 반영 " 피드백, 수동 새로고침 | 읽기 - 쓰기 지연 (SLA) 관측 |
핵심은 경계 분리와 표준화다. 쓰기·읽기·브로커 각각에 맞춤 보호대(권한·TLS/mTLS·ACL) 를 치고, Outbox+CDC와 스키마 호환성으로 데이터 흐름을 안정화한다. 마지막으로 최소권한·감사를 규정과 매핑해 신뢰를 완성한다.
모니터링 및 관측성
CQRS 에서는 쓰기와 읽기가 따로 움직여 읽기가 조금 늦게 반영될 수 있다.
그래서 두 가지 축을 꼭 본다:
- 이벤트 파이프라인이 잘 흐르는지 (컨슈머 랙·실패율·DLQ),
- 읽기 모델이 제때 갱신되는지(프로젝션 랙).
그리고 OpenTelemetry 로 요청→이벤트→프로젝션→조회를 하나의 트레이스로 묶으면, 어디서 느려졌는지 바로 찾을 수 있다.
영역 | 핵심 지표 | 의미/목적 | 수집원/도구 | 경보 아이디어 |
---|---|---|---|---|
이벤트 파이프라인 | Consumer Lag | 소비 지연 (오프셋 기준) 로 처리 적체 파악 | Kafka JMX/Exporter, Confluent/MSK 모니터링 | 랙의 절대값·증가율 경보, 파티션별 임계치. |
처리율/실패율 | 초당 이벤트 처리·실패 추세 | Broker/Consumer metrics | 실패율 급증·처리율 급감 시 경보 | |
DLQ 크기/증가율 | 재처리 필요 이벤트 적체 | 브로커/파이프라인 메트릭 | DLQ 증가율·체류 시간 경보 | |
파티션 스큐 | 일부 파티션 과부하 | Exporter·대시보드 | 스큐 비율 초과 시 재파티셔닝 | |
저장소·동기화 | 프로젝션 랙 (seconds) | 이벤트→읽기모델 반영 지연 (EC 가시화) | 프로젝션 워커 메트릭/로그 | 랙 P95/P99 경보 (업무 SLA 기준). |
Write/Read DB 지연·에러율 | 각각의 병목/장애 조기 탐지 | DB/드라이버 메트릭 | 지연·에러율 임계치 | |
추적/로깅 | 엔드투엔드 트레이스 | Command→Event→Projection→Query 연결 | OpenTelemetry, W3C Trace Context | 스팬 지연 초과·오류 스팬 비율 경보. |
신뢰성 보강 | Outbox 배출 지연 | 아웃박스→브로커 전파 지연 | CDC/퍼블리셔 메트릭 | 배출 지연·미전파 건수 경보. |
스키마 검증 실패율 | 이벤트 스키마 불일치 탐지 | 레지스트리/소비자 로깅 | 실패율 급증 경보 |
관측의 축은 이벤트 흐름 (랙·실패·DLQ) 과 읽기 반영 (프로젝션 랙), 그리고 엔드투엔드 추적이다. 이 세 축만 꾸준히 보면 CQRS 의 지연 일관성과 병목을 체계적으로 다 잡을 수 있다. 지표는 Kafka/Exporter·DB 메트릭·OpenTelemetry 로 표준화해 대시보드와 경보에 연결하라.
실무 적용 고려사항 및 주의점
카테고리 | 고려사항 | 위험/주의 | 권장사항 | 핵심 지표 |
---|---|---|---|---|
설계/도입 | Bounded Context 기준 점진 도입 | 전면 전환 실패 리스크 | 파일럿→확장, 간단 도메인은 CRUD 유지 | 전환 범위 대비 결함률 |
설계/도입 | 도메인 복잡성 평가 | 과도한 아키텍처 | 읽기≫쓰기·질의 다양에서만 적용 | R:W, 질의 수/복잡도 |
일관성/동기화 | 최종 일관성으로 인한 UX | RYW 미보장 | 낙관적 UI/세션 일관성, 중요 화면 동기화 | 뷰 갱신 지연 p95 |
일관성/동기화 | Projection/뷰 설계 | 중복·정합성 관리 | 이벤트 기반 뷰 갱신/리플레이 | 프로젝션 지연/실패율 |
신뢰성/내결함성 | 듀얼라이트 (유실/중복) | DB↔브로커 불일치 | Transactional Outbox + 멱등 | 미전송/중복 이벤트 수 |
신뢰성/내결함성 | 분산 트랜잭션 | 전역 롤백 부재 | Saga(보상/오케스트레이션) | 보상률/타임아웃 |
이벤트/순서/버전 | 순서 보장 | 파티션 확장 시 순서 붕괴 | 파티션 키로 엔티티 고정, 단일 파티션 선택적 | out-of-order 비율 |
이벤트/순서/버전 | 이벤트/스키마 버전 | 리플레이·호환성 오류 | 이벤트/스키마 버전 전략·백필 플랜 | 버전 충돌 건수 |
인프라/운영/관측 | 메시징/모니터링 인프라 | 원인 미상 지연 | Lag, 처리율, 오류율, DLQ | Lag, DLQ 적재량 |
팀/테스트/거버넌스 | 학습 곡선/테스트 복잡 | 생산성 초기 저하 | 교육·런북·계약/통합 테스트 | 실패 시 재처리 성공률 |
보안/감사 | R/W API 분리 | 권한·감사 누락 | 최소권한, 감사로그, 추적 ID | 감사 이벤트 결손률 |
운영 자동화 | Outbox/CDC 운영 | 릴레이·청소 비용 | Debezium Outbox Router/Quarkus | 릴레이 지연/적체 |
- 효과는 분명: 읽기/쓰기 분리로 성능·확장·보안이 좋아진다.
- 대가도 분명: 복잡도·운영비용이 오른다. 그래서 점진도입과 관측 지표가 필수다.
- 신뢰성·순서는 Outbox·멱등·DLQ·파티션 키로, 분산 쓰기는 Saga로 보강한다.
성능 최적화 전략 및 고려사항
핵심은 읽기와 쓰기의 길을 분리하고, 그 사이를 이벤트로 느슨하게 연결하면서 성능·확장·정합성을 균형 있게 잡는 것.
- 성능은 읽기=캐시/프로젝션, 쓰기는 비동기/배치/파티션으로 끌어올린다.
- 정합성은 Outbox+CDC로 보장하고, 파티션 키로 순서를 지킨다.
- 운영은 관측성과 스키마 진화가 성패를 가른다. 작은 구성으로 시작해 점진적 확대가 정석이다.
카테고리 | 전략/고려사항 | 정의·원리 | 구성 요소 | 목적 | 사용 상황 | 특징/주의 |
---|---|---|---|---|---|---|
읽기 최적화 | 캐시/프로젝션/머뷰 | 쓰기와 분리된 읽기 전용 뷰로 핫패스 최소화 | Redis/Elastic/읽기 DB, Projection | 지연↓·처리량↑ | 조회 비율↑ 서비스 | 캐시 무효화 정책 필수. |
쓰기·확장 | 비동기/배치/파티셔닝 | 개별 트랜잭션 비용↓, 병렬 처리↑ | 배치 프로듀서, 파티션 키 | 처리량↑·지연 균형 | 고트래픽 쓰기 | 배치 크기·재시도·백프레셔 튜닝 |
이벤트·브로커·순서 | 키=파티션 고정, 멱등성, DLQ | 파티션 내 순서 보장·중복 허용 처리 | Kafka/Rabbit, Idempotent Consumer | 순서·신뢰성 | 엔티티별 이벤트 흐름 | 글로벌 순서 불가, 키 설계가 전부. |
정합성·동기화 | Outbox+CDC | “DB 커밋=이벤트 기록 " 원자화→CDC 전달 | Outbox 테이블, Debezium SMT, Broker | 이중쓰기 제거 | 분산 동기화 | 약간의 지연, 표준화 쉬움. |
스냅샷 | 주기 스냅샷/압축 | 이벤트 재생 비용 절감 | 스냅샷 스토어 | 리플레이 비용↓ | ES 결합 | 용량/주기 설계 필요. |
스키마 진화 | Backward-compat | 소비자/프로듀서 호환성 유지 | 스키마 레지스트리/버전 핸들러 | 배포 리스크↓ | 장수 서비스 | Producer-First 금지, Consumer-First 권장. |
운영·관측성 | 분산 추적/지연 모니터링 | 명령→이벤트→프로젝션→조회 체인 추적 | OTel/APM/로그 | 이상 탐지·SLA | 다팀 운영 | 상관 ID/샘플링·알람 룰 필수 |
보안·거버넌스 | RBAC/암호화/격리 | 원천·전송·저장 보안 | IAM/KMS/VPC | 규제 충족 | 멀티테넌시·규제 도메인 | 성능·비용 트레이드오프 |
비용·용량 | TTL/아카이빙/압축 | 오래된 데이터 비용↓ | DLQ/아카이브/Topic 관리 | 비용 최적화 | 대용량 로그 | 회수·보존 정책 명문화 |
작게 시작해 (단일 DB+ 프로젝션) 병목을 찾고, 필요 시 저장소 분리·배치·파티셔닝으로 확장한다.
정합성은 Outbox+CDC 로, 순서는 파티션 키로 지킨다.
스키마는 뒤호환을 우선하며, 관측성과 비용·보안을 동일한 1 급 요구사항으로 다룬다.
고급 주제 (Advanced Topics)
도전 과제
핵심은 명령 (쓰기) 모델과 조회 (읽기) 모델의 분리로 인해 발생하는 추가적인 복잡성을 어떻게 다룰 것인가이다.
가장 큰 기술적 문제는 ’ 데이터 일관성 ‘ 이다.
쓰기 모델의 데이터가 읽기 모델로 즉시 동기화되지 않고, 비동기 이벤트에 의존하기 때문에 일시적인 데이터 불일치가 발생할 수 있다. 이는 사용자가 최신 정보를 보지 못하는 결과를 초래할 수 있다.
또한, 아키텍처가 복잡해지면서 개발 및 운영 부담이 커진다. 두 개의 모델을 관리해야 하고, 그 사이의 비동기 이벤트를 추적하고 모니터링해야 한다. 이 과정에서 데이터 중복이나 이벤트 스키마 관리에 대한 문제도 발생한다.
이러한 기술적 문제들은 결국 팀 구조와 협업 방식에 영향을 준다. 명령 모델과 조회 모델을 담당하는 팀을 분리하거나, 기존의 CRUD(Create, Read, Update, Delete) 기반 시스템에서 CQRS 로 전환하는 과정에서 기술 부채와 마이그레이션이라는 난관에 부딪히게 된다. CQRS 도입은 단순히 기술 스택을 바꾸는 것이 아니라, 조직의 운영 방식과도 깊은 관련이 있는 중대한 변화이다.
카테고리 | 도전 과제 | 원인 | 영향 | 해결 방안 |
---|---|---|---|---|
기술적 복잡성 및 일관성 관리 | 최종 일관성 (Eventual Consistency) | 명령/조회 모델의 분리 및 비동기 이벤트 통신 | 사용자 경험 저하, 일시적 데이터 불일치 | 사용자 인터페이스 (UI) 에서 지연을 명시하거나, 동기 업데이트 옵션 설계, 사용자에게 피드백 제공 |
분산 트랜잭션 관리 | 여러 데이터베이스 간 ACID 트랜잭션 보장 어려움 | 데이터 무결성 위험, 복잡한 비즈니스 로직 처리 어려움 | Saga 패턴, 2PC(Two-Phase Commit), 트랜잭션 아웃박스 (Transactional Outbox) 패턴 | |
아키텍처 복잡성 | 읽기/쓰기 모델의 분리 및 추가 시스템 (메시징 시스템 등) 도입 | 개발, 운영, 테스트 난이도 증가, 팀 온보딩 비용 상승 | 마이크로서비스 경계 명확화, 점진적 도입 (Incremental adoption), 자동화된 테스트 및 배포 파이프라인 구축 | |
데이터 및 이벤트 관리 | 데이터 중복 및 유지보수 | 읽기 모델의 비정규화된 데이터 저장 | 스토리지 비용 증가, 동기화 관리 비용 상승 | 스냅샷 (Snapshot), 변경 데이터 캡처 (CDC, Change Data Capture) 활용, 효율적인 동기화 전략 수립 |
이벤트 순서, 중복, 유실 | 분산 시스템의 비결정성, 네트워크 지연, 재시도 로직 | 데이터 불일치, 비즈니스 로직 오류 | 메시지 브로커 파티셔닝 (Partitioning), 멱등성 (Idempotency) 프로젝션, 트랜잭션 아웃박스 (Transactional Outbox) 패턴 | |
이벤트 스키마 진화 | 비즈니스 요구사항 변경에 따른 이벤트 구조 변경 | 기존 프로젝션 (Projection) 과의 호환성 문제, 기술 부채 | 스키마 레지스트리 (Schema Registry) 도입, 이벤트 버전 관리 및 호환성 유지 전략 수립 | |
조직 및 운영 부담 | 운영 및 모니터링 복잡성 | 분산된 컴포넌트들 (DB, 메시징, 서비스) | 장애 진단 및 추적 어려움, 운영 비용 증가 | 통합 로깅, 분산 추적 (Distributed Tracing), 모니터링 대시보드 (Grafana, Kibana 등) 구축 |
팀 구조 및 협업 | 명령/조회 책임 분리에 따른 팀 역할 재정의 | 개발 속도 저하, 의존성 관리 문제 | Conway’s Law(콘웨이의 법칙) 를 고려한 팀 구조 재편, API(Application Programming Interface) 우선 설계, 계약 테스트 (Contract Testing) 도입 | |
레거시 시스템 통합 | 기존 CRUD(Create, Read, Update, Delete) 기반 시스템과의 호환성 | 마이그레이션 복잡성, 기술 부채 | 점진적 마이그레이션, 스트랭글러 피그 (Strangler Fig) 패턴 적용 |
CQRS 도입의 도전 과제는 크게 세 가지 영역으로 나뉜다.
기술적 복잡성 및 일관성 관리이다.
분리된 모델과 비동기 통신 때문에 발생하는 최종 일관성 문제는 사용자 경험에 직접적인 영향을 준다. 이를 해결하기 위해 Saga 패턴 같은 분산 트랜잭션 관리 기법을 사용하거나, 아키텍처의 복잡성을 관리하는 전략이 필요하다.데이터 및 이벤트 관리이다.
읽기 모델의 데이터 중복과 이벤트의 순서, 중복, 유실 문제는 시스템의 신뢰성을 떨어뜨릴 수 있다. 멱등성을 보장하는 프로젝션 (Projection) 이나 트랜잭션 아웃박스 (Transactional Outbox) 패턴을 통해 이를 방지해야 한다. 또한, 비즈니스 변경에 따라 발생하는 이벤트 스키마 진화 문제도 중요한 관리 포인트이다.조직 및 운영 부담이다.
CQRS 는 단순히 기술적인 선택이 아니라, 팀 구조와 운영 방식을 재정립하는 일이다. 복잡해진 시스템을 효율적으로 모니터링하고, 기존 레거시 시스템과의 통합을 위한 명확한 **마이그레이션 전략 (예: Strangler Fig 패턴)**이 필요하다. 이러한 도전 과제들을 극복하려면 기술적 솔루션과 함께 조직의 변화를 유도하는 노력이 병행되어야 한다.
생태계 및 관련 기술
CQRS(Command Query Responsibility Segregation) 는 단독으로 사용되기보다는 여러 기술과 결합하여 그 진가를 발휘하는 패턴이다.
이 아키텍처의 핵심은 비동기 통신이다.
명령 (Command) 이 처리된 후, 그 결과를 이벤트 브로커인 Kafka(카프카) 나 RabbitMQ(래빗엠큐) 를 통해 읽기 모델로 전달하여 데이터를 업데이트한다. 이 과정에서 이벤트 소싱 (Event Sourcing) 과 같은 패턴을 함께 사용하면 모든 상태 변화 이력을 보존할 수 있어 시스템의 신뢰성을 높일 수 있다.
또한, CQRS 에서는 읽기 모델의 성능을 극대화하기 위해 목적에 맞는 다양한 데이터 저장소를 활용한다.
예를 들어, 복잡한 검색 기능은 **Elasticsearch(엘라스틱서치)**를 사용하고, 빠른 응답이 필요한 조회는 **Redis(레디스)**와 같은 인메모리 캐시를 활용하는 방식이다. 이러한 분산된 시스템을 효과적으로 관리하기 위해 **OpenTelemetry(오픈텔레메트리)**와 같은 관측성 (Observability) 도구는 필수적이며, **CloudEvents(클라우드이벤트)**와 같은 표준은 시스템 간의 원활한 통신을 보장한다.
카테고리 | 기술/표준 | 역할 |
---|---|---|
메시징 및 이벤트 처리 | Apache Kafka/RabbitMQ | 명령 처리 결과를 읽기 모델에 비동기적으로 전달하는 이벤트 브로커 |
Event Sourcing | 모든 상태 변화를 이벤트로 저장하여 감사 (Audit) 및 과거 상태 재구성 가능 | |
CloudEvents | 이벤트에 대한 메타데이터 (Metadata) 를 표준화하여 상호 운용성 보장 | |
데이터 저장소 및 관리 | PostgreSQL/MongoDB | 쓰기 모델의 원본 데이터 저장소로 사용 (이벤트 스토어 역할 가능) |
Elasticsearch/OpenSearch | 복잡한 검색 및 집계 쿼리를 위한 읽기 모델 저장소 | |
Redis | 빠른 응답이 필요한 읽기 모델을 위한 인메모리 캐시 | |
Debezium CDC/Transactional Outbox | 이중 쓰기 (Dual Write) 문제를 해결하고 DB 변경 내용을 이벤트로 캡처 | |
관측성 및 표준 | OpenTelemetry(OTel) | 분산 시스템 전반의 로그 (Log), 메트릭 (Metric), 추적 (Trace) 을 통합 관리하는 관측성 표준 |
JSON Schema/Avro | 이벤트 데이터의 구조와 스키마 (Schema) 를 정의하여 버전 호환성을 관리 | |
구현 및 프레임워크 | Axon Framework | CQRS 와 이벤트 소싱 구현을 돕는 전용 프레임워크 |
Azure/AWS Patterns | 클라우드 환경에서 CQRS 를 설계하고 구현하는 가이드라인 제공 |
CQRS 는 단일 기술이 아닌 여러 기술의 조합으로 완성되는 아키텍처 패턴이다.
가장 핵심적인 요소는 메시징 시스템으로, Kafka(카프카) 와 같은 이벤트 브로커가 비동기 통신의 중추 역할을 한다. 쓰기 모델의 데이터는 일반적으로 관계형 또는 NoSQL(NoSQL) 데이터베이스에 저장되고, 읽기 모델은 사용자 조회에 최적화된 Elasticsearch(엘라스틱서치) 와 같은 검색 엔진이나 Redis(레디스) 와 같은 캐시를 활용하여 성능을 높인다. 이러한 분산 환경을 효율적으로 운영하기 위해서는 OpenTelemetry(오픈텔레메트리) 같은 표준을 통해 시스템의 상태를 **관측 (Observability)**하고, 스키마 관리 도구를 사용해 데이터 호환성을 유지하는 것이 중요하다.
최신 기술 트렌드와 미래 방향
CQRS(Command Query Responsibility Segregation) 는 이제 단순한 아키텍처 패턴을 넘어, 최신 기술 트렌드와 결합하며 진화하고 있다. 가장 두드러진 변화는 클라우드 네이티브 환경으로의 전환이다. 개발자들은 더 이상 서버를 직접 관리하지 않는 서버리스 (Serverless) 아키텍처를 선호하며, 이를 통해 운영 부담을 줄이고 개발에 집중한다. 이와 함께, AI(인공지능)/ML(머신러닝) 기술이 도입되어 쿼리 패턴을 분석하고 읽기 모델을 자동으로 최적화하는 등 시스템이 스스로 진화하는 방향으로 나아가고 있다.
또한, 사용자에게 더 빠른 서비스를 제공하기 위해 **에지 컴퓨팅 (Edge Computing)**이 중요한 트렌드로 부상하고 있다. CDN(Content Delivery Network) 에지에 읽기 모델을 캐싱 (Caching) 하여 데이터 접근 지연 시간을 줄이는 방식이 대표적이다. 마지막으로, 복잡해진 CQRS 시스템을 효율적으로 운영하기 위해 **DevOps(데브옵스)**와 **GitOps(깃옵스)**를 기반으로 한 자동화된 배포 및 모니터링이 필수가 되고 있다.
카테고리 | 기술 트렌드 | 핵심 내용 |
---|---|---|
클라우드/서버리스 | 서버리스 CQRS | AWS Lambda, DynamoDB Streams 등을 활용하여 서버 관리 없이 CQRS 구현. 운영 비용 및 관리 부담 절감. |
클라우드 네이티브 | Kubernetes, Istio 와 같은 기술로 CQRS 컴포넌트의 자동 확장, 트래픽 관리, 유연성 확보. | |
지능형 자동화 | AI/ML 기반 최적화 | ML 모델이 쿼리 패턴을 분석하여 읽기 모델의 인덱스 구조, 파티셔닝 등을 자동으로 최적화. |
스트림 프로세싱 | Apache Flink, Apache Spark Streaming 등을 통해 대용량 이벤트를 고성능으로 실시간 처리. | |
분산 및 에지 컴퓨팅 | 에지 컴퓨팅 통합 | CDN 에지에 읽기 모델을 캐싱하거나, 지역별로 명령 처리를 분산하여 사용자에게 더 가까운 서비스 제공. |
마이크로서비스/데이터 메시 | 각 도메인 (Domain) 을 독립적인 데이터 제품 (Data Product) 으로 관리하여 데이터 거버넌스 (Governance) 및 통합 용이성 확보. | |
개발/운영 혁신 | DevOps 및 GitOps | 코드형 인프라 (Infrastructure as Code) 를 사용하여 배포를 자동화하고, Git 을 통해 시스템 상태를 관리. |
관측성 강화 | Prometheus, Grafana, Jaeger 등을 통합하여 분산된 CQRS 아키텍처의 상태를 실시간으로 모니터링하고 장애를 진단. |
CQRS 의 최신 트렌드는 클라우드 네이티브로의 전환과 지능형 자동화에 초점을 맞추고 있다. 서버리스 아키텍처를 통해 인프라 관리 부담을 줄이고, Kubernetes(쿠버네티스) 와 같은 기술로 시스템을 유연하게 확장한다. 또한, AI/ML을 활용하여 읽기 모델을 자동으로 최적화하고, 스트림 프로세싱 기술로 대용량 데이터를 실시간으로 처리하는 추세가 강화되고 있다. 사용자 경험을 개선하기 위한 에지 컴퓨팅의 도입도 활발하며, 이러한 복잡성을 효율적으로 관리하기 위해 **DevOps(데브옵스)**와 관측성은 필수적인 요소로 자리 잡았다.
최종 정리 및 학습 가이드
내용 정리
“도로 두 개” 를 깐다 생각하면 쉽다.
한 도로 (쓰기) 는 규칙·검증이 촘촘하고, 다른 도로 (읽기) 는 고속도로처럼 빠르다.
쓰기에서 생긴 변화는 이벤트로 읽기 쪽 표지판 (프로젝션) 을 업데이트한다. 그래서 읽기를 크게 늘려도 쓰기 성능이 무너지지 않는다. 대신 표지판 갱신이 살짝 늦을 수 있음 (최종 일관성) 을 제품·UX 로 보완해야 한다.
무엇/왜
- 정의: 읽기/쓰기 모델·파이프라인 분리.
- 장점: 조회 가속, 쓰기 안정화, 독립 스케일, 폴리글랏, 보안 경계.
- 주의: 복잡성·일관성 관리.
어떻게
- 읽기: 프로젝션/머티리얼라이즈드 뷰, 캐시·인덱스 최적화.
- 쓰기: 트랜잭션·도메인 규칙·권한 집중.
- 동기화: 이벤트·메시지 브로커, Outbox/CDC.
언제
- 읽기 편중, 도메인 규칙 복잡, 팀 분리·배포 독립이 필요, 데이터 저장소 이질성 수용 필요 시 적합.
- 단순 CRUD 엔 과설계가 될 수 있음.
사례
- Microsoft eShopOnContainers: 단순화된 CQRS 레퍼런스.
- Netflix Tudum: CQRS 구현을 RAW/Hollow 방향으로 최적화.
함께 볼 패턴
- ES(감사·재생성), EDA, Sagas/Outbox, Database-per-service.
학습 로드맵
Phase | 기간 | 목표 | 주요 학습 내용 | 산출물/성과 |
---|---|---|---|---|
1. 기초 이해 | 1~2 주 | 개념·배경 이해 | CRUD 한계, CQS vs CQRS | 개념 노트, 비교표 |
2. 이론 학습 | 1~2 주 | 설계 원리 습득 | 구성 요소, 데이터 흐름, 일관성 모델 | 아키텍처 다이어그램 |
3. 분석 | 1 주 | 적용 판단 | 장단점, 트레이드오프, 사례 분석 | 적용 가이드 |
4. 구현 실습 | 2~4 주 | CQRS 구축 | 단일 앱 구현, Outbox+CDC, 파티션 설계 | 동작 예제 코드 |
5. 운영 최적화 | 2 주 | 안정적 운영 | OTel 관측성, 지연 모니터링, 장애 복구 | 모니터링 대시보드 |
6. 고급 주제 | 지속 | 확장·최적화 | Event Sourcing, Saga, Serverless | 고급 설계 문서 |
학습 항목 매트릭스
카테고리 | Phase | 항목 | 중요도 | 설명 |
---|---|---|---|---|
기초 | 1 | CQS 원칙 | 필수 | CQRS 의 이론적 기반 (명령/조회 분리). |
기초 | 1 | CQRS 핵심 개념 | 필수 | 읽기/쓰기 모델·인터페이스를 분리해 독립 최적화. |
기초 | 1 | 최종 일관성 | 필수 | 비동기 전파로 읽기 최신성이 지연될 수 있음을 이해. |
기초 | 1 | CAP 맥락 이해 | 권장 | 가용성/일관성·분할 내성 트레이드오프 감각 잡기. |
이론 | 2 | 도메인 모델링 (애그리게이트) | 필수 | 쓰기 모델의 트랜잭션 경계·불변식 설계. |
이론 | 2 | 프로젝션/뷰 설계 | 필수 | 조회 특화 머티리얼라이즈드 뷰 설계 원칙. |
이론 | 2 | 도메인 이벤트 | 권장 | 상태 변화 사실을 이벤트로 표준화 (의미·스키마). |
구현 | 4 | 기본 CQRS 구현 | 필수 | Command/Query 핸들러·Read/Write 모델 분리. |
구현 | 4 | 메시지 브로커 도입 | 권장 | 비동기 이벤트 전달 (Kafka/RabbitMQ 등). |
구현 | 4 | Transactional Outbox | 필수급 권장 | DB 쓰기 + 이벤트 발행 원자성 보장. |
구현 | 5 | 프로젝션 파이프라인 | 권장 | 이벤트→읽기 모델 갱신 (실시간/배치). |
구현 | 5 | 폴리글랏 퍼시스턴스 | 권장 | 읽기/쓰기 저장소를 목적별로 분리·튜닝. |
구현 | 5 | MSA 통합 (선택) | 선택 | 서비스 경계 분리·API 게이트웨이·스키마 경계. |
운영 | 6 | 모니터링/관측 | 권장 | 읽기 p95·뷰 갱신 지연·Outbox 미전송·중복률. |
용어 정리
카테고리 | 용어 | 정의 | 관련/주의 |
---|---|---|---|
핵심 패턴·원칙 | CQRS | 읽기 (Query) 와 쓰기 (Command) 모델·경로를 분리하는 설계 패턴. 성능·확장성 최적화가 목적 | 대부분 시스템엔 과한 복잡성일 수 있음. 선별 적용 권장. |
CQS | 쿼리는 부작용 없고, 명령은 결과를 반환하지 않는 메서드 수준 규율 | CQRS 의 사상적 기반. | |
Event Sourcing | 모든 상태 변경을 이벤트로 영속화하고 이벤트 재생으로 상태 복원 | CQRS 와 자주 결합되나 필수 아님. | |
도메인/경계 | Aggregate | DDD 의 일관성 경계. 명령 처리와 상태 변경의 단위 | CQRS 쓰기 모델의 핵심 단위 |
Bounded Context | 모델/언어의 경계를 명시하는 DDD 전략 개념 | 마이크로서비스 경계 설정에 활용. | |
구성요소/역할 | Command | 시스템 상태를 변경하는 요청/메시지 | Command Handler 와 1:1 관계 |
Query | 상태를 조회하는 요청 (부작용 없음) | Read Model 만 접근 | |
Command Handler | 명령 검증·도메인 규칙 실행·쓰기 저장수행 | Outbox 와 함께 이벤트 발행 연결 | |
Query Handler | 조회 요청을 처리하고 읽기 모델에서 응답 | Read DB 최적화와 결합 | |
Projection | 이벤트를 소비해 읽기 모델 (물질화 뷰) 을 갱신하는 프로세스 | Denormalizer 라고도 부름 | |
Read Model / Write Model | 조회 전용 모델 / 상태 변경 전용 모델 | 분리 저장소 권장 (성능·독립 스케일) | |
Event Store | 이벤트 소싱용 이벤트 영속 저장소 | 스냅샷·리플레이 지원 | |
데이터·저장소 | Materialized View | 읽기 성능을 위해 비정규화된 실시간/준실시간 뷰 | Projection 결과물 |
Snapshot | 특정 시점 Aggregate 상태 스냅샷 | 긴 리플레이 시간 단축 | |
Polyglot Persistence | 워크로드별 최적화된 DB 를 혼합 사용 | Read/Write 저장소 이기종화 | |
메시징/동기화 | Event Broker | 이벤트 발행/구독 인프라 (Kafka, RabbitMQ 등) | 전달 보장·순서·중복 고려 |
Delivery Semantics | at-most/at-least/ exactly-once 전달 의미 | EOS 는 범위·전제조건이 있음. | |
Consumer Lag | 브로커 오프셋 대비 소비 지연 | 읽기 모델 랙 지표로 활용 | |
신뢰성·일관성 패턴 | Transactional Outbox | DB 트랜잭션 안에서 아웃박스 레코드를 기록하고 CDC/퍼블리셔로 브로커 전송 | Dual-write 문제 표준 해법. |
Saga(보상 트랜잭션) | 로컬 트랜잭션들의 연쇄와 보상 처리로 분산 일관성 확보 | 오케스트레이션/코레오그래피. | |
Idempotence | 같은 메시지를 여러 번 처리해도 결과가 동일 | 중복·재시도 안전 처리의 기본 |
참고 및 출처
- CQRS 패턴 – Microsoft Learn
- Event Sourcing 패턴 – Microsoft Learn
- CQRS – Martin Fowler
- CQRS 패턴 – microservices.io
- CQRS 패턴 – AWS Prescriptive Guidance
- CQRS 학습 가이드 – Confluent
- Kurrent(EventStoreDB) 공식 문서
- Outbox Event Router – Debezium 문서
- Transactional Outbox – microservices.io
- Transactional Outbox – AWS Prescriptive Guidance
- CQRS와 이벤트 소싱 in Java – Baeldung
- 그림으로 보는 CQRS – Red Hat
- CQRS 정의 – TechTarget
- Types of CQRS – Enterprise Craftsmanship
- Azure Cosmos DB: Event Sourcing 코드 샘플 – Microsoft
- CQRS 패턴(구성 패턴) – Confluent Developer
- Event Sourcing 패턴 – microservices.io
- Event Sourcing & CQRS 패턴 이해 – Mia-Platform
- CQRS 디자인 패턴 – GeeksforGeeks
- Demystifying CQRS: Simple CQRS Part 1 – Rootstrap
- CQRS 패턴 – System Design School
- Amazon DynamoDB로 CQRS 이벤트 스토어 구축 – AWS 블로그
- Kafka 메시지 순서 보장 전략 – Baeldung
- 모놀리스를 CQRS+ES로 마이크로서비스로 분해 – AWS Prescriptive Guidance(패턴)