📚 Data Architect/Design Pattern

CQRS(Command Query Responsibility Segregation) Pattern

신건우 2024. 11. 26. 13:17

📚 CQRS(Command Query Responsibility Segregation) Pattern

CQRS(Command Query Responsibility Segregation) 패턴은 DB로부터 읽기와 쓰기 작업을 분리하는 패턴입니다.

이 패턴을 사용하면 어플리케이션의 성능,화장성,보안성을 극대화 시킬 수 있으며, 여러 요청으로부터 들어온 복수의 업데이트 명령에 대한 충돌을 방지할 수 있습니다.

오늘은 프로젝트에 CQRS를 구현하기 전 CQRS가 무엇인지, 왜 사용해야 하는지, 사용하면 어떤 점이 개선되는지 먼저 공부해 보겠습니다.


📚 CQRS 패턴을 사용하려는 이유

CQRS를 채용하려는 이유는 현재 개발중인 로직중 RabbitMQ를 이용한 대규모 이벤트 데이터의 전처리 및 통계 처리와 RTSP Stream을 HLS로 변환해 스트리밍 하는 로직 떄문입니다.

RTSP 스트리밍과 이벤트 데이터의 통계 처리 로직에서 읽기와 쓰기의 성격이 현저히 다르고, 독립적으로 확장 및 유지보수할 필요가 있기 때문입니다.

특히, 실시간 데이터 처리와 통계 조회 간 성능 요구사항과 비즈니스 로직 복잡도의 차이를 해결하기 위해 CQRS는 적합한 패턴입니다.


저의 경우 아래 7가지 중 제가 사용해야 하는 이유는 4,5,6,7번에 해당합니다.


CQRS는 도메인과 비즈니스 로직이 간단하거나 단순한 CRUD 기능일 때 사용이 권장되지 않고, 아래 상황일 떄 사용하면 좋습니다.

1. 여러 사용자가 동일한 데이터에 병렬로 액세스하는 도메인.

2. CQRS는 도메인 레벨에서의 병합 충돌을 최소화할 수 있도록 충분히 세분화된 명령(Command)를 정의하는 것이 가능.

3. 복잡한 프로세스나 도메인 모델을 통해 가이드되는 작업 기반 사용자 인터페이스. 쓰기 모델은 비즈니스 로직, 유효성 검사 등을 모두 가진 완전한 명령(Command) 처리 기능을 가진다. 쓰기 모델은 관련된 객체들의 집합을 하나의 단위(DDD에서의 aggregate)로 다룰 있고, 이 객체들이 항상 일관된 상태를 가지도록 보장할 수 있다. 읽기 모델의 경우 비즈니스 로직이나 유효성 검사 같은 것 없이, 오직 DTO만 반환한다. 읽기 모델은 최종적으로 쓰기 모델과 일치하게 됨. (딜레이는 있을 수 있음)

4. 데이터 읽기의 성능이 데이터 쓰기의 성능과 별도로 조정이 가능해야 할 때. 특히 읽기의 수가 쓰기의 수보다 훨씬 많을 때. 이 경우, 읽기 모델은 스케일 아웃을 하고, 쓰기 모델은 적은 수로 유지할 수 있고, 적은 수의 쓰기 모델은 병향 충돌의 가능성을 최소화 해줌.

5. 한 팀은 쓰기 모델에 대한 복잡한 도메인 모델에만 집중해야 하고, 다른 한 팀은 사용자 인터페이스에 대한 읽기 모델에만 집중해야 하는 경우

6. 시스템이 시간이 지남에 따라 계속해서 진화하고, 여러 버전을 가질 수 있으며, 정기적으로 바뀔 수 있는 경우

7. 다른 시스템과의 통합, 특히 이벤트 소싱과 결합할 때. 서브시스템의 일시적 장애가 다른 시스템에 영향을 줘선 안되는 경우.


4번 : 데이터 읽기의 성능이 데이터 쓰기의 성능과 별도로 조정이 가능해야 하는 경우

통계 조회 작업은 대량의 데이터를 반복적으로 읽는 작업이 주를 이루며, 이는 RTSP 스트리밍과 같은 쓰기 작업보다 훨씬 빈번하게 발생합니다.

CQRS 패턴을 사용하면 읽기 모델은 조회 성능 최적화(예: 캐시, 읽기 전용 DB 사용)가 가능하고, 쓰기 모델은 복잡한 비즈니스 로직을 처리하도록 분리할 수 있습니다.

또, 쓰기 작업(이벤트 전처리 및 저장)과 읽기 작업(통계 조회)이 서로 독립적으로 확장될 수 있어 성능 조정이 용이합니다.


5번 : 한 팀은 쓰기 모델에 집중하고, 다른 팀은 읽기 모델에만 집중해야 하는 경우

RTSP 스트리밍과 RabbitMQ를 처리하는 쓰기 모델은 대량의 이벤트 데이터를 적재하고, 이를 전처리하는 비즈니스 로직에 초점이 맞춰져야 합니다.

