CQRS
Command Query Responsibility Segregation, 명령-조회 책임 분리로 명령(Command)과 조회(Query)의 책임을 분리하는 아키텍처 패턴이다.
이전의 전통적인 CRUD(Create/Read/Update/Delete) 모델에서는 모든 작업이 동일한 하나의 데이터 모델/저장소를 이용했다.
CQRS에서는 쓰기와 읽기 작업을 분리해서 각 작업만을 수행하는 DB를 이용해 성능과 확장성을 챙긴 아키텍처를 구축하게 된다.
CQRS 구성
읽기와 쓰기 작업을 모델별로 분리했기에 아래와 같이 구성된다.
Command 모델
데이터를 변경(Create, Update, Delete)하는 작업을 수행하는 모델
Query 모델
데이터를 조회(Query)하기 위한 모델, 오직 조회 작업만 수행한다.
이벤트
분리된 모델에 대해 싱크를 맞춰주어야한다. Command 모델에서 변경이 발생하면 이벤트를 발생시키고, 이것을 Query 모델이 구독하며 데이터를 동기화하는 수행한다.
왜 CQRS가 필요한가?
아래 상황들에서 특히 CQRS가 유용하게 작용할 수 있다.
- 읽기와 쓰기 작업의 부하량이 다른 경우
- 예를 들면 SNS나 뉴스 서비스는 읽기 작업의 비중이 쓰기 작업보다 크게 작용한다.
- 이러한 상황에서 작업을 분리하며 읽기 성능을 최적화시킬 수 있게된다.
- 확장성이 필요한 경우
- 읽기와 쓰기 작업이 분리가 되어있기에 각각 독립적으로 스케일링이 가능하다.
- 위와 같이 읽기 작업이 큰 경우, 읽기 전용 DB를 스케일 아웃하면서 부하를 줄이고 성능을 높일 수 있다.
- 접근제어 세분화
- 읽기와 쓰기에 대한 API를 분리해서 필요에 따라 접근 제어를 설정할 수도 있다.
- 예를 들어, 읽기 API는 퍼블릭하게 오픈하고 쓰기 API는 내부 서비스 전용으로 둘 수 있다는 것이다.
⇒ 확장성, 성능/데이터 모델 최적화, 접근제어를 통한 보안성 강화
CQRS의 단점
아키텍처의 복잡성 증가
기본적으로 모델을 분리했기에, 기존의 CRUD 모델에 비해 시스템의 설계와 구현이 복잡해진다.
분리된 모델 간의 데이터 동기화와 같은 작업들이 추가적으로 필요해진다.
데이터 일관성과 정합성
CQRS는 이벤트 기반 데이터 동기화를 이용하게 되며, 즉시 일관성(Strong Consistency)이 아닌 최종적인 일관성(Eventual Consistency)을 보장하게 된다. 쓰기 모델의 변경본이 읽기 모델에 반영되기까지 지연이 발생할 수밖에 없는 구조이기 때문이다.
또한, 데이터 동기화를 위한 이벤트 큐 관리가 필요하다. 이벤트 전송이 실패하거나 중복 처리되는 경우에는 데이터 정합성이 깨지게 되므로, 이를 방지하기 위한 추가적인 매커니즘이나 패턴을 구현해둘 필요가 있다.
인프라 리소스 비용 증가
모델을 2가지로 분리했기에 DB에 대한 리소스 비용이 증가한다. 기존의 단일 DB보다 운영과 유지보수에 대한 비용도 증가하며, 모델간의 데이터 일관성을 위해 메시지 브로커나 캐시 등을 도입하면 그에 대한 리소스 비용도 고려해야한다.
아키텍처 예제
읽기와 쓰기 작업이 분리된 간단한 서비스를 API Gateway와 Lambda, DB를 이용해 구성해보도록 한다.
이 아키텍처는 아래와 같이 CQRS를 적용해, 작업을 분리했다.
Command
클라이언트 → API Gateway → Lambda → DynamoDB (쓰기)
데이터 변경이 발생하면 SQS에 이벤트 전송, 이 SQS를 구독한 event_processor Lambda가 읽기 테이블에 반영
Query
클라이언트 → API Gateway → Lambda → DynamoDB (읽기)
구성한 CQRS 아키텍처를 통해, 먼저 데이터를 삽입한다 (CQRS_command)
이후, 쓰기 테이블에 데이터 변경이 발생하며 SQS에 이벤트 메시지가 전송되고, event_processor Lambda가 읽기 테이블에 반영하는 것을 볼 수 있다.
그리고 읽기 API를 통해 읽기 테이블에서 SQS 이벤트를 통해 동기화된 데이터를 읽기 테이블에서 확인할 수 있다.
사용된 아키텍처 코드는 Github에서 확인할 수 있다.