📚 CQRS
Command Query Responsibility Segregation 패턴
🎯 학습 목표
- 목표 1: CQRS가 왜 필요한 패턴인지를 \"단일 모델의 한계\" 맥락에서 자신의 언어로 설명할 수 있다.
- 목표 2: CQRS는 4단계 스펙트럼이 있다는 사실을 이해하고, 자신의 도메인에 적합한 단계를 판단할 수 있다.
- 목표 3: CQRS와 NoSQL이 결합된 표준 아키텍처를 그림으로 그릴 수 있고, 결과적 일관성의 함정을 설명할 수 있다.
📖 사전 지식
깊은 분산 시스템 경험은 필요하지 않습니다.
RDBMS를 평소에 쓰시면서 SELECT, INSERT, JOIN 정도가 익숙하시면 충분합니다. HTTP API 설계 경험도 있으시면 좋습니다. 이벤트나 메시지 브로커는 이름만 들어보셨어도 괜찮습니다.
1왜 모델을 둘로 나누는가
1.1 우리에게 익숙한 풍경부터
여러분이 처음 백엔드 개발을 시작했을 때를 생각해 보시기 바랍니다. 사용자 정보를 관리하는 코드를 작성하라는 요청을 받으면 보통 이렇게 하지 않으셨나요? 먼저 RDBMS에 users 테이블을 하나 만듭니다. 그리고 애플리케이션 코드에 User 클래스를 만듭니다. 회원가입을 받을 때도 이 객체를 쓰고, 마이페이지만 볼 때도 같은 객체를 씁니다. INSERT, SELECT, UPDATE, DELETE, 그러니까 CRUD라고 부르는 네 가지 연산이 모두 이 단일 모델을 중심으로 이루어집니다.
이 방식은 사실 굉장히 잘 작동합니다. 다만 어느 순간부터 이 모델이 깨지기 시작하는 지점이 있는데, 그 지점이 무엇인지를 먼저 잘 이해해야 CQRS가 왜 등장했는지를 자연스럽게 받아들일 수 있습니다.
1.2 첫 번째 통증: 읽기와 쓰기가 다른 속도로 자랍니다
전자상거래 서비스를 한번 머릿속에 그려 봅시다. 주문이 한 번 들어올 때 그 주문에 대한 조회는 수십 번에서 수백 번까지 일어난다는 점입니다.
📊 수치로 보는 부하 비대칭
가상의 중대형 쇼핑몰 기준:
• 일일 주문: 50만 건 → 초당 약 6 TPS
• 일일 주문 조회: 5천만 건 → 초당 약 580 TPS
• 읽기/쓰기 비율: 약 100:1
이 100배 차이의 부하를 같은 단일 데이터베이스가 모두 감당해야 합니다. 서비스가 자라면 세 가지 통증이 차례로 나타납니다:
① JOIN 폭발
마이페이지 주문 내역 화면 하나를 그리려면 orders, order_items, products, users, shipping_addresses, payments, shipments처럼 6~8개 테이블을 JOIN해야 합니다.
② 잠금 경합
쓰기가 늘수록 dead tuple이 쌓이고 AUTOVACUUM 작업이 자주 돌며, 인덱스 재구성이나 트리거 실행처럼 공유 락이 필요한 작업이 읽기/쓰기 모두에 영향을 줍니다.
③ 스키마 변경 비용
마케팅팀에서 \"적립금 사용액도 보여주세요\" 요청 하나에 며칠짜리 마이그레이션이 따릅니다. 백필이 며칠씩 걸리고, 락이 걸리거나 성능이 저하될 수 있습니다.
⚠️ 핵심 통찰
이 세 가지 통증이 모두 같은 뿌리에서 나옵니다. \"쓰기 작업과 읽기 작업이 본질적으로 다른 일인데, 같은 모델을 공유하기 때문에 서로를 방해한다\"는 사실입니다.
1.3 두 번째 통증: 한 모델이 두 가지 일을 동시에 해야 합니다
같은 \"주문\" 객체를 두 시점에서 보겠습니다:
- 주문 버튼 클릭 시: 재고 확인 → 결제 승인 → 도메인 규칙 검증 → 저장.
가장 중요한 것은 \"정합성\"입니다. - 주문 내역 화면 조회 시: 사용자 이름, 상품 이미지, 배송 상태 등 의미 있는 화면.
가장 중요한 것은 \"표현\"입니다.
같은 Order 클래스가 이 두 가지 책임을 동시에 짊어지면 어떻게 될까요? 시간이 지날수록 클래스가 비대해지고, 어느 쪽 책임도 깔끔하게 수행하지 못합니다.
비즈니스 규칙 코드와 화면 표현 코드가 한 파일 안에서 부딪치는 모습입니다. 이런 클래스를 객체지향 설계에서는 \"신 객체(God Object)\"라고 부릅니다.
1.4 그래서 우리가 풀어야 할 질문
쓰기와 읽기를 분리하면 무엇이 좋아지고, 무엇이 어려워질까? — 이 질문이 CQRS 패턴의 출발점입니다
2CQRS 개념 이해
2.1 CQRS는 무엇을 의미하는가
Command Query Responsibility Segregation. \"명령과 질의의 책임 분리\"입니다.
이 패턴은 Greg Young이 2010년경에 처음 제안했고, Martin Fowler가 2011년에 자기 블로그에 정리해서 널리 알려졌습니다.
\"어떤 정보를 갱신할 때 사용하는 모델과 같은 정보를 읽을 때 사용하는 모델을 서로 다르게 둘 수 있다. 어떤 상황에서는 이 분리가 가치가 있다. 다만 대부분의 시스템에서 CQRS는 위험한 복잡도를 추가한다는 점을 경계해야 한다.\" — Martin Fowler, 2011
✓ 핵심 정의
데이터를 갱신할 때 쓰는 모델과 읽을 때 쓰는 모델을 굳이 같게 둘 필요가 없다.
2.2 어디서 왔는가: CQS 원칙
CQRS는 그 이전부터 객체지향 설계 쪽에서 통용되던 CQS (Command-Query Separation) 원칙에서 진화한 것입니다. Bertrand Meyer가 제안했지요.
| 구분 | 적용 범위 | 분리 단위 |
|---|---|---|
| CQS (Bertrand Meyer) | 단일 객체 | 메서드 |
| CQRS (Greg Young) | 시스템/아키텍처 | 모델 (선택적으로 DB까지) |
2.3 Command와 Query는 무엇이 다른가
| 구분 | Command | Query |
|---|---|---|
| 의도 | 상태 변경 | 정보 조회 |
| 부수효과 | 있음 | 없음 |
| 반환값 | 없거나 성공/실패만 | 데이터 |
| 예시 | PlaceOrder, CancelSubscription |
GetOrdersByUser, SearchProducts |
| 책임 | 도메인 규칙 검증 | 응답 최적화 |
나쁜 예 — 섞여 있는 경우
좋은 예 — 분리된 경우
2.4 가장 가벼운 형태: DB는 하나
CQRS라고 하면 곧바로 DB 두 개를 떠올리는 분들이 많습니다. 그렇지 않습니다!
💡 가장 가벼운 CQRS
DB는 그냥 하나입니다.
단지 애플리케이션 코드에서 쓰기 쪽 모델과 읽기 쪽 모델만 나눠 둡니다. 도메인 객체와 DTO를 명확히 분리하고, Command Service와 Query Service를 따로 두는 것만으로도 출발할 수 있습니다. CQRS를 처음 도입하실 때는 이 단계에서 충분한 가치를 얻을 수 있습니다.
2.5 CQRS는 스위치가 아니라 스펙트럼입니다
CQRS는 켜고 끄는 스위치가 아니라 적용 강도의 스펙트럼입니다.
| 단계 | 구성 | 일관성 | 적합 도메인 |
|---|---|---|---|
| 1단계 | 모델만 분리, DB는 하나 | 강한 일관성 | 사내 시스템, B2B 백오피스 |
| 2단계 | 읽기 복제본 활용 | 약한 결과적 일관성 | 콘텐츠 서비스, 대시보드 |
| 3단계 | RDBMS + NoSQL 분리 | 결과적 일관성 | 전자상거래, 소셜 피드 |
| 4단계 | Event Sourcing 결합 | 이벤트 기반 | 금융, 의료 (감사 추적 필수) |
⚠️ 점진적 도입 권장
단계는 위로 올라갈수록 한 번 도입하면 되돌리기 어렵습니다. 무작정 3, 4단계부터 시작하는 것은 매우 위험합니다. 1단계에서 시작해서 통증이 명확하게 드러날 때 한 단계씩 올려가는 점진적 도입이 강력하게 권장됩니다.
2.6 흔한 오해 세 가지
❌ 틀린 주장들
- \"CQRS는 데이터베이스가 항상 두 개다\" — 단계 1, 2는 아닙니다
- \"CQRS는 Event Sourcing이 필수다\" — 두 패턴은 독립적입니다
- \"CQRS는 마이크로서비스에만 쓴다\" — 모놀리식 안에서도 적용 가능합니다
3NoSQL 기초
3.1 왜 NoSQL을 다루는가
이 자료의 종착점은 \"CQRS + NoSQL\"입니다. CQRS의 단계 3 이상에서 읽기 측 DB로 NoSQL이 자주 선택되는데, 왜 그런지를 이해하려면 NoSQL이 무엇인지 짧게 다지고 갑니다.
3.2 NoSQL이란 무엇인가
NoSQL은 단일한 기술의 이름이 아닙니다. RDBMS가 아닌 데이터베이스들을 묶어 부르는 우산 개념입니다.
| 유형 | 대표 제품 | 주된 사용처 |
|---|---|---|
| Document | MongoDB, Couchbase | 비정규화된 문서, 화면 단위 응답 |
| Key-Value | Redis, DynamoDB | 캐시, 세션, 빠른 조회 |
| Wide-Column | Cassandra, HBase | 대규모 시계열, 분석 |
| Graph | Neo4j, Neptune | 관계 중심 데이터, 소셜 그래프 |
3.3 RDBMS와의 결정적 차이
| 항목 | RDBMS | NoSQL |
|---|---|---|
| 스키마 | 사전 정의·강제 | 유연·동적 |
| 확장 | 수직 확장 중심 | 수평 확장 친화 |
| 트랜잭션 | ACID | 제품별 다양, 보통 약함 |
| 조인 | 강력 | 제한적/미지원 |
| 일관성 | 강한 일관성 | 결과적 일관성 |
✓ 핵심 기억
NoSQL은 RDBMS의 대체재가 아니라 보완재입니다. RDBMS는 일관성과 정합성을 책임지고, NoSQL은 유연한 스키마와 수평 확장을 책임집니다.
3.4 CAP 정리
CAP 정리: 분산 시스템은 다음 세 가지를 동시에 만족할 수 없다는 주장입니다.
- Consistency (일관성): 모든 노드가 같은 시점에 같은 데이터를 봄
- Availability (가용성): 모든 요청이 응답을 받음
- Partition tolerance (분할 내성): 네트워크 장애에도 시스템이 계속 동작
분산 환경에서 P는 사실상 필수이므로, 실제 선택은 C와 A 사이의 트레이드오프가 됩니다. NoSQL은 보통 가용성을 우선하고 일관성을 약하게 가져갑니다.
3.5 결과적 일관성 (Eventual Consistency)
즉시 일관성: 단일 RDBMS에서 쓰기를 한 직후 같은 데이터를 읽으면 새로 쓴 값을 봅니다. \"쓴 직후 읽으면 새 값\"이라는 약속입니다.
결과적 일관성: CQRS 단계 3 이상에서는 쓰기 측과 읽기 측이 서로 다른 시스템입니다. 쓰기가 일어난 후 변경이 읽기 측에 반영되기까지 시간이 걸립니다. \"지금 당장은 다를 수 있지만, 충분한 시간이 지나면 결국 같아진다\"는 약속입니다.
📍 일관성 갭 (Consistency Gap)
t0 (쓰기) → t1 (이벤트 발행) → t2 (Projector 갱신) → t3 (조회 가능)
t0부터 t2까지의 구간이 일관성 갭입니다. 정상 상황에서는 약 50~100ms 정도입니다.
문제 상황:
- 사용자가 매우 빠른 액션(주문 직후 마이페이지 이동)をする場合
- Projector나 Kafka 컨슈머에 지연이 생기는 경우 → 갭이 수 초~분 단위로 늘어남
4CQRS + NoSQL 결합 아키텍처
4.1 왜 NoSQL을 읽기 모델에 두는가
| 이유 | 해결하는 통증 |
|---|---|
| 스키마 유연성 | 새 필드 추가 시 마이그레이션 거의 불필요 |
| 수평 확장 | 읽기 트래픽 폭증 시 노드 추가 대응 |
| 비정규화 친화 | JOIN이 필요 없어짐 |
✓ 핵심 정신
\"쓰기는 정규화, 읽기는 비정규화\"
이 한 줄이 CQRS + NoSQL의 핵심입니다.
4.2 표준 결합 아키텍처
세 영역으로 깔끔하게 나뉩니다:
- 왼쪽 (쓰기 영역): Command API → Write DB (RDBMS)
- 가운데 (이벤트 버스): 도메인 이벤트 → Kafka → Projector
- 아래쪽 (읽기 영역): Query API ← Read DB (NoSQL)
흐름을 따라가 봅시다:
- 쓰기: 클라이언트 → Command API → 도메인 검증 → Write DB 저장 (ACID)
- 이벤트: Write DB → 도메인 이벤트 → Kafka
- 읽기 갱신: Kafka → Projector → Read DB 비정규화 문서 작성
- 조회: 클라이언트 → Query API → Read DB 단일 페치 → 응답
4.3 쓰기 측의 책임
쓰기 측이 정합성의 영역입니다:
- ① 도메인 규칙 검증 (재고, 결제, 권한)
- ② ACID 트랜잭션 보장
- ③ 도메인 이벤트의 신뢰성 있는 발행
Outbox 패턴
이벤트 발행의 함정을 해결합니다. 트랜잭션 안에서 도메인 데이터와 이벤트를 같이 저장해 두고, 별도 프로세스가 그 이벤트를 발행하는 방식입니다.
4.4 읽기 측의 책임
읽기 측이 조회 최적화의 영역입니다:
- ① 화면 단위로 비정규화된 문서 유지
- ② 조회 요청에 빠르게 응답
- ③ 새 화면 요구사항에 유연하게 대응
읽기 모델은 데이터베이스 이론에서 말하는 구체화된 뷰(Materialized View)의 개념과 같습니다.
4.5 동기화 메커니즘 두 가지
| 방식 | 장점 | 단점 |
|---|---|---|
| 애플리케이션 이벤트 발행 | 도메인 의미가 풍부한 이벤트 가능 | 애플리케이션 코드에 책임 추가 |
| CDC (Change Data Capture) | 애플리케이션 코드 수정 불필요 | 도메인 의미가 빈약 (데이터 레벨) |
4.6 Event Sourcing — 짧은 만남
Event Sourcing: 상태를 직접 저장하는 게 아니라, 상태가 변경된 사건(이벤트)들을 시간순으로 저장하는 방식입니다.
⚠️ CQRS와 Event Sourcing은 독립적입니다
CQRS를 도입한다고 Event Sourcing이 자동으로 따라오지 않습니다. CQRS의 단계 3까지는 일반적인 RDBMS와 NoSQL만으로 충분히 구현됩니다. Event Sourcing은 그 자체로 학습과 운영 비용이 큰 별개의 결정입니다.
5설계 사례
5.1 사례 1 — 전자상거래 주문 시스템
비즈니스 컨텍스트
- 일일 주문: 50만 건 (평균 6 TPS, 피크 60~100 TPS)
- 일일 주문 조회: 5천만 건 (평균 580 TPS, 피크 5,000~10,000 TPS)
- 읽기/쓰기 비율: 100:1
데이터 모델 비교
쓰기 측 — PostgreSQL (정규화)
읽기 측 — MongoDB (비정규화)
트레이드오프 — 결과적 일관성이 만드는 함정
⚠️ 시나리오
사용자가 주문 완료 버튼을 누르고 곧바로 마이페이지로 이동했습니다. 그런데 방금 주문한 게 안 보입니다. 왜? Write DB에는 저장됐지만 MongoDB 갱신이 아직 완료되지 않았기 때문입니다.
해결 옵션:
- 낙관적 UI 업데이트: Command 응답으로 받은 데이터로 화면을 먼저 채우고, 백그라운드 동기화를 기다림
- Command 응답에 화면 필수 데이터를 함께 담기: 클라이언트가 별도 조회 없이 화면 구성
- 단기 폴링: 주문 직후 짧은 시간 Query API를 폴링해서 동기화 완료 확인
5.2 사례 2 — 소셜 피드 시스템
비즈니스 컨텍스트
- 일일 활성 사용자: 1천만 명
- 1인당 게시물 작성: 1~2건/일
- 1인당 타임라인 조회: 50~100회/일
- 읽기/쓰기 비율: 100~200:1
Fan-out on Write
핵심 통찰: \"조회 시점이 아니라 작성 시점에 미리 계산해 둔다\"
- 사용자가 게시물 작성 → Posts DB에 원본 저장
- Posts DB가 `PostCreated` 이벤트 발행
- Fan-out Worker가 팔로워 모든 사람의 타임라인에 게시물 ID push
- 팔로워가 자기 타임라인 조회 → 자기 캐시 한 번만 페치
Fan-out 전략 비교
| 전략 | 쓰기 시점 | 조회 시점 | 적합 사용자 |
|---|---|---|---|
| Fan-out on Write | 팔로워 N명 캐시에 복사 O(N) | 자기 캐시 1회 페치 O(1) | 일반 사용자 |
| Fan-out on Read | 작업 없음 | 팔로우 목록 조회 + 게시물 머지 | 셀러브리티 (팔로워 수백만) |
| 하이브리드 | 일반 계정은 on-write, 셀러브리티는 on-read | 두 결과 머지 | 대형 서비스 표준 |
데이터 모델 — Redis Sorted Set
한계와 절충
- 저장 공간 폭발: 평균 팔로워 100명이면 100배 저장 공간. 최근 500개만 유지하고 그 너머는 동적 계산
- 셀러브리티 문제: 팔로워 천만 명 = 천만 번 push. 임계치(예: 100만) 이상은 Fan-out on Read로
- 캐시 일관성: 삭제 시 모든 사본 갱신은 비현실적. hydrate 시점에 원본 확인 후 필터링
6안티패턴과 적용 기준
⚠️ 가장 중요한 메시지
\"패턴을 배우는 것보다 쓰지 말아야 할 때를 아는 것이 더 중요합니다.\"
CQRS는 모든 시스템에 적용해야 하는 트렌드가 아닙니다. 잘못 적용하면 시스템을 오히려 더 어렵게 만듭니다.
6.1 언제 CQRS를 쓰면 안 되는가
❌ CQRS 도입을 피해야 할 시나리오
- 단순한 CRUD 도메인: 화면이 데이터를 거의 그대로 보여주고 도메인 규칙이 단순한 경우
- 즉시 일관성이 필수인 도메인: 잔액, 재고 표시처럼 사용자가 즉시 정확한 값을 보아야 하는 경우
- 소규모 팀: 분산 시스템 운영 경험이 없는 작은 팀에게 단계 3 이상은 부담 과중
- 충분히 복잡하지 않은 도메인: CRUD 이상의 비즈니스 규칙이 없다면 복잡도만 더함
6.2 실제 실패 사례 세 가지
| 패턴 | 상황 | 결과 |
|---|---|---|
| \"트렌드라 도입했다\" | 10명 스타트업이 단순 CRUD에 단계 3 도입 | 개발 속도 50% 감소, 6개월 후 단계 1로 롤백 |
| \"시스템 전체에 일괄 적용\" | 모든 Bounded Context에 CQRS 적용 | 결제 도메인에 결과적 일관성 → 사용자 혼란 |
| \"운영 역량 없이 단계 4\" | 인프라팀 없는 서비스에 Event Sourcing 도입 | 이벤트 스키마 깨지자 과거 재생 실패, 복구 며칠 소요 |
6.3 Martin Fowler의 경고
\"CQRS는 시스템 전체가 아니라 시스템의 특정 부분(DDD 용어로 BoundedContext)에만 사용되어야 한다. 특히 CQRS가 소프트웨어 시스템을 심각한 어려움에 빠뜨린 사례들을 봐 왔다.\" — Martin Fowler
6.4 도입 체크리스트
✓ CQRS 도입 검토 시 6가지 질문
- 읽기:쓰기 비율이 10:1 이상 비대칭인가?
- 조회 요구사항이 정규화로 풀리지 않는가?
- 결과적 일관성을 UX 측면에서 수용 가능한가?
- 운영 인력이 메시지 브로커와 다중 저장소를 다룰 수 있는가?
- 도메인이 CRUD 이상으로 충분히 복잡한가?
- CQRS 적용 범위를 단일 Bounded Context로 한정 가능한가?
🎯 판정 기준
4개 이상에 \"예\"라고 답할 수 있을 때만 도입을 고려하세요. 그보다 적으면 단일 모델로 충분한 경우가 대부분입니다.
📝 자가 평가
평가 1. CQRS가 왜 필요한가?
친구가 \"CQRS가 뭐고 왜 쓰는 거야?\"라고 묻는다고 가정합니다. 3분 안에 설명해 주세요.
좋은 답변: 단일 모델 CRUD의 한계를 짚을 수 있어야 함 (부하 비대칭, 모델 책임 충돌 등). CQRS의 한 줄 정의 포함. 적합/부적합 경우 구분.
평가 2. CQRS 4단계 스펙트럼 자가 적용
현재 다루고 있거나 익숙한 시스템 하나를 떠올려 보세요. 어느 단계가 적합할까요?
좋은 답변: 도메인의 읽기/쓰기 비율과 복잡도를 평가했어야 함. 4단계 중 하나 선택 + 이유. 도입 안 함도 좋은 답.
평가 3. 표준 아키텍처 그리기
종이에 CQRS + NoSQL 표준 아키텍처를 그려 보세요. 결과적 일관성이 만드는 함정 한 가지를 적어 보세요.
좋은 답변: 쓰기/이벤트/읽기 세 영역 분리. 화살표 방향 정확. 일관성 갭과 UX 함정 설명.
📚 참고 자료
- ① Martin Fowler, \"CQRS\" (2011)
https://martinfowler.com/bliki/CQRS.html - ② Microsoft Learn, \"CQRS pattern\"
https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs - ③ Microsoft Learn, \"Event Sourcing pattern\"
https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing - ④ Confluent Developer, \"CQRS\"
https://developer.confluent.io/patterns/compositional-patterns/command-query-responsibility-segregation/
'스파르타 심화 과정' 카테고리의 다른 글
| RAG 이해하기 (0) | 2026.06.09 |
|---|---|
| 모니터링과 부하테스트 (0) | 2026.06.02 |
| spring 과제 (0) | 2026.04.21 |
| 4/20 Spring 기초(2) 특강 (0) | 2026.04.20 |
| spring 특강 1 (0) | 2026.04.20 |
