CQRS와 Elasticsearch 읽기/쓰기 분리
CQRS로 변경 비용을 분리하고 Kafka→Elasticsearch 파이프라인에서 최종 일관성·멱등·스키마 리스크를 다루는 방법
요약 (5–7줄)
CQRS는 성능 튜닝 기법이 아니라 변경 비용을 분리하는 아키텍처 선택이다.
쓰기 모델은 “정합성”, 읽기 모델은 “질의 효율”에 최적화된다.
Kafka → Elasticsearch 파이프라인은 지연·누락·중복·스키마 변화라는 구조적 리스크를 내포한다.
CQRS와 이벤트 소싱은 독립 개념이며, 함께 쓰일 수도 분리될 수도 있다.
Elasticsearch는 트랜잭션 DB가 아니라 검색 최적화된 문서 스토어다.
이 구조의 핵심은 “언제 불일치를 허용할 것인가”에 대한 명시적 합의다.
1. 정의
CQRS(Command Query Responsibility Segregation)는
시스템의 쓰기(Command) 와 읽기(Query) 책임을 모델·저장소·확장 전략 차원에서 분리하는 설계다.
분리는 단순히 API를 나누는 것이 아니라, 변경 비용이 전파되는 경로를 끊는 것을 의미한다.
2. 직관
하나의 모델로 쓰기와 읽기를 모두 만족시키려 하면
- 쓰기는 제약 때문에 느려지고
- 읽기는 구조 때문에 비싸진다
CQRS는 “둘 다 적당히”가 아니라
각각을 극단적으로 최적화하는 선택이다.
3. CQRS의 ‘분리’가 의미하는 것
3.1 모델 분리
- Write 모델: 불변식, 검증, 트랜잭션 중심
- Read 모델: 조인 최소화, 질의 패턴 중심
3.2 저장소 분리
- Write: RDB / 트랜잭션 DB
- Read: Elasticsearch / 캐시 / 머티리얼라이즈드 뷰
3.3 배포 분리
- Read 스케일링이 Write에 영향 없음
- 검색 트래픽 폭증 시에도 쓰기 안정성 유지
3.4 팀 경계 분리
- 쓰기 도메인 팀
- 읽기/검색 최적화 팀
CQRS는 조직 구조와 강하게 결합되는 아키텍처다.
4. 작동 원리 (Kafka → Elasticsearch)
1) Write 서비스가 상태 변경을 이벤트로 발행
2) Kafka가 이벤트 로그를 보존
3) Read 파이프라인이 이벤트를 소비
4) Elasticsearch 인덱스를 갱신
5) Query API는 ES만 조회
핵심 전제: 읽기는 항상 쓰기보다 늦다.
5. Kafka → Elasticsearch 파이프라인의 최대 리스크 4가지
5.1 지연 (Latency)
- 인덱싱 파이프라인 정체
- 해결: SLA를 “최종 일관성” 기준으로 명시
5.2 누락 (Loss)
- 소비 실패 + 오프셋 커밋 오류
- 해결: 재처리 가능 구조 + 백필 전략
5.3 중복 (Duplication)
- at-least-once 전달
- 해결: 문서 ID 기준 멱등 업데이트
5.4 스키마 변화
- 이벤트 필드 변경 → 인덱스 매핑 충돌
- 해결: 스키마 버전 관리 + 점진적 매핑
6. 이벤트 소싱과 CQRS의 관계
- 이벤트 소싱: 상태의 근원을 이벤트로 저장
- CQRS: 읽기/쓰기 책임 분리
관계 정리:
- CQRS ≠ 이벤트 소싱
- 이벤트 소싱 없이 CQRS 가능
- 이벤트 소싱 + CQRS는 강한 결합 패턴일 뿐 필수는 아님
실무에서는 CQRS만 도입하고 이벤트는 파이프라인 용도로만 사용하는 경우가 더 많다.
7. Elasticsearch를 Read Store로 쓰는 이유와 한계
7.1 쓰는 이유
- 복잡한 검색 조건
- 정렬·집계·전문 검색
- 문서 단위 조회 최적화
7.2 한계
- 트랜잭션 없음
- 강한 정합성 없음
- 즉시 반영 보장 불가
ES는 DB의 대체재가 아니라 읽기 전용 투영(projection)이다.
8. 운영 관점 핵심 개념
8.1 인덱스 매핑
- 필드 타입은 한 번 정하면 변경 비용 큼
- “미리 넉넉하게”가 아니라 질의 기준으로 최소화
8.2 리인덱싱
- 구조 변경 시 불가피
- 신규 인덱스 생성 → 스위치
8.3 백필 (Backfill)
- 과거 이벤트 재적재
- 재처리 가능한 시스템의 필수 능력
9. 트레이드오프
- 장점: 읽기 확장성, 질의 자유도, 쓰기 안정성
- 비용: 복잡도 증가, 운영 부담, 일시적 불일치
CQRS는 성능 문제가 아니라 변경 압력이 높을 때 의미가 생긴다.
10. 최소 예시
10.1 Toy 예시
- Write 모델:
Order(id, status, total) - Read 모델:
OrderView(id, status, total, customerName)
→ 고객명은 쓰기 모델에 없음
→ 읽기 편의를 위해 투영된 데이터
10.2 실무 예시: 트윗/피드 검색
- Write 최적화:
- 트윗 생성은 빠르고 단순해야 함
- Read 최적화:
- 키워드, 해시태그, 최신순, 인기순 검색
충돌 지점:
- 실시간성 vs 인덱싱 비용
- 해결: “몇 초 지연”을 제품 요구사항으로 명시
11. 실무 함정 → 해결 패턴
| 함정 | 결과 | 해결 패턴 |
|---|---|---|
| CQRS 남용 | 시스템 과복잡 | 변경 압력 기준 도입 |
| ES를 DB처럼 사용 | 데이터 오염 | Read 전용 규율 |
| 재처리 불가 | 장애 시 복구 불능 | 이벤트 기반 백필 |
12. 오해/실수 3개 + 교정
1) “CQRS = 성능 최적화” → 변경 비용 분리
2) “이벤트 소싱 필수” → 독립 개념
3) “ES는 그냥 DB” → 검색 특화 투영소
13. 판단 기준
사용해야 할 때
- 읽기 패턴이 복잡하고 자주 바뀜
- 검색 트래픽이 쓰기를 압도
- 팀/도메인 분리가 필요
쓰지 말아야 할 때
- CRUD 중심 단순 서비스
- 강한 즉시 정합성 필수
- 운영 인력·경험 부족
14. 재학습 체크리스트 (10–14)
- 쓰기/읽기 변경 압력이 다른가?
- 읽기 모델이 쓰기 모델을 오염시키고 있지 않은가?
- 읽기 지연을 허용하는가?
- 이벤트 누락 시 복구 경로가 있는가?
- 중복 처리가 가능한가?
- 스키마 버전 전략이 있는가?
- 리인덱싱 절차가 문서화돼 있는가?
- 백필이 가능한가?
- ES 매핑 변경 비용을 인지하는가?
- 읽기 트래픽 폭증 시 쓰기가 안전한가?
- CQRS 도입 이유를 팀이 합의했는가?
