CQRS 도입 결정 기준 - 조회가 느려졌다고 바로 CQRS를 쓰면 안 되는 이유
CQRS는 강력한 패턴이지만 도입 비용이 높습니다. 조회 성능 문제가 생겼을 때 CQRS가 진짜 답인지, 먼저 해볼 것들이 무엇인지, 어떤 조건이 되면 CQRS로 가는 것이 맞는지 실무 기준으로 정리합니다.
CQRS를 너무 빨리 꺼내는 경우
조회 API가 느려지면 자주 나오는 대화가 있습니다.
“읽기 트래픽이 쓰기보다 훨씬 많아서 CQRS를 도입해야 할 것 같습니다.”
틀린 말은 아닙니다.
하지만 CQRS는 도입하면 시스템이 복잡해집니다.
복잡도를 정당화하려면 그만한 이유가 필요합니다.
CQRS가 해결하는 진짜 문제
CQRS는 명령(쓰기)과 조회(읽기)의 모델을 분리하는 패턴입니다.
이게 가치 있는 상황은 아래와 같습니다.
-
조회 모델이 쓰기 모델과 근본적으로 다를 때
주문 생성은 단건 데이터를 저장하지만, 조회는 여러 테이블을 조인하고 집계해야 한다 -
읽기와 쓰기의 스케일링 요구가 다를 때
쓰기는 트랜잭션 DB에서 처리하고, 읽기는 캐시나 읽기 전용 복제본이 필요하다 -
이벤트 소싱과 함께 쓸 때
상태 저장 방식이 이벤트 스트림이라서 조회를 위한 별도 뷰가 필수다
이 상황이 아니라면 대부분 CQRS 없이 해결할 수 있습니다.
CQRS 전에 먼저 해볼 것들
1. 인덱스를 제대로 걸었는가
조회가 느린 가장 흔한 원인은 인덱스 문제입니다.
-- 느린 조회
SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC;
-- 확인할 것
EXPLAIN SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC;
-- (user_id, status, created_at) 복합 인덱스가 있는가
복합 인덱스 하나가 CQRS보다 먼저입니다.
2. 읽기 복제본을 붙였는가
RDS, PostgreSQL, MySQL 모두 읽기 복제본을 지원합니다.
쓰기는 Primary, 읽기는 Read Replica로 분리하면 쓰기 트래픽이 많은 환경에서 효과가 큽니다.
# Spring 예시 - 읽기/쓰기 DataSource 분리
spring:
datasource:
primary:
url: jdbc:postgresql://primary-host/db
replica:
url: jdbc:postgresql://replica-host/db
이것도 CQRS의 일부라고 볼 수 있지만, 이벤트 동기화 없이 DB 레벨에서 해결됩니다.
3. 조회 전용 뷰나 캐시를 붙였는가
자주 조회되는 집계 데이터는 캐시나 Materialized View로 처리합니다.
-- PostgreSQL Materialized View
CREATE MATERIALIZED VIEW order_summary AS
SELECT
user_id,
COUNT(*) as total_orders,
SUM(amount) as total_amount,
MAX(created_at) as last_order_at
FROM orders
GROUP BY user_id;
-- 주기적 갱신
REFRESH MATERIALIZED VIEW CONCURRENTLY order_summary;
대시보드 같은 집계 조회는 이것만으로도 충분한 경우가 많습니다.
그래도 CQRS가 맞는 상황
위 방법을 써봤는데도 한계가 있다면 CQRS를 검토합니다.
조회 모델이 DB 구조와 너무 다를 때
예를 들어, 사용자 피드를 만들려면 팔로우, 포스트, 좋아요, 댓글 등을 조인해야 하고
이게 페이지 렌더링마다 반복된다면 쓰기 DB를 그대로 쓰는 것이 점점 무거워집니다.
이때는 피드 전용 Read Model (예: Redis, Elasticsearch)을 두고
쓰기 이벤트가 발생할 때 Read Model을 갱신하는 구조가 맞습니다.
[Write] 사용자가 포스트 작성
→ posts 테이블에 저장
→ PostCreatedEvent 발행
[Read Model 갱신]
→ PostCreatedEvent 수신
→ 팔로워들의 피드 캐시에 포스트 추가 (Redis)
→ Elasticsearch 인덱스 갱신
[조회] 피드 API
→ Redis에서 피드 가져오기 (DB 조인 없음)
CQRS 도입 시 반드시 고려할 것
최종 일관성을 수용할 수 있는가
Write Model과 Read Model 사이에는 지연이 생깁니다.
사용자가 글을 쓰고 피드를 바로 보면 아직 반영 안 됐을 수 있습니다.
이 경험이 사용자에게 허용 가능한지 먼저 판단해야 합니다.
Read Model 갱신 실패를 어떻게 처리할 것인가
이벤트 처리가 실패하면 Read Model이 Write Model과 어긋납니다.
Dead Letter Queue, 재시도 정책, 수동 재동기화 절차가 필요합니다.
팀이 이 복잡도를 감당할 수 있는가
CQRS는 테스트 범위, 디버깅 경로, 배포 의존성이 늘어납니다.
팀 규모와 운영 여력에 맞는 결정이 필요합니다.
의사결정 트리
조회 성능 문제 발생
│
├─ 인덱스 최적화로 해결 가능? → Yes → 인덱스부터
│
├─ 읽기 복제본으로 해결 가능? → Yes → Read Replica
│
├─ 캐시/Materialized View로 가능? → Yes → 캐시 레이어
│
└─ 조회 모델이 쓰기 모델과 근본적으로 다른가?
└─ Yes → 최종 일관성 수용 가능한가?
└─ Yes → CQRS 검토
└─ No → 동기 Read Model 갱신 방식 검토
정리
CQRS는 도구입니다.
”읽기가 느리다”는 증상은 여러 원인에서 옵니다.
원인을 특정하지 않고 CQRS를 먼저 꺼내면
해결보다 복잡도가 먼저 올라갑니다.
인덱스 → 복제본 → 캐시 순으로 단계별로 해결하고,
그래도 Read Model 분리가 필요한 이유가 명확할 때 CQRS를 쓰는 것이 맞습니다.