Post

CQRS 개념 알아보기

들어가며

원티드에서 무료로 CQRS에 대한 프리온보딩 행사를 주최해주었다.
신입으로서 CQRS를 익히고 프로젝트에 적용해본 경험이 있으면 완벽한 스펙이 되겠다 싶었는데, 마침 이에 대한 챌린지를 열어준 원티드하규태 강사님께 감사드린다.
강의자료는 공개할 수 없지만 진행했던 실습코드는 아래 링크에서 확인 가능하다.
https://github.com/hagyutae/wanted-preonboarding-challenge-backend-31


막상 들어보니 생각했던 것 이상으로 시스템 설계가 복잡했다.
그래서 프로젝트에 적용할 엄두가 잘 안나긴 하지만, 개념들을 먼저 정리하고 추후에 프로젝트에 적용하거나 면접에서 제대로 답할 수 있도록 정리하고자 한다.
CQRS는 들어가면 끝이 없기 때문에, 단순 정리만 했음에 유의하기 바란다.


CQRS란?

Command Query Responsibiliy Segregation으로, 직역하면 쓰기와 읽기의 책임을 분리한다 라는 뜻이다.

보통 분리라는 표현을 쓸 때, Separation이라는 용어를 쓰는데 이는 모듈/소도구 레벨의 분리를 뜻하고, Segregation은 시스템 레벨의 분리를 뜻한다고 한다.

쓰기와 읽기를 분리하면, 각 목적에 맞게 모델을 독립적으로 최적화할 수 있고, 읽기 비율이 극단적으로 높은 시스템에서 높은 성능을 보인다.


CQRS 단계

정확히 읽기 DB와 쓰기 DB를 분리하는 게 CQRS가 아니라 코드 레벨에서 분리하는 것도 CQRS의 일종이라 볼 수 있다.

  1. 코드 수준 분리(CQS)
    읽기 전용 클래스, 쓰기 전용 클래스를 분리한다.
    클린코드를 지향하는 사람이라면, 서비스 계층에서 분리하는 형식으로 이미 경험해봤을 것이다.

  2. 모델 수준 분리
    동일한 DB를 사용하면서 다른 테이블/스키마를 사용한다.
    Query용 비정규화 테이블을 분리하는 것이다.

  3. 저장소 수준 분리
    CQRS의 정석적인 형태로, Command 모델과 Query 모델을 완전히 다른 구조/스키마로 설계하는 것이다.
    예시로 쓰기 모델은 정규화된 RDB를 사용하고, 읽기 모델은 역정규화된 MongoDB 등을 사용하는 것이다.


읽기 전용 DB(Read Replica)와의 차이

가장 큰 차이는 읽기 복제본은 마스터 DB의 구조 그대로 사용해야 하지만, CQRS는 둘의 모델을 다르게 두어도 된다는 것이다.
또한 읽기 복제본은 생각보다 성능 향상이 크진 않다고 한다.


CQRS의 핵심 원칙

  • 단일 책임 원칙 적용: 읽기와 쓰기라는 서로 다른 책임의 시스템적 분리
  • 목적 최적화: 각 모델을 해당 목적에 맞게 최적화
  • 느슨한 결합: Command와 Query 간 직접적 의존성 제거
  • 필요한 수준만큼만 적용: 복잡성과 비즈니스 가치를 고려한 적용 범위 선택


언제 써야 할까?

  1. 10:1 이상으로 읽기 비율이 높을 때
  2. 다양한 조회 요구사항이 있을 때
  3. 읽기 작업의 독립적 확장이 필요할 때


Command와 Query의 본질적인 차이

Command

  • 중복 제거를 위해 정규화를 수행한다.
  • 무결성을 달성해야 한다.
  • 트랜잭션을 통해 데이터 일관성을 보장해야 한다.

→ RDBMS가 적합하다.


Query

  • 호출 횟수가 많기 때문에 높은 트래픽에 대응되어야 한다.
  • 빠른 응답 시간이 필요하다.
  • 일관성이 필요없다.

    Command 모델과의 일관성이 중요하다.(읽기 일관성)

→ 분산형 DB, Query 패턴에 최적화된 DB가 적합하다.


모델 성능 최적화

