대부분의 애플리케이션은 데이터를 읽는 빈도가 쓰는 빈도보다 훨씬 높다. 그런데도 Read와 Write를 동일한 모델에 묶어두면 두 작업이 서로의 성능과 복잡도를 끌어내린다. CQRS는 이 두 책임을 명확히 갈라 각자 최적화할 수 있게 해주는 아키텍처 패턴이다.
CQRS는 Command와 Query를 완전히 다른 경로로 처리한다.
CreateOrder, CancelShipment)GetOrderSummary, ListProducts)이 분리 원칙은 Bertrand Meyer의 CQS(Command Query Separation)에서 출발했고, CQRS는 이를 아키텍처 수준으로 확장한다.
┌────────────┐ Command ┌─────────────────┐
│ Client │ ─────────────► │ Write Model │
│ │ │ (도메인 로직) │
│ │ Query └────────┬────────┘
│ │ ─────────────► │ 이벤트/저장
│ │ ┌────────▼────────┐
│ │ ◄────────────── │ Read Model │
└────────────┘ 결과 반환 │ (조회 최적화) │
└─────────────────┘
전통적인 CRUD 아키텍처는 하나의 모델이 읽기와 쓰기를 모두 담당한다.
| 구분 | CRUD (단일 모델) | CQRS (분리 모델) |
|---|---|---|
| 모델 수 | 1개 | 최소 2개 |
| 조회 최적화 | 어려움 | Read Model에 집중 |
| 도메인 복잡도 | Write에 섞임 | Write Model에 격리 |
| 스케일링 | 함께 확장 | 독립 확장 가능 |
Read Model은 조회에 특화된 비정규화 뷰를 가질 수 있다. 복잡한 JOIN 없이 단순 조회가 가능하고, 별도 Read DB(예: Elasticsearch, Redis)로 분리하면 쓰기 부하와 완전히 독립적으로 확장된다.
// Command
interface CreateOrderCommand {
userId: string;
items: { productId: string; quantity: number }[];
}
// Command Handler
class CreateOrderHandler {
constructor(private repo: OrderRepository) {}
async execute(cmd: CreateOrderCommand): Promise<void> {
const order = Order.create(cmd.userId, cmd.items);
await this.repo.save(order);
}
}
// Query
interface GetOrderSummaryQuery {
orderId: string;
}
// Query Handler — 별도 Read DB 조회
class GetOrderSummaryHandler {
constructor(private readDb: ReadDatabase) {}
async execute(query: GetOrderSummaryQuery): Promise<OrderSummaryDto> {
return this.readDb.findOrderSummary(query.orderId);
}
}
Read Model은 도메인 규칙 없이 오직 DTO 반환에만 집중한다.
DTO(Data Transfer Object) — 레이어 간 데이터를 운반하기 위한 단순 객체. 비즈니스 로직을 포함하지 않는다.
CQRS는 Event Sourcing과 자주 함께 쓰이지만 필수 조합은 아니다. Event Sourcing을 적용하면 Write Model이 상태 대신 이벤트 로그를 저장한다. 이 이벤트를 구독해 Read Model을 비동기로 갱신하는 구조가 자연스럽게 만들어진다.
Command → Write Model → OrderCreatedEvent
│
┌─────────▼──────────┐
│ Event Handler │
│ (Read Model 갱신) │
└────────────────────┘
단, 이 경우 Read Model은 최종 일관성(Eventual Consistency) 을 가진다. 명령 직후 조회 시 아직 반영 전일 수 있음을 설계에서 명시해야 한다.
CQRS는 복잡도를 동반한다. 모델이 둘로 늘고, 동기화 로직이 생기며, 팀이 패턴을 이해해야 한다. 다음 조건이 맞을 때 도입을 고려하라.
단순 CRUD 서비스에 CQRS를 적용하면 복잡도만 늘고 이점은 없다.