CQRS 패턴

임태환·2025년 2월 19일

study

목록 보기
9/10

CQRS란?

소프트웨어 설계 패턴으로 Command(명령) 와 Query(조회)의 책임을 분리하여
시스템의 읽기와 쓰기 작업을 별도로 처리하는 방식이다.

즉 하나 이상의 쿼리가 구현된 하나 이상의 뷰 DB를 유지하는 기법으로
API 조합 패턴으로는 효율적으로 구현하기 어려운 쿼리(다중 서비스 쿼리등)에서 유용하다.

  1. API를 조합하여 여러 서비스에 흩어진 데이터 조회 시 값비싸고 비효율적인 인메모리-조인
  2. 데이터를 가진 서비스는 필요한 쿼리를 효율적으로 지원하지 않는 DB에 또는 그런 형태로 저장
  3. 관심사를 분리할 필요가 있다는 것은 데이터를 가진 서비스가 쿼리 작업을 구현할 장소로 부적합

이 3가지 문제를 해결할 수 있는 방법이 바로 CQRS 패턴이다.

CQRS의 필요성

일전에 API 조합 패턴을 사용하면 데이터를 조회하는 쿼리를 쉽게 작성이 가능하다 하였다.

하지만 API 조합기에서 인메모리 조인을 사용하는 방식은 거대한 데이터 뭉치일 경우 효율성이
크게 떨어진다.

API 조합 패턴의 한계

대규모 데이터 처리의 비효율성

각 마이크로서비스가 반환하는 데이터가 클 경우 API 조합기는 모든 데이터를 메모리에 로드 후 병합

예시 : 수백만 건의 주문 데이터와 관련된 결제 및 배송 데이터 병합 시 메모리 사용량 급증
결과 : OOME문제 발생 가능, 서버 성능 저하 및 다른 요청 처리에도 영향

ID 기반 데이터 요청 시

대량 조회 API가 없을 경우 각 ID에 대해 개별적으로 요청을 보내야하므로
과도한 네트워크 트래픽 유발 및 응답 시간이 증가하며 운영 비용이 증가한다.

어려운 단일 서비스 쿼리

  1. 데이터를 가진 서비스에 쿼리를 구현하는 것이 부적절한 경우

1-1. 서비스의 역할 초과

  • 단일 서비스가 복잡한 쿼리 로직 처리 시 비즈니스 로직 외의 데이터 병합,필터링,정렬 작업 실행
    • 서비스의 역할이 지나치게 확장되어 유지보수성, 확장성 저하

1-2. 캡슐화 위반

  • 데이터를 가진 서비스가 외부 요청에 따라 복잡한 쿼리 제공 시 내부 데이터 구조 스키마 노출
    • 다른 서비스가 해당 스키마에 의존하여 캡슐화에 위반되며 결합성이 증가하게 된다.

1-3. 성능 저하

  • 단일 서비스에서 복잡한 쿼리 처리 시 해당 서비스의 리소스 사용량 증가
    • 핵심 비즈니스 로직 수행 속도가 느려질 수 있다.
  1. 서비스 DB가 효율적인 쿼리를 지원하지 않는 경우

2-1. 데이터베이스 설계의 한계

  • MSA에서는 각 서비스가 자신의 DB를 소유하며 해당 도메인의 트랜잭션 처리를 최적화하도록 설계
    • 트랜잭션 중심 설계는 대량 조회나 복잡한 집계 작업을 효율적으로 처리하기 어렵다.

2-2. 데이터 모델링 제약

  • 서비스는 자신만의 데이터 스키마를 가져 다른 도메인 간의 관계를 고려하지 않은 상태에서 설계
    • 단일 서비스 내에서 복잡한 관계형 쿼리를 작성하기 어렵다.

2-3. 기술적 한계

  • 일부 DB는 고급 쿼리 기능(다중 조인,서브쿼리,집계 함수)을 지원하지 않거나 성능이 떨어질 수 있음
    • NoSQL DB는 스키마리스 구조로 관계형 조인을 지원하지 않는다 (애플리케이션 레벨에서 처리)
    • 분산형 DB는 쓰기 및 읽기 성능은 뛰어나지만 복잡한 분석 쿼리에 부적합

