CQRS는 데이터 저장소에 대한 읽기 및 업데이트 작업을 구분하는 패턴인 명령과 쿼리의 역할 분리를 의미한다. CQRS 패턴은 데이터 변형 또는 시스템의 명령 부분과 쿼리(조회) 부분과 분리하여 처리량, 지연 시간 또는 일관성에 대한 요구 사항이 서로 다른 경우에 잘 대응할 수 있따. 명령측은 create, update, delete 요청을 처리하고, 쿼리 측에서는 읽기 전용 복제본을 사용하여 query 부분을 실행하는 구조이며, 이때 쿼리 측 DB는 읽기를 위한 단순 사본일 수도 있고, 혹은 조회 목적에 따라 최적화된 전혀 다른 이기종 혹은 스키마로 구성될 수 있다.
CQRS 기본 개념
일반적인 아키텍처에서는 데이터베이스를 쿼리하고 업데이트하는 데 동일한 데이터 모델을 사용하나, 최근의 복잡한 애플리케이션(읽기 및 쓰기 워크로드는 매우 다른 성능 및 확장성 요구 사항을 포함하여 종종 비대칭인)에서는 동일 DB의 사용이 적합하지 않다.
CQRS가 필요한 경우
- 작업의 일부로 필요하지 않더라도 정확하게 업데이트되어야만 하는 추가 열이나 속성과 같이 데이터의 읽기와 쓰기 표현 사이에 불일치가 나타나는 경우
- 동일한 데이터 세트에서 작업을 병렬로 수행할 때 데이터 경합이 발생 가능
- 기존의 접근 방식은 데이터 저장소와 데이터 액세스 계층에 가해지는 부하뿐 아니라 정보를 검색하는 데 필요한 쿼리의 복잡성으로 인해 성능 현안 가능
- 엔터티 각각이 읽기와 쓰기 작업의 대상으로 잘못된 컨텍스트에 데이터를 노출될 수 있어, 보안 및 권한 관리가 더 복잡성 증대
CQRS는 데이터를 업데이트하는 명령 및 데이터를 읽는 쿼리를 사용하여 읽기 및 쓰기를 다른 모델로 구분하여 문제를 해결한다.
- 명령은 데이터 중심이 아닌 작업을 기반이어야 하며("호텔 객실을 예약하라"는 명령을 처리리하는 것이지 "ReservationStatus"를 "Reserved"로 변경하라는 형태여서는 안된다), 사용자 상호 작용 스타일을 변경할 수 있음. 명령을 보내기 전에 클라이언트에서 몇 가지 유효성 검사 규칙을 실행하여 명령이 정상적으로 실행될 수 있도록 사전에 준비하여(예약 버튼을 비활성화하고 UI에서 남은 방이 없다고 보여주는 등) 서버 쪽 명령의 처리 오류의 원인을 경합 조건(마지막 방을 예약하려는 두 명의 사용자)으로 좁힐 수 있다. 경합 조건의 문제는 대기자 목록 등을 활용하여 비즈니스 로직으로 해소 필요.
- 명령은 비동기 처리를 위해 큐에 할당할 수 있음.
- 쿼리는 데이터베이스를 수정하지 않고, 도메인 정보를 캡슐화하지 않는 DTO를 반환.
CQRS의 일부 구현에서는 이벤트 소싱 패턴을 사용하여 애플리케이션 상태는 이벤트의 시퀀스로 저장되고, 데이터에 대한 변경 집합을 나타내는 이벤트를 재생(apply)하여 현재 상태를 구축할 수 있다. CQRS 컨텍스트에서 이벤트 소싱을 사용하는 이점으로 다른 구성 요소에 변경 사항을 통보하는데 동일한 이벤트를 사용할 수 있다는 점으로, 특히 읽기 모델에 데이터의 변화를 연동하는 것이효율 적이다. 따라서 읽기 모델은 현재 상태의 스냅샷을 만드는 데 이벤트를 사용할 수 있다. 다만 이벤트의 길이가 무한히 커지는 것을 막기 위해 snapshop을 만들고 해당 지점 이후부터 이벤트를 유지하는 형태로 관리가 가능하다.
CQRS의 장/단점
CQRS 장점
- 독립적인 크기 조정: CQRS를 통해 읽기 및 쓰기 워크로드를 독립적으로 확장하고 더 적은 수의 잠금 경합이 발생할 수 있습니다.
- 최적화된 데이터 스키마: 읽기 쪽에서는 쿼리에 최적화된 스키마를 사용하는 반면 쓰기 쪽에서는 업데이트에 최적화된 스키마를 사용할 수 있습니다.
- 보안: 올바른 도메인 엔터티만 데이터에서 쓰기를 수행할 수 있는지 쉽게 확인할 수 있습니다.
- 관심사의 분리: 읽기 및 쓰기 쪽을 구분하면 유지 가능하고 유연한 모델을 생성할 수 있습니다. 대부분의 복잡한 비즈니스 논리는 쓰기 모델로 이동합니다. 읽기 모델은 상대적으로 간단할 수 있습니다.
- 단순한 쿼리 읽기: 데이터베이스에서 구체화된 뷰를 저장하여 쿼리할 때 애플리케이션은 복잡한 조인을 방지할 수 있습니다.
CQRS 단점
- 복잡성: CQRS의 기본 개념은 간단하지만 실제 이기종 데이터 복제는 간단하지 않으며, 추가로 이벤트 소싱 패턴을 포함하는 경우에 특히 더 복잡해질 수 있음
- 메시징: CQRS에 메시징이 필요하지 않지만 명령을 처리하고 업데이트 이벤트를 게시하는 데 공통적으로 메시징을 사용할 수 있음. 따라서 애플리케이션은 메시지 오류 또는 중복 메시지를 처리해야 함
- 결과적 일관성: 읽기 및 쓰기 데이터베이스를 구분하는 경우 읽기 데이터는 이미 변경된 값을 응답할 수 있음. 사용자가 오래된 읽기 데이터를 기반으로 요청을 발급하면 변경된 사항을 검색하기 어려울 수 있어 대응 로직이 필요함
CQRS 구현
CQRS 처리 흐름

