CQRS 패턴의 오버엔지니어링

Monolithic Architecture 에서 서비스 규모가 커지면 MSA 를 고려할 수 밖에 없습니다.

MSA는 보통 도메인 단위로 서비스를 분리하는데,
도메인 안에서도 READ 트래픽 처리와 WRITE 트래픽 처리가 확연히 나뉠 수 있습니다.

예를 들어 쇼핑몰 서비스라면 조회 요청이 7~80% 일태고 이를 위해 조회를 처리하는 서비스, 명령을 처리하는 서비스로 다시 두가지 서비스로 나뉠 수 있습니다.

이러한 설계를 Command Query Responsibility Segregation 패턴 이라 부릅니다.

이렇게 CQRS패턴은 MSA 구조에서 자연스럽게 동시에 설계하게 되는 패턴이라고 생각해도 됩니다.

하지만 CQRS 는 복잡성을 안고가야 한다는 커다란 단점이 존재 합니다.

클래스 파일이 많아 진다는 복잡성…
코드 로직의 복잡성…
물리적으로 READ와 COMMAND DB가 분리 되어 관리 포인트 이중화…
DB가 나뉘어져 있으면 동기화는 어떻게…?
DB 동기화 처리를 위한 다시 또 등장하는 이벤트소싱 이란 기술
이벤트소싱을 처리 하기 위한 이벤트 메세징 처리 기술
MQTT? KAFKA?

관련하여 닷넷에서 CQRS패턴에 대한 오버엔지니어링에 대한 토픽을 공유해 드리고자 이 스레드를 생성 합니다.


CQRS in .NET: The Most Overengineered Solution for Simple Problems | by Is It Vritra - SDE I | Medium


Medium 포스트 무료 버전 | CQRS in .NET: The Most Overengineered Solution for Simple Problems

9개의 좋아요

CQRS… 개발은 할 수 있지만 운영하려면 피토하는

1개의 좋아요

CQRS 해보려고 하는데 고려 사항이 많은가 보네요.

1개의 좋아요

어떤 기술이든 프로젝트의 목적에 맞는 기술을 써야 할 듯 합니다. ㅎㅎ
닭 잡는데 소 잡는 칼 쓴다든지, 벌목 하는데 도끼 안 쓰고 회칼 쓰면 참 비효율적이겠죠~:grinning:

3개의 좋아요

@aroooong 질문이 있습니다.

여러 고민이 있겠지만 읽기DB과 쓰기DB 분리 시 읽기DB와 쓰기DB의 데이터 스키마 구조가 다르게 하는 것이 목적인 다른 곳에서 것처럼 설명하는데요.

물론 같은 구조로 가면 그냥 읽기 전용 인스턴스를 늘리면 될 것 같은데, 그런 것이 아니라 읽기 최적화 스키마를 한다고 하는데 이것을 어떻게 하는지 잘 모르겠네요. GPT류에 질문해도 답변에 대해 설득이 잘 않 되네요.

예를 들어 게시물과 댓글들이 있을 경우 읽기 전용에서는 어떻게 이것을 구조화 한다는 것인지 혹시 실무를 경험하신 분으로 이야기 해 주실 수 있나요?

2개의 좋아요

CQRS 패턴은 게시물+댓글이 있는 기능 단위보단
도메인 단위로 보셔야 합니다.

음… 현재 제가 회사에서 매장 통합 주문 처리 시스템을 하고 있는데, 이 기반으로 관련해서 대충(?) 설명을 드리자면 대략적으로 이런 구성의 도식이 나올 수 있습니다.


그림을 보시면 매장에서 발생되는 주문 처리를 하기 위해 각 도메인 단위로 세세하게 서비스를 나뉠 수 있습니다.

[주문 접수] [결제처리] [User] … 등이 각 독립적으로 관리 및 운영되는 서비스로 MSA 형태라고 보시면 됩니다.

그중 [결제 처리 서비스]를 보시면 다시, 명령과 조회를 처리하는 하나의 작은 서비스 형태로 다시 나뉜 모습을 볼 수 있습니다.
↑↑ 이러한 설계 부분이 바로 CQRS 입니다.

이러한 상황에서 DB도 독립적으로 나뉘어지게 됩니다.

단순히 이렇게 DB를 나누어 놓기만 한다면 성능상 큰 이점을 보긴 어렵습니다.

때문에 보통 성능 최적화에 중점을 두어 Command와 Query 데이터베이스 목적에 맞게 데이터를 효율적으로 관리 합니다.

일반적으로 Command 데이터는 정규화로 데이터를 보관하고,
Query는 조회를 빠르게 하기 위해 정규화 하지 않고 조회에 필요한 모든 데이터가 모두 포함되어 있는 denormalize 상태로 NoSQL 을 사용하여 데이터를 보관 합니다.


이런 설계로 목표가 어느정도 뚜렸해 진다면 독립적으로 나누어진 DB를 어떻게 동기화 처리를 할 수 있는지에 대해 다음 목표 방향으로 자연스럽게 흘러가게 됩니다.

이때 백앤드 서버 진영에서 제시되는 방법중 하나가 [이벤트 드리븐 아키텍처] 의 하나중 MQTT나 Kafka 같은 이벤트 처리를 통한 동기화 기법 입니다.

이렇게,