비CQRS 구조와 CQRS 구조 차이

비 CQRS 구조의 특징

CRUD 작업이 모두 하나의 DB에서 이루어진다.
서비스는 단일 도메인 모델을 통해 모든 작업을 처리한다.

도메인 모델(애그리거트)이 CRUD 작업을 처리하며 조회도 동일한 경로를 통해 처리
조회 요청은 별도의 최적화 없이 그대로 통과하여 DB에서 데이터를 가져온다

비 CQRS 구조의 단점

읽기와 쓰기의 성능 충돌

  • 읽기와 쓰기가 동일한 DB 사용 -> 읽기 요청이 많아질 경우 쓰기 성능에 영향 (반대도 동일)
    • EX) 대규모 조회 작업이 쓰기 작업의 속도를 저하시킬 수 있다.

복잡한 도메인 모델

  • 도메인 모델이 읽기, 쓰기를 모두 처리 -> 복잡도 증가 , 유지보수성 하락
  • 비즈니스 로직과 조회 로직이 섞여 있어 분리하기 어렵다

확장성 제한

  • 단일 DB를 사용하므로 읽기와 쓰기를 독립적으로 확장하지 못함
    • EX) 읽기 요청 폭증 시 -> 전체 시스템이 병목 현상을 겪을 수 있음

CQRS 구조의 특징

읽기와 쓰기의 완전한 분리

쓰기 작업(CUD)은 커맨드 모델에서 처리하고 읽기 작업(R)은 쿼리 모델에서 처리한다.
커맨드 모델, 쿼리 모델은 독립적인 DB를 사용한다 (커맨드 DB, 쿼리 DB)

도메인 이벤트 기반 동작

커맨드 모델에서 데이터 변경(CUD)이 발생하면 도메인 이벤트가 발행된다.
(이벤추에이트 트램이나 이벤트 소싱등의 프레임워크를 이용하여 도메인 이벤트를 발행한다)
쿼리 모델은 이 이벤트를 구독하고, 자신의 DB를 업데이트하여 최신 상태를 유지한다.

CQRS 구성 요소

커맨드 모델

CUD 작업을 처리하며 비즈니스 로직과 데이터 무결성을 검증
EX) 주문 생성 요청 -> 커맨드 DB에 저장 -> OrderCreated 이벤트 발행

쿼리 모델

R 작업을 처리하며 복잡한 조회 요청에 최적화된 구조로 설계
도메인 이벤트를 구독하여 쿼리 DB를 최신 상태로 유지
EX) OrderCreated 이벤트 수신 -> 쿼리 DB에 새로운 주문 정보 추가 -> 사용자 조회 요청 시 최신 데이터 반환

독립적인 DB

커맨드 DB : 트랜잭션 중심으로 설계되어 CUD 작업에 최적화
쿼리 DB : 읽기 작업에 최적화된 비정규화된 데이터 구조 또는 캐시 사용(redis, Elasticsearch)

CQRS와 쿼리 전용 서비스

CQRS는 일반적으로 하나의 서비스 내부에서 사용된다
CQRS 패턴을 이용하여 여러 마이크로서비스 간 데이터를 통합하거나 특정 요구사항에 맞춘
독립적인 쿼리 서비스를 정의하는 것도 가능하다.

쿼리 전용 서비스는 CQRS 패턴의 "Query" 부분을 독립적인 서비스로 구현한 형태다.
이 서비스는 오직 읽기 작업(Read)에만 집중하며, 다른 마이크로서비스에서 발행한 도메인 이벤트를 구독하여 데이터를 유지한다.

쿼리 전용 서비스 특징

커맨드 작업 없음

쿼리 전용 서비스는 데이터를 CUD하는 작업을 처리하지 않고 데이터를 조회하는 API로만 구성

도메인 이벤트 구독

다른 마이크로 서비스가 발행한 도메인 이벤트를 구독하여 최신 상태로 유지

이벤트 핸들러

  • 발행된 이벤트를 수신하고 이벤트 내용을 분석하여 적절한 데이터 변경 작업 수행

    도메인 이벤트를 처리하여 쿼리 전용 서비스의 DB를 업데이트 하는 로직 구현

최신 상태 유지

