CQRS 책임 분리
명령과 조회 모델을 분리해 변경 비용을 낮추고 이벤트 기반 읽기 모델로 최종 일관성을 관리하는 CQRS 적용 가이드
Posted Updated
By okorion
CQRS 책임 분리
이 글의 결론: CQRS는 성능 최적화 기법이 아니라 명령(결정)과 조회(설명)의 책임을 분리해 변경 비용을 낮추는 설계다.
CQRS의 본질은 “DB를 두 개로 나눈다”가 아니다. 상태를 바꾸는 모델과, 상태를 보여주는 모델을 서로 간섭하지 않게 만드는 것이 핵심이다.
CQRS가 필요한 조건 결론: 조회가 문제의 원인일 때만 분리하라.
CQRS를 고려해야 하는 신호는 다음 셋 중 하나 이상이다.
조회 부하가 쓰기 모델을 망친다
- 목록/검색/집계가 많아 Aggregate 설계가 왜곡됨
모델 복잡도가 충돌한다
- 쓰기는 불변식/상태머신이 중요한데
- 읽기는 조인/필터/정렬이 중요함
읽기 최적화 요구가 구조를 깨뜨린다
- 인덱스/캐시/비정규화 요구가 도메인 규칙을 침식
이 셋이 없으면 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가지
- CQRS = 읽기 DB 하나 더 두는 기법이라는 오해
- 문제: 성능 때문에 무작정 읽기 DB를 추가
- 결과: 동기화 지연·운영 복잡도만 증가
- 교정 기준: CQRS는 성능이 아니라 책임 분리(결정 vs 설명)가 목적이다.
- 조회 모델에 비즈니스 규칙을 넣는 실수
- 문제: “승인 가능 여부” 같은 판단을 Read Model에서 계산
- 결과: 규칙 중복, 정합성 붕괴
- 교정 기준: 조회 모델은 사실 반영만, 판단은 Write Model에서만.
- 모든 화면마다 전용 조회 테이블을 만드는 과잉 설계
- 문제: 유스케이스 단위 고려 없이 화면 단위 테이블 생성
- 결과: 스키마 폭발, 유지보수 불가
- 교정 기준: 유스케이스 기준 뷰, 공통 조회는 재사용.
대표 실패 시나리오 3가지
- 동기화 지연으로 UX 불만 폭증
- 상황: 주문 승인 직후 목록에 미반영
- 원인: eventual consistency에 대한 UX 설계 부재
- 결과: “승인 안 됐다”는 오해, 재시도 폭주
- 대응: 처리 중 상태 표시 + 반영 SLO 명시
- 이벤트 스키마 변경으로 조회 모델 재구축 불가
- 상황: 이벤트 필드 삭제/의미 변경
- 원인: 이벤트를 내부 DTO처럼 취급
- 결과: 과거 이벤트 재생 불가, 데이터 손실
- 대응: 이벤트 계약 고정 + 하위 호환 유지
- 디버깅 불가 상태
- 상황: 조회 데이터가 틀린데 원인 불명
- 원인: 이벤트 추적/재생 경로 부재
- 결과: 수동 데이터 수정
- 대응: 추적 ID + 리플레이 가능한 파이프라인 확보
실무 체크리스트 (CQRS 적용 점검)
- 조회 요구 때문에 Write Model이 왜곡되고 있는가
- 읽기와 쓰기의 변경 주기가 다른가
- 조회 모델이 비즈니스 판단을 하지 않는가
- 조회 모델이 이벤트만으로 재구축 가능한가
- 동기화 지연을 UX로 흡수할 설계가 있는가
- 조회 모델 스키마 증가를 통제할 기준이 있는가
- 이벤트 스키마 변경 시 조회 영향이 예측 가능한가
- 조회 장애가 쓰기 안정성에 영향을 주지 않는가
- 캐시/리드 레플리카로는 해결되지 않는 문제인가
- CQRS를 전면이 아니라 부분 적용하고 있는가
- 조회 모델 재생성 절차가 문서화돼 있는가
- “지금 CQRS를 안 쓰면 무엇이 더 망가지는가?”에 답할 수 있는가
This post is licensed under CC BY 4.0 by the author.