- 비즈니스에서 생성/변경/삭제 등의 이벤트가 발생되면, 쓰기API 기능의 애플리케이션은 요청을 받아 처리/응답을 수행한다.
- 응용 프로그램은 들어오는 명령을 처리하며, 데이터의 일관성 유지 등의 추가적인 기능을 포함하여 작업의 검증, 권한 부여 및 실행 등을 수행한다.
- 응용 프로그램은 명령 데이터를 쓰기(명령) 데이터베이스에 유지한다.
- 명령이 쓰기 데이터베이스에 저장되면 읽기 (쿼리) 데이터베이스의 데이터를 업데이트하는 이벤트가 트리거된다.
- 읽기 (쿼리) 데이터베이스는 생성된 이벤트를 처리하여 쓰기 DB와 일관성을 맞추도록 데이터를 처리하고 유지한다. 읽기 데이터베이스는 특정 유형의 쿼리 요구 사항에 맞게 최적화되도록 선택되고 설계해야 한다.
- 비즈니스는 조회 이벤트가 발생되면 읽기 API와 상호 작용하여 애플리케이션의 쿼리를 발생시킨다.
- 애플리케이션은 들어오는 쿼리를 읽기 데이터베이스에서 데이터를 검색하여 응답한다.
CQRS 구성
CQRS 구성을 위하 DBMS는 SQL 및 NoSQL등을 다양하게 조합할 수 있으며, 비즈니스 애플리캐이션 특성에 따라 구성이 가능하다.
- 명령과 쿼리 측 모두에 관계형 데이터베이스 관리 시스템 (RDBMS) 데이터베이스를 사용하여, 쓰기 작업은 기본 데이터베이스로 이동하고 읽기 작업은 읽기 전용 복제본에서 처리하는 형태(MySQL의 복수 복제본을 읽기 전용으로 사용 등)
- 명령 측에는 RDBMS 데이터베이스를 사용하고 이벤트 소싱 등을 활용하여 쿼리 측의 NoSQL 데이터베이스를 현행화 하여 읽기 전용으로 사용 형태
- 명령 및 쿼리 측 모두에 NoSQL 데이터베이스 사용
- 명령 측에는 NoSQL 데이터베이스를 사용하고 쿼리 측에는 RDBMS 데이터베이스를 사용
다음 그림은 AWS의 가이드에 나온 DynamoDB와 데이터 스토어를 사용하여 쓰기 처리량을 최적화하고 유연한 쿼리 기능을 제공하면서, Lambda 함수를 통해 이벤트를 소싱 등을 통해 Aurora(RDBMS)에 구성하는 형태이다. 따라서 데이터를 추가할 때 액세스 패턴이 잘 정의된 워크로드에서 쓰기 확장성에 유리하며, 관계형 데이터베이스인 Aurora를 사용하여 분석/리포팅 등에 필요한 복잡한 쿼리 기능을 사용할 수 있다.