쿼리 전용 서비스는 자체 DB를 사용하며 읽기 작업에 최적화된 구조를 가지며
다른 마이크로서비스의 DB에 직접 접근하지 않고도 필요한 데이터를 효율적으로 제공 가능

특정 뷰 제공

여러 마이크로 서비스의 데이터를 통합하여 특정 요구사항에 맞춘 뷰를 제공
EX) 관리자 대시보드 통계, 사용자별 주문 내역

스탠드얼론 서비스로 구현 가능

특정 도메인에 종속되지 않으므로 독립적인 스탠드얼론 서비스로 구현하기 적합

CQRS VS 쿼리 전용 서비스

특징CQRS쿼리 전용 서비스
주요 역할읽기와 쓰기를 분리하여 독립적으로 처리오직 읽기 작업(Read)에만 집중
데이터베이스 구조Command DB와 Query DB를 별도로 유지자체 독립적인 데이터베이스 사용
도메인 이벤트 활용 여부도메인 이벤트 발행(커맨드) 및 구독(쿼리)주로 도메인 이벤트 구독
복잡한 조회 처리Query 모델에서 복잡한 조회 지원여러 서비스에서 받은 데이터를 병합하거나 가공하여 반환

CQRS 장점 및 단점

CQRS 장점

MSA에서 효율적인 쿼리가 가능하다

  • 여러 서비스의 데이터를 조회하는 쿼리를 효율적으로 구현할 수 있게 해준다.
    • 여러 서비스에서 데이터를 미리 조인해놓는 CQRS 뷰를 이용하는 것이 간편하고 효율적이다.

다양한 쿼리를 효율적으로 구현할 수 있다.

  • 다양한 쿼리를 애플리케이션 서비스에 효율적으로 구현할 수 있다.
    • 각 쿼리가 효율적으로 구현된 하나 이상의 뷰를 정의하여 단일 데이터 저장소의 한계 극복

이벤트 소싱 애플리케이션에서 쿼리가 가능하다

  • 이벤트 소싱의 중요한 한계(이벤트 저장소는 기본키 쿼리만 지원)을 극복하게 해준다
    • 하나 이상의 애그리거트 뷰를 정의하고 이벤트 소싱 기반의 애그리거트가 발행한
      이벤트 스트림을 구독해서 항상 최신 상태를 유지한다
      (이벤트 소싱 애플리케이션은 거의 예외 없이 CQRS 사용)

관심사가 더 분리된다.

  • 서비스의 커맨드. 쿼리쪽에 각각 알맞은 코드 모듈과 DB 스키마를 별도로 정의한다
    • 커맨드/쿼리 양쪽 모두 관리하기 간편해진다.

CQRS 단점

아키텍처가 복잡하다

  • 개발자는 뷰를 조회/수정하는 쿼리 서비스를 작성해야한다
    • 별도의 데이터 저장소를 관리해야하는 운영 복잡도 가중
    • 종류가 다양한 DB 애플리케이션이라면 개발/운영 복잡도 가중

복제 시차를 신경 써야 한다

  • 커맨드/쿼리 양쪽 뷰 사이의 시차를 처리해야한다.
    • 커맨드쪽이 이벤트를 발행하는 시점 및 쿼리쪽에서 이벤트를 받아 뷰 업데이트하는 시점 사이 지연 발생
    • 클라이언트 애플리케이션이 애그리거트를 업데이트한 즉시 뷰를 쿼리하면 이전 애그리거트를
      바라보게 될 수도 있다. (데이터 일관성 문제)

복제 시차 해결방법

커맨드/쿼리 양쪽 API가 클라이언트에 버전 정보를 전달해서 데이터를 분간할 수 있게 만드는 방법

  • 클라이언트는 최신 데이터를 받을 때까지 쿼리 쪽 뷰를 계속 풀링

네이티브 모바일 앱이나 SPA같은 UI 애플리케이션

  • 쿼리를 하지않고 커맨드가 성공하면 로컬 모델을 업데이트 하는 방법으로 복제 시차 해결
    (모델을 업데이트하려면 UI 코드가 서버 쪽 코드를 복제해야 하는 단점 존재)

정리

가능한 API 조합 패턴을 사용하고 필요할 경우에만 CQRS를 사용해야한다.

profile
웹 개발자

0개의 댓글