CQRS (명령 조회 책임 분리) 읽기 쓰기 분리 스케일 아웃
핵심 인사이트 (3줄 요약)
- 본질: CQRS(Command Query Responsibility Segregation)는 시스템의 상태를 변경하는 명령(Command, Write) 모델과 시스템의 상태를 반환하는 조회(Query, Read) 모델을 완벽하게 분리하는 아키텍처 패턴이다.
- 가치: 읽기와 쓰기의 트래픽 비율(보통 100:1 이상)과 요구되는 성능 특성이 완전히 다름에도 불구하고 하나의 DB와 도메인 모델을 공유하던 전통적 구조의 병목을 해소하여, 읽기 전용 DB의 극단적인 스케일 아웃(Scale-out)을 가능하게 한다.
- 융합: CQRS는 단독으로 쓰이기보다 이벤트 소싱(Event Sourcing) 및 **이벤트 주도 아키텍처(EDA, Kafka)**와 결합되어, 쓰기 DB의 변경 사항을 읽기 DB로 비동기 동기화(Projection)하는 방식으로 구현되며, 이 과정에서 **최종 일관성(Eventual Consistency)**의 수용이 필수적이다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 전통적인 아키텍처는 하나의 엔티티 객체가 데이터베이스에 데이터를 저장(Insert/Update)하고 조회(Select)하는 역할을 모두 수행한다. CQRS는 이를 분리하여 CUD(명령) 전용 모델과 R(조회) 전용 모델로 나누고, 심지어 물리적인 데이터베이스까지 두 개로 쪼개는 패턴이다.
-
필요성: 웹 서비스의 트래픽은 99%가 읽기(조회)이고 1%만이 쓰기(명령)다. 복잡한 비즈니스 로직(정합성 검증, Lock)이 필요한 쓰기 작업과, 단순하지만 매우 빨라야 하고 여러 테이블을 조인(Join)해야 하는 읽기 작업이 하나의 RDBMS를 공유하면, 테이블 인덱스 설계의 충돌(쓰기를 위해 인덱스를 지우면 읽기가 느려지고, 읽기를 위해 인덱스를 추가하면 쓰기가 느려짐)과 커넥션 풀 고갈이 발생한다. 이를 물리적으로 분리하지 않고서는 대규모 트래픽을 견딜 수 없다.
-
💡 비유: 도서관에서 책을 빌려 가는 '대출 데스크(Command)'와 책을 찾는 '검색용 PC(Query)'가 하나로 합쳐져 있으면, 한 명의 사서가 대출 처리와 검색을 모두 해줘야 해서 엄청난 줄이 생깁니다. 대출 데스크는 1개만 두고, 검색용 PC는 100대를 깔아두어 역할을 완벽히 분리하는 것이 CQRS입니다.
-
등장 배경 및 발전 과정:
- CQS (Command Query Separation): 버트란드 마이어가 제안한 객체 지향 원칙으로, "상태를 변경하는 메서드는 값을 반환하면 안 되고, 값을 반환하는 메서드는 상태를 변경하면 안 된다"는 단일 객체 내의 코드 레벨 분리 원칙이었다.
- CQRS의 등장: 그렉 영(Greg Young)은 CQS 원칙을 아키텍처 레벨로 확장하여 모델 자체를 분리하는 CQRS를 창시했다.
- 이벤트 소싱 및 MSA와의 결합: 마이크로서비스 환경에서 각 서비스가 자신의 데이터를 독점(Database per Service)하면서, 타 서비스의 데이터를 조회하기 위해 API를 호출하면 성능이 저하되는 문제가 발생했다. 이를 해결하기 위해 타 서비스의 이벤트를 구독하여 자신만의 읽기 DB를 구성하는 CQRS 모델이 MSA의 핵심 패턴으로 정착했다.
-
📢 섹션 요약 비유: 요리사(명령 모델)는 주방에서 복잡하게 요리를 만들고, 웨이터(조회 모델)는 만들어진 요리를 손님에게 빠르게 서빙만 하도록 주방과 홀의 동선을 완벽히 분리하는 것과 같습니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
구성 요소
| 요소명 | 역할 | 내부 동작 | 관련 기술 | 비유 |
|---|---|---|---|---|
| Command Model (쓰기) | 도메인 로직 검증 및 상태 변경 | ACID 트랜잭션, 정합성 보장, 이벤트 발행 | RDBMS, Aggregate Root | 도서 대출 승인 데스크 |
| Query Model (읽기) | 화면 렌더링에 최적화된 데이터 반환 | 복잡한 로직 없이 DTO/View 모델 즉시 반환 | NoSQL(MongoDB, Elasticsearch) | 빠르고 단순한 도서 검색기 |
| Event Broker | 쓰기 DB의 변경 사항을 읽기 DB로 전달 | 비동기 메시징 처리 (Pub/Sub) | Kafka, RabbitMQ, AWS SQS | 대출 내역을 검색기에 전달하는 메신저 |
| Projector (투영기) | 발생한 이벤트를 읽기 모델 스키마에 맞게 변환하여 저장 | 뷰(View) 빌드 및 역정규화(Denormalization) | Consumer 애플리케이션 | 메세지를 받아 검색기 화면을 업데이트 |
CQRS 아키텍처 모델의 3단계 진화
CQRS는 분리하는 수준에 따라 3가지 단계로 구현할 수 있다.
- Level 1: 코드 레벨 분리 (단일 DB)
- 물리적인 DB는 하나지만, 코드의 계층(Service/Repository)을 Command용과 Query용으로 분리한다. (예: JpaRepository는 쓰기에 쓰고, Querydsl은 읽기에 사용)
- Level 2: 물리적 DB 분리 (Write RDBMS / Read NoSQL)
- 쓰기 전용 DB와 읽기 전용 DB를 분리한다. 쓰기 트랜잭션이 완료되면 메시지 큐를 통해 읽기 DB를 업데이트한다. (결과적 일관성 수용)
- Level 3: 이벤트 소싱 (Event Sourcing) 결합
- 쓰기 DB 자체를 없애고 모든 상태 변경을 '이벤트'로만 저장(Event Store)한 뒤, 이를 바탕으로 읽기 DB를 무한대로 찍어낸다.
┌───────────────────────────────────────────────────────────────┐
│ 물리적 DB가 분리된 CQRS 아키텍처 (Level 2~3 수준) │
├───────────────────────────────────────────────────────────────┤
│ │
│ [ Client (Web/App) ] │
│ │ ▲ │
│ (1) │ Command │ (5) Query │
│ ▼ │ │
│ ┌───────────────┐ │ ┌───────────────┐ │
│ │ Command API │ │ │ Query API │ │
│ │ (명령 서비스) │ │ │ (조회 서비스) │ │
│ └──────┬────────┘ │ └───────▲───────┘ │
│ │ │ │ │
│ (2) │ Write │ │ (4) Read │
│ ▼ │ │ │
│ ┌───────────────┐ │ ┌───────┴───────┐ │
│ │ Write DB │ │ │ Read DB │ │
│ │ (RDBMS/Event) │ │ │ (MongoDB/ES) │ │
│ └──────┬────────┘ │ └───────▲───────┘ │
│ │ │ │ │
│ │ │ │ │
│ (3) │ Event Publish (3) │ Event Consume │
│ ▼ │ │
│ [ Message Broker (Kafka / RabbitMQ) ] │
│ │
│ ▶ 읽기 DB는 조인(Join)이 필요 없도록 화면에 뿌릴 모양 그대로 저장! │
│ (역정규화, Denormalization). 따라서 Read 속도가 O(1) 수준임. │
└───────────────────────────────────────────────────────────────┘
[다이어그램 해설] 클라이언트가 상태 변경(Command)을 요청하면 Command API가 비즈니스 로직을 검증한 뒤 Write DB에 저장하고, 변경되었다는 '이벤트(Event)'를 Kafka 같은 브로커에 발행한다. 쓰기 트랜잭션은 여기서 즉시 종료된다. 이후 뒤편에서 비동기로 도는 Projector(또는 Query API의 Consumer)가 이벤트를 가져와 Read DB에 데이터를 밀어 넣는다. 이때 Read DB는 철저하게 UI 화면에 뿌려질 모양 그대로 미리 조인(Join)되어 평탄화된(역정규화된) 상태로 저장된다. 클라이언트가 조회를 요청하면 Query API는 복잡한 로직 없이 Read DB에서 1대 1 매핑된 데이터를 그대로 퍼다 주기만 하면 되므로 응답 속도가 극대화된다.
Ⅲ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 마이크로서비스 간의 복잡한 Join 문제: MSA로 분리된 이커머스에서 "내 주문 내역" 화면을 보여주려면
Order서비스,User서비스,Product서비스,Delivery서비스의 데이터가 모두 필요하다. API 게이트웨이나 BFF(Backend For Frontend)에서 4개의 API를 각각 호출하여 메모리에서 조립(API Composition)하려니 네트워크 레이턴시가 커지고 타임아웃이 빈발하는 상황.- 판단: API 조립(Composition) 패턴은 조인 대상이 많아질수록 성능이 기하급수적으로 저하된다.
- 해결책:
OrderHistory View라는 읽기 전용 CQRS 서비스를 하나 새로 만든다. 이 서비스는 4개 서비스가 발행하는 모든 이벤트를 Kafka로 구독(Subscribe)하여, 자체 MongoDB에 "내 주문 내역" 화면에 딱 맞는 거대한 단일 문서(Document)로 역정규화하여 저장(Materialized View)해둔다. 클라이언트는 이 단일 View DB만 O(1)로 조회하면 끝난다.
-
시나리오 — 최종 일관성(Eventual Consistency)으로 인한 사용자 불만: CQRS 적용 후, 사용자가 글을 작성(Submit)하고 곧바로 자신이 쓴 글을 조회(Query)했는데, Kafka 처리 지연으로 아직 Read DB에 반영되지 않아 "내가 쓴 글이 안 보여요"라는 클레임이 발생하는 상황.
- 판단: CQRS의 가장 큰 약점인 읽기/쓰기 간의 '지연 시간(Replication Lag)' 문제다.
- 해결책: 백엔드의 완벽한 실시간 동기화는 물리적으로 불가능하므로, 프론트엔드 단에서 **낙관적 UI 업데이트(Optimistic UI Update)**를 적용한다. 글 작성이 성공하면 백엔드 조회를 기다리지 않고 프론트엔드 로컬 스토어의 데이터를 먼저 업데이트하여 화면에 띄워주고, 백엔드의 최종 일관성은 뒤에서 맞춰지도록 사용자 경험(UX)으로 기술적 한계를 커버한다.
도입 체크리스트
- 비즈니스적: 시스템의 읽기/쓰기 비율이 어떻게 되는가? (조회 트래픽이 압도적으로 많지 않거나, 도메인이 단순한 CRUD에 불과하다면 CQRS는 득보다 실이 많은 오버엔지니어링이다.)
- 아키텍처적: 화면에 즉각적으로 데이터가 보이지 않아도 되는 시간(Replication Lag, 통상 수백 밀리초)을 비즈니스 부서와 협의하고 용인받았는가? (최종 일관성 수용)
안티패턴
- 단순 CRUD 도메인에 물리적 CQRS 강제 적용: 게시판 공지사항처럼 단순히 읽고 쓰는 단순 도메인에까지 Kafka와 MongoDB를 도입하여 CQRS를 강제 적용하는 패턴. 인프라 비용과 개발자의 인지 부하만 가중시키고 아무런 성능적 이득을 얻지 못한다.
Ⅳ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 모놀리식 단일 DB | CQRS (명령/조회 물리적 분리) | 개선 효과 |
|---|---|---|---|
| 정량 | 트래픽 폭주 시 DB 락 및 커넥션 고갈 | 조회 서버(Read DB)만 무한 스케일 아웃 | 읽기 처리량(Read Throughput) 10배~100배 증가 |
| 정량 | 화면 구성을 위한 다중 테이블 Join 쿼리 | O(1) 조회를 위한 역정규화 Document 구성 | API 응답 시간 수십 밀리초 이내로 단축 |
| 정성 | 쓰기 로직과 읽기 로직이 한 코드에 섞임 | Command와 Query 모델의 완벽한 분리 | 코드 가독성 향상 및 도메인 로직 순수성 보장 |
CQRS는 "모든 데이터베이스의 역할은 하나여야 한다"는 고정관념을 깨부수는 패턴이다. 기술사는 트래픽의 비대칭성을 분석하여, 데이터의 '정합성'을 책임지는 진실의 원천(Write DB)과 '퍼포먼스'를 책임지는 복제본(Read DB)을 과감히 분리하는 아키텍처를 설계해야 한다. 단, 이 과정에서 파생되는 이벤트 브로커 관리와 데이터 불일치 해소(최종 일관성)의 복잡성을 통제할 역량이 필수적이다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 이벤트 소싱 (Event Sourcing) | CQRS의 Command 모델을 극단적으로 최적화한 형태로, 쓰기를 오직 Event Append로만 처리하며, 이 이벤트를 이용해 Query 모델을 프로젝션(만들어냄)한다. |
| Materialized View (구체화된 뷰) | Query DB에 데이터를 저장하는 방식으로, Join 연산을 미리 수행하여 화면에 출력할 형태 그대로 데이터를 물리적으로 저장해두는 기법이다. |
| 최종 일관성 (Eventual Consistency) | Command DB와 Query DB 사이의 데이터 복제 시점 차이로 인해 필연적으로 발생하는 일시적 데이터 불일치 상태를 의미한다. |
| API Composition (조립) | MSA에서 여러 서비스의 데이터를 모아서 보여줄 때 사용하는 대안 패턴이나, 조인 대상이 많아지면 성능이 저하되어 CQRS로 전환되는 원인이 된다. |
| 도메인 주도 설계 (DDD) | 애그리게이트(Aggregate)를 설계할 때 Command를 위한 도메인 모델과 Query를 위한 DTO 모델을 분리하는 사상적 근거를 제공한다. |
👶 어린이를 위한 3줄 비유 설명
- 햄버거 가게에서 '주문받고 요리하는 직원'과 '완성된 햄버거를 손님에게 나눠주는 직원'이 한 명이라면 줄이 엄청나게 길어지겠죠?
- 그래서 요리하는 주방(쓰기/Command)은 하나만 두고, 완성된 햄버거를 빠르게 집어갈 수 있는 진열대와 서빙 직원(읽기/Query)은 여러 개로 늘려놓는 거예요.
- 이렇게 복잡하게 상태를 바꾸는 일과 단순히 결과를 보여주는 일을 완벽하게 나눠서, 각자 제일 잘하는 방식으로 일하게 만드는 것을 'CQRS'라고 한답니다!