DB 동기화까지 고려하여 설계를 하고 완벽하다고 볼 수 있겠지만 또 한가지의 고려대상이 튀어 나옵니다.

바로 데이터의 히스토리 관리를 어떻게 할지 고민이 생깁니다.
이전에 Query DB는 denormalize 상태로 데이터를 관리 한다고 설명 했습니다.
이런 데이터는 바로 데이터가 어떻게 생성 되어 졌는지에 대해 히스토리 추적이 불가능합니다.

예를 들어 취소된 주문 데이터에 대해 어떤 스탭으로 주문이 취소 되었는지 역추적 하기가 힘들다는 문제가 발생 되는 것이죠.

이때 또 다시 [이벤트소싱] 이라는 기법이 등장 합니다. :joy:
[이벤트소싱] 은 이벤트 스토어를 별도로 두고
데이터를 최종적인 상태만을 기록하는 것이 아닌 이벤트가 발생되는 시점을 모두 기록해서 이벤트를 정주행 했을때 온전한 최종 상태를 볼 수 있다는 특징이 있습니다.
반대로 데이터 추적도 역주행으로 쉽게 가능하지요!

이렇게 MSA를 고려 했을때 자연스럽게 서비스간 통신을 위한
이벤트 드리븐 아키텍처 → CQRS → DB 동기화 → 이벤트 소싱 등등등 고려해야 할 부분이 많습니다.

4개의 좋아요

NoSQL 이야기하면 DBA가 반감을 드러내기 쉬워서
현업에서 NoSQL ops를 누가 할지 고통 받게 되더라고요.

다행히 회사에서 snowflake 계약해서 너무 좋아졌는데 일반적인 상황은 아닙니다.

말씀하신 형태로 경험한 적이 없어 이해가 잘 안 가네요.
CQRS에서 이 부분만 이해에 도움이 되는 게시물이나 강좌가 있을까요?
아시는 분 !!!

모놀리식 패턴 지옥에서 벗어나고자 마이크로서비스가 나오고 어차피 지옥, 결국엔 자작…

1개의 좋아요

굳이 MSA나 DB분리를 하지 않은

모놀리스 시스템에서도 CQRS를 도입하여 얻을 수 있는 이점이 많습니다.

각각 Query / Command 에 대해 취할 수 있는 전략은 아래와 같습니다.


Query

쿼리는 일반적으로 빠른 응답속도를 목표로 리팩토링하게 됩니다.
여러가지 전략이 있겠지만 제가 생각할 때 몇가지 방법은 아래와 같습니다.

  • 요청-응답 간의 시간을 측정하여 일정 범위를 초과하는 쿼리를 Warning Level 로깅한 후 이후 리펙토링

  • 쿼리 캐싱을 통해 동일한 요청에 대해 더 빠른 응답 (e.g. In-Memory, Redis)

  • Readonly 전용 DbContext 구현하여 개발편의성 개선 및 속도개선

    • EF를 사용 중이라면
      • AsNoTracking() 자동으로 추가도록 필터 적용
      • AsSplitQuery() 를 이용한 쿼리 속도 개선
    • Dapper를 사용 중이라면
      • 직접적 효율적인 쿼리를 작성/리팩토링하여 속도 개선

Command

커맨드의 경우에는 데이터에 대해 직접적인 영향을 준다는 특징이 있습니다.
그로 인해 야기되는 문제와 전략은 아래 같은 방법 들이 있습니다.

네트워크 연결 문제로 인한 중복 요청 방지

  • 클라이언트 측: Command 요청 시 임의의 유니크한 값을 헤더에 추가하여 요청합니다.
    - Header
        'x-command-key': '<Guid>'
    - Body
        '{ ... }'
    
  • 서버 측:
    Command 수신 시 요청받은 유니크 값을 캐시에서 찾습니다.
    • 캐싱 값이 없는 경우
      1. 일정 기간의 TTL과 함께 유니크 값을 캐싱합니다.
      2. Command 의 후속 처리 비즈니스 로직을 실행합니다.
    • 캐싱 값이 이미 있는 경우
      1. 중복된 요청으로 판단합니다
      2. 아래의 예시와 같은 응답으로 요청에 거절로 응답합니다.
        • HTTP 429: Too many request
        • HTTP 409: Conflict
        • HTTP 400: Bad Request

악의적인 요청 및 책임소재 파악

  • Command의 Content와 요청한 User를 로깅합니다.
  • 로깅 보관 기간을 쿼리보다 길게 보관합니다.
  • 이슈 발생 시 로그를 기반으로 역추적합니다.
5개의 좋아요

CQRS가 참 논리적으로 좋지만 일반적인 그림은 이렇죠.
서비스도 복잡하지만 persistance 부분이 정말 최악인데요.

  • Event Store는 nosql
  • Data는 sql

이종 DB를 다루어야 하고 Event Bus는 자동으로 되는게 아니고 이벤트 누락 되면 안되니까 관측하려면 머리 터지죠

Query는 매우 단순한데요. Command 처리는 매우 복잡합니다.
이는 오히려 Event Store에서 Query 가능한 Data를 적재하지 못해 발생한 사이드 이펙트라고 생각이 드네요.

세월이 흘러 흘러 nosql을 쿼리로 할 수 있는 도구가 생기면서 materialized view 를 할 수 있는 nosql도 있고 좋아 졌지만… 그렇다고 쉽다는 건 아닙니다.

4개의 좋아요