Post

CQRS 책임 분리

명령과 조회 모델을 분리해 변경 비용을 낮추고 이벤트 기반 읽기 모델로 최종 일관성을 관리하는 CQRS 적용 가이드

CQRS 책임 분리

이 글의 결론: CQRS는 성능 최적화 기법이 아니라 명령(결정)과 조회(설명)의 책임을 분리해 변경 비용을 낮추는 설계다.

CQRS의 본질은 “DB를 두 개로 나눈다”가 아니다. 상태를 바꾸는 모델과, 상태를 보여주는 모델을 서로 간섭하지 않게 만드는 것이 핵심이다.


CQRS가 필요한 조건 결론: 조회가 문제의 원인일 때만 분리하라.

CQRS를 고려해야 하는 신호는 다음 셋 중 하나 이상이다.

  1. 조회 부하가 쓰기 모델을 망친다

    • 목록/검색/집계가 많아 Aggregate 설계가 왜곡됨
  2. 모델 복잡도가 충돌한다

    • 쓰기는 불변식/상태머신이 중요한데
    • 읽기는 조인/필터/정렬이 중요함
  3. 읽기 최적화 요구가 구조를 깨뜨린다

    • 인덱스/캐시/비정규화 요구가 도메인 규칙을 침식

이 셋이 없으면 CQRS는 과잉이다.


이 강의에서 CQRS를 “작게” 적용한 이유 결론: 분리는 비용이기 때문에 최소 범위로만 쓴다.

이 강의는 전면 CQRS가 아니라 부분 적용을 택한다.

  • 쓰기 모델: Order Aggregate (DDD 규칙 유지)
  • 읽기 모델:

    • 단순 조회는 로컬 테이블
    • 교차 서비스 조회는 이벤트 기반 동기화 뷰

이유:

  • 목적은 성능 과시가 아니라 도메인 보호
  • 운영 복잡도를 통제하면서 변경 비용만 낮추기

이벤트 기반 CQRS 흐름 결론: 조회 모델은 “결정”을 하지 않고 사실을 반영한다.

기본 흐름

1
2
3
4
5
6
Command
 → Write Model (Aggregate)
   → Domain Event
     → Kafka
       → Read Model Consumer
         → Read DB Update

규칙:

  • 조회 모델은 비즈니스 판단 금지
  • 이벤트는 사실만 전달
  • 읽기 모델은 멱등 업서트

실무 함정 4개 결론: CQRS의 실패는 대부분 “운영 감각 부족”에서 나온다.

1) 동기화 지연으로 인한 UX 문제

  • 증상: 방금 승인했는데 목록에 안 보임
  • 대응:

    • UI에 “처리 중” 상태 노출
    • SLO 정의(예: 2초 이내 반영)

2) 조회 모델 스키마 폭발

  • 증상: 화면마다 테이블/뷰 추가
  • 대응:

    • 유스케이스 단위 뷰
    • 공통 조회는 재사용

3) 이벤트 스키마 변경 영향

  • 증상: 이벤트 변경 → 조회 깨짐
  • 대응:

    • 이벤트는 계약
    • 조회 모델은 하위 호환으로 진화

4) 디버깅 난이도 증가

  • 증상: “어디서 틀렸는지 모르겠다”
  • 대응:

    • 이벤트 추적 ID
    • 재생 가능한 파이프라인

예시 1 결론: 주문 서비스는 로컬 테이블을 “조회 모델”로 재사용할 수 있다.

흐름

  • 쓰기:

    • Order Aggregate → Order Table
  • 읽기:

    • Order Table을 조회 전용 관점으로 사용
    • 복잡한 규칙 접근 ❌

의미:

  • CQRS는 물리적 분리 필수 아님
  • 논리적 책임 분리가 우선

예시 2 결론: 고객 서비스는 토픽 기반으로 조회 모델을 동기화한다.

흐름

1
2
3
OrderApproved Event
 → Customer Service Consumer
   → CustomerOrderView Update

특징:

  • 고객 서비스는 주문 Aggregate를 모름
  • 필요한 사실만 로컬 뷰로 유지
  • 조인/집계는 조회 모델에서만 수행

CQRS 비교 표 결론: “얻는 것과 잃는 것을 동시에 본다.”

구분얻는 것잃는 것대안권장 기준
CQRS쓰기 모델 보호, 조회 최적화운영 복잡도캐시, 리드 레플리카조회 요구가 도메인을 침식
캐시빠른 응답일관성 관리TTL/무효화데이터 단순
리드 레플리카읽기 분산지연튜닝스키마 동일
단일 모델단순성성능/변경 비용없음소규모/초기

흔한 오해 / 실수 3가지

  1. CQRS = 읽기 DB 하나 더 두는 기법이라는 오해
    • 문제: 성능 때문에 무작정 읽기 DB를 추가
    • 결과: 동기화 지연·운영 복잡도만 증가
    • 교정 기준: CQRS는 성능이 아니라 책임 분리(결정 vs 설명)가 목적이다.
  2. 조회 모델에 비즈니스 규칙을 넣는 실수
    • 문제: “승인 가능 여부” 같은 판단을 Read Model에서 계산
    • 결과: 규칙 중복, 정합성 붕괴
    • 교정 기준: 조회 모델은 사실 반영만, 판단은 Write Model에서만.
  3. 모든 화면마다 전용 조회 테이블을 만드는 과잉 설계
    • 문제: 유스케이스 단위 고려 없이 화면 단위 테이블 생성
    • 결과: 스키마 폭발, 유지보수 불가
    • 교정 기준: 유스케이스 기준 뷰, 공통 조회는 재사용.

대표 실패 시나리오 3가지

  1. 동기화 지연으로 UX 불만 폭증
    • 상황: 주문 승인 직후 목록에 미반영
    • 원인: eventual consistency에 대한 UX 설계 부재
    • 결과: “승인 안 됐다”는 오해, 재시도 폭주
    • 대응: 처리 중 상태 표시 + 반영 SLO 명시
  2. 이벤트 스키마 변경으로 조회 모델 재구축 불가
    • 상황: 이벤트 필드 삭제/의미 변경
    • 원인: 이벤트를 내부 DTO처럼 취급
    • 결과: 과거 이벤트 재생 불가, 데이터 손실
    • 대응: 이벤트 계약 고정 + 하위 호환 유지
  3. 디버깅 불가 상태
    • 상황: 조회 데이터가 틀린데 원인 불명
    • 원인: 이벤트 추적/재생 경로 부재
    • 결과: 수동 데이터 수정
    • 대응: 추적 ID + 리플레이 가능한 파이프라인 확보

실무 체크리스트 (CQRS 적용 점검)

  1. 조회 요구 때문에 Write Model이 왜곡되고 있는가
  2. 읽기와 쓰기의 변경 주기가 다른가
  3. 조회 모델이 비즈니스 판단을 하지 않는가
  4. 조회 모델이 이벤트만으로 재구축 가능한가
  5. 동기화 지연을 UX로 흡수할 설계가 있는가
  6. 조회 모델 스키마 증가를 통제할 기준이 있는가
  7. 이벤트 스키마 변경 시 조회 영향이 예측 가능한가
  8. 조회 장애가 쓰기 안정성에 영향을 주지 않는가
  9. 캐시/리드 레플리카로는 해결되지 않는 문제인가
  10. CQRS를 전면이 아니라 부분 적용하고 있는가
  11. 조회 모델 재생성 절차가 문서화돼 있는가
  12. “지금 CQRS를 안 쓰면 무엇이 더 망가지는가?”에 답할 수 있는가

This post is licensed under CC BY 4.0 by the author.