반면, 통계 조회는 사용자 인터페이스(UI)와 연결되며, 읽기 모델이 특정 요구사항(예: 특정 기간의 통계 조회, 실시간 시각화)에 맞게 설계될 필요가 있습니다.

CQRS를 사용하면 쓰기와 읽기에 대한 팀의 역할을 분리할 수 있어, 데이터 적재 로직과 조회 로직이 독립적으로 발전할 수 있습니다.


6번 : 시스템이 시간이 지남에 따라 진화하고, 여러 버전을 가질 수 있는 경우

이벤트 데이터를 처리하는 로직은 시간이 지남에 따라 새로운 이벤트 유형, 처리 방식, 또는 데이터 저장 형식을 도입해야 할 수 있습니다.

통계 조회 모델은 별도로 유지되므로, 기존 이벤트 처리 로직을 유지하면서 새로운 읽기 모델을 추가하거나 업데이트할 수 있습니다.

CQRS를 사용하면 쓰기 모델과 읽기 모델이 분리되어 있기 때문에, 하위 호환성을 보장하며 시스템을 점진적으로 개선할 수 있습니다.


7번 : 다른 시스템과의 통합

이벤트 소싱과의 통합뿐 아니라, 현재 개발중인 도메인 특성상 여러 다른 솔루션을 하나의 플랫폼에 Integration 하는 작업도 여러가지 있기 떄문에 해당합니다.

RabbitMQ와 같은 메시지 브로커를 활용한 이벤트 기반 시스템에서는 이벤트 소싱과 CQRS를 결합하면 장애 복구 및 데이터 동기화에 유리합니다.

예를 들어, 스트리밍 데이터 처리 중 일부 시스템이 장애를 겪더라도 이벤트가 저장된 상태에서 다시 재생하여 현재 상태를 재구성할 수 있습니다.

통계 조회는 결국 쓰기 작업에서 발생한 데이터를 활용하기 때문에, CQRS는 쓰기-읽기 간 일시적인 불일치 허용과 같은 유연성을 제공합니다.


📚 이벤트 소싱 & CQRS

보통 CQRS 패턴은 이벤트 소싱 패턴과 자주 같이 쓰입니다.

🧙‍♀️ 이벤트 소싱 패턴이란?

어플리케이션의 상태를 데이터베이스에 저장하는 방식 중 하나로 기존에는 데이터에서 상태 변경이 일어 날 시 컬럼의 상태를 바꾸었지만,

이벤트 소싱 패턴은 상태 자체를 저장하는 대신 상태 변경 내역(이벤트) 을 순차적으로 저장합니다.

이 방식은 이벤트의 히스토리를 기반으로 언제든 현재 상태를 재구성할 수 있는 강력한 패턴입니다.

CQRS 기반 시스템은 분리된 읽기/쓰기 모델을 가지며, 이벤트 소싱 패턴과 같이 쓸때는 이벤트 저장소(메인 저장소)가 쓰기 모델, 읽기 모델은 역정규화된 materialized view를 제공합니다.

이 View들은 어플리케이션의 요구사항에 최적화 되어 있어 조회 성능을 극대화 시킬 수 있습니다.


특정 시점의 실제 데이터를 사용하는 대신 쓰기 저장소로서 이벤트 스트림을 사용하는 것은 하나의 aggregate에 대한 병합 충돌을 방지하고 성능과 확장성을 극대화합니다.

그리고 이벤트들은 비동기적으로 읽기 저장소의 materialized view들을 생성하고 변경하는 데 사용됩니다.

이벤트 저장소가 공식 메인 저장소이기 때문에 시스템이 변경되거나 단순히 읽기 모델이 변경되었을 때,

materialized view를 삭제하고 과거의 모든 이벤트를 다시 실행하여 새로운 형태로 생성하는 것도 가능하며 materialized view는 사실상 읽기 전용 캐시라고 보면 됩니다.


CQRS와 이벤트 소싱을 같이 사용할 때 고려해야 할 사항

  • 읽기/쓰기 저장소가 분리되어있는 시스템에서는, 이벤트가 실행되어 저장소가 수정되기 전 약간의 딜레이가 있을 수 있습니다.
  • 이 패턴은 시스템의 복잡성을 증가시키고, CQRS의 복잡성은 성공적인 구현을 더 어렵게 만들 수도 있습니다.
  • 하지만 이벤트 소싱 방식을 사용하면 도메인 모델링을 더 쉽게 할 수도 있고, 데이터 변경 이벤트들이 보존되기 때문에 뷰들을 리빌딩하는 것도 쉽게 할 수 있습니다.
  • 읽기 모델에서 materialized view를 만들어내는 데는 오랜 실행 시간이나 리소스가 필요할 수 있으며. 특히 오랜 기간 동안의 데이터 합계나 분석 자료와 같은 것이 필요할 땐 더더욱 그렇고 이럴 경우, 일정 간격으로 데이터의 스냅샷을 만들어냄으로써 (예: 특정 액션의 총합, 한 엔티티의 현재 상태) 해결할 수 있습니다.

참조 : Perplexity AI