이벤트 소싱
CQRS 패턴은 이벤트 소싱 패턴과 함께 사용되는 경우가 많다. CQRS 기반 시스템은 별도의 읽기 및 쓰기 데이터 모델을 사용하며 각각 관련 작업에 맞춤화되고 종종 물리적으로 분리된 저장소에 배치된다. 이벤트 소싱 패턴과 함께 사용할 때 이벤트 저장소는 쓰기 모델이며 정보의 공식적인 출처가 된다. 보통 CQRS 기반 시스템의 읽기 모델은 고도로 비정규화된 뷰로 데이터의 구체화된 뷰를 제공해야 한다.
특정 시점의 실제 데이터 대신 이벤트의 스트림을 쓰기 저장소로 사용하면 단일 집계에서 업데이트 충돌을 방지하고 성능과 확장성을 최대화할 수 있으며, 읽기 저장소를 채우는 데 사용하는 데이터의 구체화된 뷰를 비동기적으로 생성하는 데 이벤트를 사용할 수 있다.
이벤트 저장소는 정보의 공식적인 출처이기 때문에, 시스템이 진화하거나 읽기 모델을 변경해야 할 때 구체화된 뷰를 삭제하고 모든 지난 이벤트를 재생해 현재 상태의 새로운 표현을 생성할 수 있으며, 사실상 구체화된 뷰는 데이터의 지속형 읽기 전용 캐시 형태로 볼 수 있다.
CQRS를 이벤트 소싱 패턴과 함께 사용할 때는 다음 사항들을 고려해야 한다.
- 쓰기 및 읽기 저장소가 분리되는 다른 패턴처럼, CQRS를 구현하면 결과적 일관성만을 유지 가능. 즉, 생성되는 이벤트와 업데이트되는 데이터 저장소 사이에는 약간의 시차(지연)이 나타날 수 있음
- 이벤트를 시작하고 처리하며 쿼리나 읽기 모델에 필요한 적절한 뷰 또는 개체와 관련된 코드를 생성해야 하기 때문에, 이벤트 소싱 패턴은 더 복잡함. 그러나 이벤트 소싱은 도메인을 더 쉽게 모델링할 수 있고 데이터를 변경한 의도가 보존되기 때문에 뷰를 다시 작성하거나 새로 만들기가 더 쉬울 수 있음
- 특정 엔터티 또는 엔터티 모음을 위한 이벤트를 재생하고 처리하여 데이터의 읽기 모델 또는 프로젝션에 사용할 구체화된 뷰를 생성하려면 상당한 처리 시간과 리소스 사용이 필요할 수 있으며, 장기간 값의 합계 또는 분석이 필요한 경우에 특히 문제가 될 수 있음. 이러한 문제는 발생한 특정 동작의 총 개수 또는 엔터티의 현재 상태와 같이 예약된 간격으로 데이터의 스냅샷을 구현해 해결 가능
AWS의 CQRS 구현 예제
CQRS 구현
CQRS 패턴은 데이터 액세스 객체 단일 CRUD(생성, 읽기, 업데이트, 삭제) 모델과 같은 단일 개념적 운영 모델을 명령 및 쿼리 운영 모델로 구분합니다. 명령 모델은 생성, 업데이트 또는 삭제와 같이 상태를 변경하는 모든 작업을 말합니다. 쿼리 모델은 값을 반환하는 모든 작업을 가리킵니다.

고객 CRUD 모델에는 다음과 같은 인터페이스가 포함됩니다.
Create Customer()
UpdateCustomer()
DeleteCustomer()
AddPoints()
RedeemPoints()
GetVIPCustomers()
GetCustomerList()
GetCustomerPoints()
요구 사항이 더욱 복잡해지면 이 단일 모델 접근 방식을 벗어나도 됩니다. CQRS는 명령 모델과 쿼리 모델을 사용하여 데이터 쓰기 및 읽기 책임을 분리합니다. 이렇게 하면 데이터를 독립적으로 유지 관리하고 관리할 수 있습니다. 책임을 명확히 구분하면 각 모델의 개선이 다른 모델에는 영향을 미치지 않습니다. 이렇게 분리하면 유지 관리 및 성능이 향상되고 애플리케이션이 성장함에 따라 복잡성이 줄어듭니다.