Command와 Query의 차이에 따라 각 모델은 다음과 같은 방향으로 성능을 최적화한다.

Command

  • 정규화된 모델: 데이터 중복 최소화를 통한 업데이트 효율성
  • 배치 처리: 여러 개의 작은 쓰기 작업을 묶어서 효율적으로 처리
  • 이벤트 드리븐: 시스템 상태 변경을 이벤트로 표현하여 느슨한 결합


Query

  • 반정규화: 조인 최소화를 위한 데이터 중복 허용
  • 목적별 특화 인덱스: 다양한 조회 패턴에 최적화된 인덱스 전략
  • 데이터 분산 저장: 단일 파티션 부하 감소 및 병렬 처리 성능 향상


CQRS 단점

  • 모델이 분리되어 전체 시스템의 복잡도가 증가한다.
  • Command에서 Query로 데이터 동기화를 위한 추가 장치가 필요하다.
  • 비동기 동기화로 인한 일시적 데이터 불일치가 발생할 수 있다.
  • 두 개의 독립적인 모델 개발 및 테스트가 필요하다.


모델 간 데이터 동기화

읽기 일관성을 달성하기 위해서, 데이터 동기화 작업이 필수적이다.
사실상 CQRS에서 가장 복잡한 부분이며, 이 과정에서 파생되는 어려움이 많지만 여기서는 다루지 않는다.
크게 어플리케이션 레벨 동기화, 메시지 큐/이벤트 스트림, CDC 방식이 있으며 그 외에 데이터베이스 트리거, 배치 프로세스 방식도 있다.


어플리케이션 레벨 동기화

Command 처리 후 직접 Query 모델을 업데이트한다.
애플리케이션 로직에서 강하게 결합되고, 읽기 동기화가 실패하면 전체 작업이 실패하게 된다.


메시지 큐/이벤트 스트림

Command 처리 후 도메인 이벤트를 발행하면 메시지 큐를 통해 Query 모델로 전달되어 동기화된다.
consumer에서 장애가 발생하면 Query 모델이 누락될 수 있다.


Change Data Capture(CDC)

DB의 변경 로그를 감지해 외부 시스템에서 Query 모델을 갱신한다.
코드 변경이 없어 결합도가 낮지만, CDC 툴이 추가되어 운영 복잡도가 증가한다.


Query 모델을 위한 최적의 데이터 저장소 선택 기준

Query 모델 분리를 했다면, Query 성능을 최적화하기 위해 기술부터 이해하고 합리적으로 선택해야 한다.


문서형 NoSQL(MongoDB)

유연하고, 수평적 확장에 용이하다.
상품 카탈로그, 사용자 프로필 등에 사용한다.


검색 엔진(ElasticSearch)

역색인 기반 구조로 전문 검색에 최적화 되어있다.
사이트 검색, 로그 분석, 실시간 모니터링 등에 사용한다.


인메모리 캐시(Redis/Valkey)

메모리를 사용하여 성능이 빠르지만 데이터가 유실 될 수 있으므로 임시 데이터를 저장한다.
인기 콘텐츠 캐싱, 세션 데이터, 실시간 카운터 등에 사용한다.


키-값 DB(DynamoDB)

단순 키-값 구조로 처리량이 높다.
사용자 데이터, 설정 정보, 대규모 이벤트 데이터 등에 사용한다.


그래프 DB(Neo4j)

노드와 관계 중심으로 경로 탐색에 최적화 되어있다.
친구 추천, 사기 탐지, 영향력 분석 등에 사용한다.


벡터 DB(Milvus)

벡터 유사도 검색에 용이하다.
유사 상품, 이미지 검색, 콘텐츠 추천 등에 사용한다.


예시 - 커머스 플랫폼의 상품 관리/검색 시스템(CDC)

example

출처 - 하규태 강사님 강의자료

상품의 여러가지 특성에 기반한 필터링과 텍스트 기반 검색에 유리한 검색 엔진을 Query 모델로 사용한다.
RDB에 쓰기 작업이 발생하면 CDC가 변경 로그를 감지하여 이벤트 큐에 전송한다.
순차적으로 CDC 이벤트를 소비하여 검색 엔진에 동기화한다.

This post is licensed under CC BY 4.0 by the author.