본문으로 건너뛰기
김신건의 로그

[Pattern] CQRS: Command / Query 분리, read model

· 수정 · 📖 약 1분 · 542자/단어 #cqrs #architecture #pattern #backend
CQRS, Command Query Responsibility Segregation, read model, write model, projection, materialized view

정의

CQRS (Command Query Responsibility Segregation) = 쓰기 모델읽기 모델분리. 같은 데이터를 다른 형태 로 저장 / 조회.

IMPORTANT

CQRS 는 쓰기 / 읽기 모델 분리 라는 추상 패턴. 반드시 다른 DB 일 필요는 없다. 대부분 시작 단계는 같은 DB + 다른 모델.

동기

flowchart LR
    Trad[전통 CRUD]
    Trad --> ProbW[복잡한 비즈니스 로직 + 정규화 DB]
    Trad --> ProbR[복잡한 join 으로 화면 그리기]
    CQRS[CQRS]
    CQRS --> Write[Write: 정규화, 트랜잭션, 비즈니스 규칙]
    CQRS --> Read[Read: 화면 친화, denormalized, 빠른 조회]

CRUD 의 읽기 / 쓰기 사용 패턴이 매우 다른 워크로드 (대시보드, 추천, 검색) 에서 효과적.

기본 구조

flowchart TB
    Cmd[Command<br/>CreateOrder, ConfirmOrder] --> CH[Command Handler]
    CH --> WM[(Write Model<br/>정규화, 트랜잭션)]
    WM -->|event| EH[Event Handler / Projection]
    EH --> RM[(Read Model<br/>denormalized, optimized)]
    Query[Query<br/>GetOrderSummary] --> QH[Query Handler]
    QH --> RM
항목Command SideQuery Side
입력비즈니스 의도 (Command)정보 요청 (Query)
검증비즈니스 규칙권한만
결과변경 사실 (성공/실패)데이터
모델정규화 (3NF)denormalized
DBRDB 가 일반다양 (RDB, ES, Redis)
일관성보통 eventual

예시: 전자상거래

-- Write Model (정규화)
TABLE orders (id, user_id, status, ...);
TABLE order_items (id, order_id, product_id, qty, price);
TABLE products (id, name, ...);

-- Read Model (denormalized, 화면 친화)
TABLE order_summary_view (
  order_id, user_name, total, item_count,
  status_label, created_at, items_json
);

읽기 1 쿼리:

SELECT * FROM order_summary_view WHERE order_id = ?;

쓰기 - 정상 트랜잭션 + event 발행:

BEGIN;
INSERT INTO orders ...;
INSERT INTO order_items ...;
INSERT INTO outbox (event) VALUES ('OrderCreated', ...);
COMMIT;

이후 event handlerorder_summary_view 갱신.

Event Sourcing 과 자주 같이

flowchart LR
    Cmd[Command] --> Agg[Aggregate]
    Agg -->|emit event| ES[(Event Store)]
    ES -->|project| V1[(Read View 1)]
    ES -->|project| V2[(Read View 2)]
    ES -->|project| V3[(Read View 3)]
    Query --> V1 & V2 & V3

자세한 건 event-sourcing.

Read Model 종류

Read Model적합
Materialized view (DB)같은 RDB 안
ElasticSearch / OpenSearch검색 + aggregation
Redis작은 hot data
Cassandra시계열 / 대용량
GraphQL cacheAPI 응답 캐시
클라이언트 IndexedDBoffline-first

Eventual Consistency

sequenceDiagram
    User->>API: POST /orders (command)
    API->>WriteDB: INSERT
    WriteDB-->>API: OK
    API-->>User: 200 OK (order_id)
    Note over API,ReadDB: 비동기 projection 진행
    User->>API: GET /orders/123 (query)
    API->>ReadDB: SELECT
    ReadDB-->>API: NOT FOUND? (projection lag)

CAUTION

방금 만든 주문이 안 보이는 UX 함정. 해결책:

  • 짧은 lag (수 ms) 보장 + UI 가 낙관적 표시
  • write 후 즉시 같은 transaction 으로 readwrite DB 우회
  • 작성자 본인은 강 일관성, 다른 사람은 eventual (read-your-writes)

언제 CQRS? (안 쓸 때가 더 많다)

flowchart TD
    Q1{비즈니스 도메인 복잡?}
    Q1 -->|아니오| Simple[CRUD 충분]
    Q1 -->|예| Q2{읽기 / 쓰기 패턴 매우 다름?}
    Q2 -->|아니오| Simple
    Q2 -->|예| Q3{팀이 ES + 분산 시스템 이해?}
    Q3 -->|아니오| Risk[학습 비용 큼 → 천천히]
    Q3 -->|예| CQRS[CQRS 시작]

IMPORTANT

대부분의 앱은 CQRS 가 과한 솔루션. 진짜 도움 되는 곳 (큰 e-commerce, 트레이딩 시스템, 큰 SaaS 대시보드) 가 아니면 모듈 모놀리스 + 일부 read model 정도면 충분.

단순 CQRS (가벼운 형태)

flowchart LR
    Cmd[Command Handler<br/>POST /orders] --> Write[(DB)]
    Write -->|trigger / event| Mat[Materialized View<br/>갱신]
    Query[Query Handler<br/>GET /orders/dashboard] --> Mat
  • 같은 DB.
  • materialized view 또는 summary table.
  • 별도 인프라 없이 80% 의 효과.

흔한 함정

WARNING

  1. 모든 곳에 CQRS = 단순 CRUD 도 분리. 복잡도 폭증.
  2. Projection 동기 실행 = read model 갱신이 write 막음. async + idempotent.
  3. Read-your-writes 무시 = 사용자가 방금 한 일을 못 봄 UX.
  4. Read model 재구성 어려움 = event log 없으면 replay 불가. ES + CQRS 가 자주 묶이는 이유.

관련 위키

이 글의 용어 (5개)
[Pattern] Event Sourcing: 상태 대신 이벤트 누적distributed-systems
정의 Event Sourcing = 현재 상태 대신 과거 이벤트 시퀀스 를 저장. 현재 상태 = event 모두 replay 한 결과. 핵심 약속 - 과거 모든 변화 기록. 감사…
[Pattern] Microservices vs Monolith: 언제 분리, 언제 통합distributed-systems
정의 | | Monolith | Modular Monolith | Microservices | |---|---|---|---| | 배포 단위 | 1개 | 1개 (모듈 명확) | …
[Pattern] Outbox Pattern: DB + 메시지의 원자성distributed-systems
정의 Outbox Pattern = DB 변경 + 메시지 발행 의 원자성 보장. 이중 쓰기 (dual write) 문제 의 표준 해결. 문제: Dual Write | 시나리오 |…
[Pattern] Saga: 분산 트랜잭션의 보상 흐름distributed-systems
정의 Saga = 여러 service 의 로컬 트랜잭션 을 연결한 긴 흐름. 한 단계 실패 시 이전 단계의 보상 (compensating) 트랜잭션 으로 논리적 롤백. [!IMP…
[Redis] Cache Patterns: Cache-Aside, Stampede, Evictiondatabase-internals
정의 Cache Pattern 은 Redis (또는 다른 캐시) 를 어떤 경로로 읽고 쓰는지 의 디자인 결정. 같은 인프라로도 패턴 선택에 따라 일관성 / 지연 / 부하 가 완전…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기