-
고객 명령 모델의 인터페이스.
Create Customer()
UpdateCustomer()
DeleteCustomer()
AddPoints()
RedeemPoints()
-
고객 쿼리 모델의 인터페이스.
GetVIPCustomers()
GetCustomerList()
GetCustomerPoints()
GetMonthlyStatement()
그런 다음 CQRS 패턴은 데이터베이스를 분리합니다. 이러한 디커플링은 각 서비스의 완전한 독립성으로 이어지며, 이는 마이크로서비스 아키텍처의 주요 요소입니다.

CQRS에는 다음과 같은 이점이 있습니다.
- 독립적 규모 조정: 각 모델은 서비스의 요구 사항 및 수요를 충족하도록 확장 전략을 조정할 수 있습니다. 고성능 애플리케이션과 마찬가지로 읽기와 쓰기를 분리하면 모델을 독립적으로 규모를 조정하여 각 요구 사항을 충족할 수 있습니다. 또한 컴퓨팅 리소스를 추가하거나 줄여 다른 모델에 영향을 주지 않으면서 한 모델의 확장성 요구를 해결할 수 있습니다.
- 독립적 유지 관리: 쿼리와 명령 모델을 분리하면 모델의 유지 관리 용이성이 향상됩니다. 다른 모델에는 영향을 주지 않으면서 한 모델의 코드를 변경하고 개선할 수 있습니다.
- 보안: 읽기 및 쓰기를 위해 별도의 모델에 권한과 정책을 적용하는 것이 더 쉽습니다.
- 읽기 최적화: 쿼리에 최적화된 스키마를 정의할 수 있습니다. 예를 들어, 집계된 데이터에 대한 스키마를 정의하고 팩트 테이블에 대해 별도의 스키마를 정의할 수 있습니다.
- 통합: CQRS는 이벤트 기반 프로그래밍 모델에 적합합니다.
- 복잡성 관리: 쿼리와 명령 모델로의 분리는 복잡한 도메인에 적합합니다.
CQRS를 사용할 때는 다음 주의 사항을 명심하십시오.
- CQRS 패턴은 애플리케이션의 특정 부분에만 적용되며 전체 애플리케이션에는 적용되지 않습니다. 패턴에 맞지 않는 도메인에 구현하면 생산성이 저하되고 위험이 증가하며 복잡성이 생길 수 있습니다.
- 이 패턴은 읽기 및 쓰기 작업이 불균형하고 자주 사용되는 모델에 가장 적합합니다.
- 처리하는 데 시간이 걸리는 대용량 보고서와 같이 읽기 중심의 애플리케이션의 경우 CQRS는 적절한 데이터베이스를 선택하고 집계된 데이터를 저장할 스키마를 생성할 수 있는 옵션을 제공합니다. 이렇게 하면 보고서 데이터를 한 번만 처리하고 집계된 테이블에 덤프하므로 보고서를 읽고 보는 응답 시간이 향상됩니다.
- 쓰기가 많은 애플리케이션의 경우 쓰기 작업을 위해 데이터베이스를 구성하고 쓰기 수요가 증가할 때 명령 마이크로서비스가 독립적으로 규모가 조정되도록 할 수 있습니다.

- 명령 Lambda 함수는 데이터베이스에서 생성, 업데이트 또는 삭제와 같은 쓰기 작업을 수행합니다.
- 쿼리 Lambda 함수는 데이터베이스에서 가져오기 또는 선택과 같은 읽기 작업을 수행합니다.
- 이 Lambda 함수는 명령 데이터베이스의 DynamoDB 스트림을 처리하고 변경 사항에 맞게 쿼리 데이터베이스를 업데이트합니다.
이벤트 소싱
다음 단계는 명령 실행 시 이벤트 소싱을 사용하여 쿼리 데이터베이스를 동기화하는 것입니다. 예를 들어 다음 이벤트를 고려합니다.
- 고객 보상 포인트가 추가되어 쿼리 데이터베이스의 고객 총 보상 포인트 또는 집계된 보상 포인트를 업데이트해야 합니다.
- 명령 데이터베이스에서 고객의 성이 업데이트되므로 쿼리 데이터베이스의 대리 고객 정보를 업데이트해야 합니다.
기존 CRUD 모델에서는 트랜잭션이 완료될 때까지 데이터를 잠가서 데이터의 일관성을 보장합니다. 이벤트 소싱에서는 구독자가 해당 데이터를 업데이트하는 데 사용하는 일련의 이벤트 게시를 통해 데이터가 동기화됩니다.
이벤트 소싱 패턴은 데이터에 대해 취해진 일련의 전체 조치를 보장 및 기록하고 일련의 이벤트를 통해 데이터를 게시합니다. 이러한 이벤트는 해당 이벤트의 구독자가 기록을 최신 상태로 유지하기 위해 처리해야 하는 일련의 데이터 변경 사항을 나타냅니다. 이러한 이벤트는 구독자가 소비하여 구독자 데이터베이스의 데이터를 동기화합니다. 이 경우에는 쿼리 데이터베이스가 바로 그것입니다.
다음 다이어그램은 AWS 기반 CQRS에서 사용되는 이벤트 소싱을 보여줍니다.

