이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Read Write 분리와 CQRS를 도입하기 전에 어떤 운영 준비가 필요한가?
- 복잡도를 올리는 단계와 멈춰야 하는 신호는 무엇인가?
- CQRS의 운영 복잡도는 성능, 정합성, 장애 격리와 어떤 trade-off를 만드는가?
개요
Read Write 분리와 CQRS는 설계가 성숙해 보이는 선택이다.
하지만 실제로는 복잡도를 빌려 성능과 격리를 얻는 선택이다.
read model, projection worker, outbox, replay, lag alert, schema version, DLQ가 생기면 장애 원인도 늘어난다.
따라서 “가능한가”보다 “운영할 수 있는가”가 먼저다.
복잡도를 감당할 수 없다면 단일 모델, 인덱스, 캐시, read replica에서 멈추는 것이 더 좋은 설계일 수 있다.
복잡도 증가 경로
복잡도는 한 번에 뛰지 말고 단계적으로 올린다.
| 단계 | 구조 | 얻는 것 | 새 비용 | 멈출 조건 |
|---|---|---|---|---|
| 1 | 단일 모델 | 구현 단순, 강한 정합성 | 복잡한 조회가 느림 | 트래픽 작고 모델 동일 |
| 2 | 인덱스와 pagination | 조회 개선 | write 비용 증가 | 쿼리 몇 개만 문제 |
| 3 | Cache | 반복 조회 속도 | invalidation과 stale | TTL로 설명 가능 |
| 4 | Read replica | 읽기 부하 분산 | replica lag와 장애 전환 | 스키마 그대로 조회 가능 |
| 5 | Materialized view | 집계 조회 단순화 | refresh와 lock 고려 | DB 내부 집계로 충분 |
| 6 | Projection | 조회 모델 독립 | 이벤트, replay, rebuild | 화면 모델이 원본과 다름 |
| 7 | CQRS | command/query 책임 분리 | 팀, 배포, 운영 절차 증가 | 도메인과 트래픽이 충분히 큼 |
좋은 설계는 가장 복잡한 구조를 고르는 것이 아니다.
요구사항을 만족하는 가장 단순한 구조에서 멈추는 것이다.
도입 전 체크리스트
CQRS나 projection을 도입하려면 다음 질문에 답해야 한다.
- 원천 데이터의 owner는 누구인가?
- read model은 원천에서 다시 만들 수 있는가?
- rebuild 시간은 목표 복구 시간 안에 들어오는가?
- event schema version은 어떻게 올리는가?
- old projector와 new event가 동시에 존재할 수 있는가?
- DLQ 이벤트는 누가 언제 재처리하는가?
- lag가 커지면 사용자는 무엇을 보게 되는가?
- 운영자는 어떤 metric과 alert를 보는가?
- read model 장애 때 command API는 계속 받을 수 있는가?
- 비용이 증가했을 때 줄일 수 있는 기능은 무엇인가?
이 중 절반 이상 답하지 못하면 CQRS보다 단순한 대안을 먼저 선택한다.
운영 준비 예시
관리자 대시보드용 projection을 운영한다고 하자.
projection lag SLO = 60s
read model rebuild target = 2h
event retention = 14d
DLQ first response = 15m
schema compatibility window = 2 versions
owner = order-platform team이 값들은 문서 장식이 아니다.
장애가 났을 때 어떤 대응이 가능한지 결정한다.
event retention이 1일인데 rebuild가 2일 걸리면 원천 이벤트만으로 복구할 수 없다.
schema compatibility window가 없으면 배포 순서가 틀렸을 때 projector가 이벤트를 해석하지 못한다.
배포와 마이그레이션
Read model schema를 바꿀 때는 기존 table을 바로 수정하는 것보다 병행 전환이 안전하다.
- 새 read model schema를 추가한다.
- projector가 old/new model을 같이 채우게 한다.
- backfill이나 replay로 새 model을 채운다.
- old/new count와 주요 지표를 비교한다.
- query API를 새 model로 전환한다.
- 문제가 없으면 old model을 제거한다.
이 절차는 운영 복잡도를 늘린다.
하지만 read model이 잘못 만들어졌을 때 되돌아갈 수 있는 길을 만든다.
장애 Runbook
Projection lag가 급증했을 때 runbook은 구체적이어야 한다.
증상: projection.source_lag.seconds > 180
1. 실패 이벤트가 한 종류에 몰렸는지 확인한다.
2. DLQ 증가 여부와 첫 실패 eventId를 확인한다.
3. 특정 aggregateId가 계속 실패하면 격리 목록에 둔다.
4. worker scale out이 효과 있는 병목인지 DB write 병목인지 확인한다.
5. 사용자 화면에 delayed 상태를 노출한다.
6. lag가 600초를 넘으면 관리자 통계 일부를 마지막 안전 시점으로 고정한다.
7. 수정 후 격리 이벤트를 재처리하고 count/checksum을 비교한다.runbook 없이 worker 수만 늘리면 실패 이벤트를 더 빠르게 반복 처리해 DB와 broker를 더 압박할 수 있다.
비용 계산
복잡도는 사람과 인프라 비용으로 나타난다.
read model row = 200,000,000
average row size = 800 bytes
index overhead = 1.5x
storage = 200,000,000 * 800 * 1.5 = 240GB
daily events = 80,000,000
replay speed = 20,000 events/sec
full replay = 80,000,000 / 20,000 = 약 67분이 계산은 대략이어도 좋다.
중요한 것은 read model이 무료가 아니라는 점을 설계 단계에서 드러내는 것이다.
저장소, 네트워크, worker, observability, on-call 시간이 모두 추가된다.
코드와 설정의 최소 기준
복잡도를 감당하려면 코드에도 경계가 보여야 한다.
public interface ProjectionHandler<T extends DomainEvent> {
boolean supports(String eventType, int schemaVersion);
void apply(T event);
}projection:
order-summary:
lag-warning-seconds: 60
lag-critical-seconds: 180
max-retry: 5
dlq-topic: order-summary-dlq
rebuild-batch-size: 1000설정값은 운영자가 조정할 수 있어야 한다.
코드에 retry 횟수와 lag 기준이 박혀 있으면 장애 중 대응이 느려진다.
도입하지 말아야 할 신호
- 팀이 read model을 원본 데이터처럼 수정하려고 한다.
- 이벤트 재처리보다 수동 SQL 패치를 더 자주 쓴다.
- lag를 측정하지 않는데 stale data 불만이 나온다.
- schema version 없이 이벤트 payload를 자주 바꾼다.
- 장애 때 owner가 command 팀인지 query 팀인지 불명확하다.
- 단순 CRUD인데 아키텍처 일관성을 이유로 모든 기능에 CQRS를 적용한다.
이 신호가 있으면 CQRS를 넓히기보다 경계를 줄이고 단순화해야 한다.
개인 프로젝트 기준
개인 프로젝트에서는 운영 체계를 모두 갖추기 어렵다.
대신 다음 기준으로 현실적인 선을 잡는다.
- 단일 DB와 query DTO 분리로 시작한다.
- 캐시나 read model을 쓰면 stale 허용 시간을 README나 문서에 남긴다.
- rebuild는 수동 스크립트라도 준비한다.
- event schema는 최소한 version 필드를 둔다.
- 장애 시나리오를 포트폴리오 설명에 포함한다.
기업 운영 기준
기업에서는 다음이 준비되어야 CQRS를 확장할 수 있다.
- owner team과 on-call이 분명하다.
- lag, failure, rebuild 지표가 dashboard와 alert에 연결되어 있다.
- schema 변경 절차와 호환성 테스트가 있다.
- replay와 backfill이 운영 트래픽과 격리된다.
- DLQ 처리 책임과 SLA가 있다.
- 장애 때 read 기능을 degraded로 낮출 수 있다.
- 비용 증가를 추적하는 저장소와 worker capacity 계획이 있다.
성능 문제 하나를 해결하려고 이 모든 책임을 새로 만들 가치가 있는지 판단해야 한다.
확인 질문
확인 질문
- 확인 질문: CQRS를 도입하기 전에 “운영할 수 있는가”를 먼저 묻는 이유는 무엇인가?
- 답변: CQRS는 읽기 성능과 장애 격리를 얻는 대신 lag, replay, rebuild, schema version, DLQ 운영을 새로 만들기 때문이다.
- 확인 질문: 복잡도를 단계적으로 올려야 하는 이유는 무엇인가?
- 답변: 인덱스, 캐시, read replica로 해결 가능한 문제에 projection과 CQRS를 도입하면 불필요한 장애 원인과 운영 비용이 생기기 때문이다.
- 확인 질문: 이 문서의 선택이 바꾸는 설계 축은 무엇인가?
- 답변: 성능과 장애 격리는 좋아질 수 있지만, 정합성 보장 방식과 운영 복잡도는 크게 증가한다.