CQRS란?
기존 단일 모델 방식의 한계
일반적인 애플리케이션에서는 하나의 데이터베이스와 모델을 사용해 읽기와 쓰기를 모두 처리한다.
서비스가 커질수록 다음과 같은 문제가 발생할 수 있다.
- 복잡한 JOIN: 정규화로 데이터가 여러 테이블에 나뉘어 있어 조회 시 여러 JOIN이 필요할 수 있다.
- 읽기와 쓰기의 요구사항 차이: 쓰기는 정합성과 트랜잭션이 중요하고, 읽기는 빠르고 간단한 조회가 중요하다.
- 스키마 변경 비용 증가: 하나의 모델을 읽기와 쓰기가 함께 사용하면 변경 시 영향을 받는 범위가 커질 수 있다.
CQRS란?
CQRS(Command Query Responsibility Segregation)는 데이터의 쓰기(Command)와 읽기(Query) 책임을 분리하는 패턴이다.
- Command: Create, Update, Delete
- Query: Read
이를 통해 Command는 데이터 정합성과 트랜잭션에 집중하고, Query는 조회 성능에 맞게 별도로 최적화할 수 있다.
CQRS의 4단계
CQRS는 읽기와 쓰기를 분리하는 정도에 따라 다음과 같이 발전시킬 수 있다.
1단계 - 모델만 분리
하나의 데이터베이스를 사용하지만 Command 모델과 Query 모델을 분리한다.
Command Model ─┐
├─ RDBMS
Query Model ───┘
저장소는 동일하기 때문에 구조가 비교적 단순하면서도 읽기와 쓰기의 책임을 분리할 수 있다.
2단계 - 읽기 복제본 분리
쓰기 DB와 읽기용 복제본을 분리한다.
Command → Primary DB
│
Replication
↓
Query → Read Replica
Command는 Primary DB에서 처리하고 Query는 복제된 Read Replica에서 처리한다.
읽기 요청이 많을 때 조회 부하를 분산할 수 있다는 장점이 있다.
다만 복제에는 시간이 필요하기 때문에 Primary DB와 Read Replica 사이에 일시적인 데이터 차이가 발생할 수 있다.
3단계 - 이중 DB 분리
Command와 Query가 서로 다른 데이터베이스를 사용한다.
Command → RDBMS
│
동기화
↓
Query → NoSQL
예를 들어 쓰기 측에서는 트랜잭션과 데이터 정합성이 중요한 MySQL과 같은 RDBMS를 사용하고, 읽기 측에서는 빠른 조회를 위한 Redis나 조회 형태에 맞게 데이터를 저장할 수 있는 MongoDB와 같은 NoSQL을 사용할 수 있다.
이때 쓰기 DB에서 발생한 변경 사항을 읽기 DB에 전달하기 위해 Kafka와 같은 메시지 브로커를 활용할 수 있다.
Command → MySQL(RDBMS)
↓
Kafka
↓
Query → Redis / MongoDB(NoSQL)
즉, RDBMS는 쓰기와 데이터 정합성에 집중하고, Kafka는 데이터 변경 이벤트를 전달하며, Redis나 MongoDB와 같은 NoSQL은 조회에 최적화된 데이터를 제공하는 형태로 구성할 수 있다.
4단계 - Event Sourcing + 다중 View
Command 측의 현재 상태만 저장하는 것이 아니라 발생한 이벤트 자체를 Event Store에 저장한다.
Command
↓
Event Store
│
├─→ 주문 조회 View
├─→ 통계 View
└─→ 검색 View
예를 들어 주문 상태가 변경되었다면 최종 상태만 저장하는 것이 아니라 다음과 같은 이벤트를 저장한다.
OrderCreated
PaymentCompleted
OrderShipped
각 Query 모델은 필요한 이벤트를 바탕으로 자신에게 필요한 형태의 View(Read Model)를 구성한다.
따라서 하나의 이벤트 흐름을 이용해 목적이 서로 다른 여러 조회 모델을 만들 수 있다.
정리
단계구조핵심
| 1단계 | 모델 분리 | Command / Query 모델 분리 |
| 2단계 | 읽기 복제본 | Primary / Read Replica 분리 |
| 3단계 | 이중 DB | RDBMS / NoSQL 등 저장소 분리 |
| 4단계 | Event Sourcing | Event Store + 다중 View |
NoSQL이란?
NoSQL은 관계형 데이터베이스가 아닌 다양한 형태의 데이터베이스를 포괄하는 개념이다.
| Document | MongoDB | JSON 형태 데이터, 조회 모델 |
| Key-Value | Redis | 캐시, 세션, 빠른 조회 |
| Wide-Column | Cassandra | 대규모 데이터, 로그 |
| Graph | Neo4j | 관계 데이터, SNS |
RDBMS와 NoSQL
| 구조 | Table | 다양한 데이터 구조 |
| 스키마 | 비교적 엄격 | 비교적 유연 |
| 관계 | JOIN 중심 | 비정규화 활용 가능 |
| 장점 | 정합성, 트랜잭션 | 유연성, 확장성 |
| 예시 | MySQL | MongoDB, Redis |
CQRS에서는 Command는 RDBMS, Query는 NoSQL처럼 서로 다른 DB를 사용할 수도 있다.
Query 전용 DB에 화면에서 필요한 데이터를 미리 구성하면 복잡한 JOIN을 줄이고 조회에 최적화된 구조를 만들 수 있다.
Consistency Gap
CQRS에서 Write DB와 Read DB를 분리하면 두 DB가 항상 동시에 변경되는 것은 아니다.
Write DB 저장
↓
이벤트 전달
↓
Read DB 반영
Write DB에는 변경됐지만 아직 Read DB에는 반영되지 않은 짧은 시간이 발생할 수 있다.
이를 Consistency Gap이라고 한다.
따라서 DB까지 분리하는 CQRS 3단계 이상부터 중요하게 고려해야 하는 문제이다.
일정 시간이 지나 두 데이터가 같아지는 방식을 최종적 일관성(Eventual Consistency)이라고 한다.
Write DB와 Read DB 동기화
대표적으로 두 가지 방법이 있다.
1. 이벤트 발행 + Outbox
애플리케이션에서 변경 사항을 도메인 이벤트로 발행한다.
OrderPlaced
PaymentApproved
OrderCancelled
비즈니스 의미가 명확하다는 장점이 있지만 이벤트를 생성하고 관리하는 코드가 필요하다.
2. CDC(Change Data Capture)
DB의 변경 로그를 감지하여 이벤트로 전달한다.
MySQL → binlog → Debezium → Kafka → Read DB
애플리케이션 코드 변경을 줄일 수 있어 기존 시스템에 적용하기 좋지만, 도메인 의미가 부족하다는 단점이 있다.
Event Sourcing
CQRS와 Event Sourcing은 서로 다른 패턴이다.
- CQRS: 읽기와 쓰기의 책임을 분리한다.
- Event Sourcing: 현재 상태 대신 상태를 만들어낸 이벤트를 저장한다.
일반적인 방식은 현재 상태를 저장한다.
장바구니
상품 A : 1개
상품 B : 1개
Event Sourcing은 상태가 변경된 과정을 저장한다.
상품 A 추가
상품 A 추가
상품 B 추가
상품 A 제거
이 이벤트들을 순서대로 적용하면 현재 상태를 만들 수 있다.
일반적인 방식 Event Sourcing
| 일반적인 방식 | Event Sourcing |
| 현재 상태 저장 | 이벤트 저장 |
| 과거 과정 확인 어려움 | 변경 과정 추적 가능 |
| 구조가 단순 | 구조가 복잡 |
| 일반적인 CRUD에 적합 | 이력 추적이 중요한 시스템에 유용 |
즉, CQRS는 읽기/쓰기의 분리, Event Sourcing은 상태 저장 방식의 변화라고 구분하면 된다.