- 명령 Lambda 함수는 데이터베이스에서 생성, 업데이트 또는 삭제와 같은 쓰기 작업을 수행합니다.
- 쿼리 Lambda 함수는 데이터베이스에서 가져오기 또는 선택과 같은 읽기 작업을 수행합니다.
- 이 Lambda 함수는 명령 데이터베이스의 DynamoDB 스트림을 처리하고 변경 사항에 맞게 쿼리 데이터베이스를 업데이트합니다. 또한 이 함수를 사용하여 Amazon SNS에 메시지를 게시하여 구독자가 데이터를 처리할 수 있도록 할 수도 있습니다.
- (선택 사항) Lambda 이벤트 구독자는 Amazon SNS에서 게시한 메시지를 처리하고 쿼리 데이터베이스를 업데이트합니다.
- (선택 사항) Amazon SNS는 쓰기 작업에 대한 이메일 알림을 보냅니다.
AWS에서는 DynamoDB 스트림으로 쿼리 데이터베이스를 동기화할 수 있습니다. DynamoDB는 DynamoDB 테이블에서 시간 순서에 따라 항목 수준 수정을 거의 실시간으로 캡처하고 24시간 내에 정보를 안정적으로 저장합니다.
DynamoDB 스트림을 활성화하면 데이터베이스에서 이벤트 소싱 패턴을 가능하게 하는 일련의 이벤트를 게시할 수 있습니다. 이벤트 소싱 패턴은 이벤트 구독자를 추가합니다. 이벤트 구독자 애플리케이션은 이벤트를 소비하고 구독자의 책임에 따라 이벤트를 처리합니다. 이전 다이어그램에서 이벤트 구독자는 변경 내용을 쿼리 DynamoDB 데이터베이스에 푸시하여 데이터를 동기화된 상태로 유지합니다. Amazon SNS, 메시지 브로커 및 이벤트 구독자 애플리케이션을 사용하면 아키텍처가 분리된 상태로 유지됩니다.
이벤트 소싱에는 다음과 같은 이점이 있습니다.
- 트랜잭션 데이터의 일관성
- 데이터에서 취해진 조치를 모니터링하는 데 사용 가능한 신뢰할 수 있는 감사 추적 및 작업 기록
- 마이크로서비스와 같은 분산 애플리케이션에 환경 전반에서 걸쳐 데이터 동기화 허용
- 상태가 변경될 때마다 신뢰할 수 있는 이벤트 게시
- 과거 상태 재구성 또는 재생
- 모놀리식 애플리케이션에서 마이크로서비스로의 마이그레이션을 위해 이벤트를 교환하는 느슨하게 결합된 엔터티
- 동시 업데이트로 인한 충돌 감소, 이벤트 소싱 사용으로 데이터 스토어에서 객체 직접 업데이트 불필요
- 작업과 이벤트를 분리하는 데 따른 유연성 및 확장성
- 외부 시스템 업데이트
- 단일 이벤트에서 여러 작업 관리
이벤트 소싱을 사용할 때는 다음 주의 사항을 명심하십시오.
- 소스 구독자 데이터베이스 간에 데이터를 업데이트하는 데 약간의 지연이 있기 때문에 변경을 취소할 수 있는 유일한 방법은 이벤트 저장소에 보상 이벤트를 추가하는 것입니다.
- 이벤트 소싱은 프로그래밍 스타일이 다르기 때문에 구현을 익히는 데 시간이 많이 걸립니다.
참조 문헌
https://martinfowler.com/bliki/CQRS.html
https://learn.microsoft.com/ko-kr/azure/architecture/patterns/cqrs
https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/modernization-data-persistence/cqrs-pattern.html
https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/patterns/decompose-monoliths-into-microservices-by-using-cqrs-and-event-sourcing.html