architecture/micro service

[arch] cqrs란 - 조회와 비조회의 엄연한 분리

방푸린 2022. 1. 11. 18:09
반응형
CQRS는 Command and Query Responsibility Segregation(명령과 조회의 책임 분리)의 약자이다. 

 

**명령(Command)**와 **조회(Query)**의 책임을 분리하여 시스템의 확장성과 유지보수성을 높이는 데 목적이 있으며..

쉽게 말해서 data에 대한 read와 write는 분리되어 개발되어야 한다는 것이다.

eventually consistent!

 

1. Before

설명을 하기 전에 그림을 먼저보자. 먼저 아래는 일반적인 우리의 서버 구조이다.

before CQRS

api 만 다를 뿐 결국 같은 서비스 안에서 같은 디비를 바라보고 있다. 백 만 건의 조회가 생겨 디비 행이 걸리면 업데이트에도 지연이 있을 수 있고 반대로 몇 천 건을 수정할 경우 디비 락이 걸려 조회가 안될 수도 있다. 이를 개발하는 소스 안에서도 게으른 개발자는 등록과 조회가 같은 dto를 사용할 수도 있고 한 dto 안에서 등록용 함수, 조회용 함수 등이 덕지덕지 섞여 어느샌가 리팩토링 조차 힘든 상황이 올 수도 있다.

 

2. Initial CQRS

그렇다면 CQRS는 어떤 개념인가. 아래 그림을 보자.

initial CQRS

CQRS를 처음 제시한 사람은 CQRS에 대해 아주 단순하게 말했다.

It has one simple assumption: instead of having one big model for reads and writes, you should have two separate models. One for writes and one for reads.

단순하게 말하면 등록/조회 같은 dto 쓰지마라..ㅋ (사실 더 나아가선 application도 분리해야한다; 녹색이 어플리케이션, 노란색이 dto라고 보면 된다)

 

그리고 두 가지 개념을 들고온다: query와 command

Query should not modify anything, just return the data.
Command is the opposite one: it should make changes in the system, but not return any data.

나는 query는 조회용, command는 업데이트(수정,등록 등)용이라고 이해했다(다른 원문에서는 read와 update 라고 분리하기도하는데 공통된 의미라고 생각한다).

위 그림에서도 내부적인(service; dto, 함수 등) 분리는 된 듯 한데, DB의 분리는 아직이다. 사실 이 정도만 분리해도 괜찮다(현실적이다)는 평도 있다.

 

3. Ultimate CQRS

하지만 모든 원론이 그러하듯 궁극적인  CQRS는 한 단계 더 진화한다. 필자도 경험해봤지만 결국 대용량 서비스는 조회의 싸움이다.

특히 MSA 구조에서 데이터 조회란, 수 많은 테이블의 join의 결과체이며 이로인해 DB cpu가 100을 찍는 경우도 허다하다.

궁극적인 CQRS는 아래와 같이 제안한다.

ultimate CQRS

차이점은 read, write의 완전한 분리 그리고 event sourcing

여기서 핵심 내용은 write 와 read 할 때 사용되는 db(물리적인 db, in-memory db 등 데이터를 저장하는 모든 것을 일컬음)는 communicate이 불가하고 오로지 event를 통해서만 communicate 할 수 있다는 점이다.

위 플로우를 write -> event storage(업데이트 로그) -> events -> event handler -> write 라고 설명하는 사람도 있는데, 이 때 event storage가 전체 플로우 중 transactional 이 가능한 유일한 아이라고 설명하는 사람도 있다(write할 때도 event storage에 event생성만 하고 끝이니 딱히 transacitonal할 필요 없음). 적당히 이해하고 보면 될 듯 하다.

정리하면 아래와 같다.

write event 생성, throw error, void 만 가능
event를 생성하고 event store에 publish해서 전파하는 방식으로 데이터를 업데이트
read 만들어진 event를 subscribe 하여 적용된 데이터를 조회
query는 데이터만 return 하고 다름 side effect은 없음
event  한번만 실행됨을 보장해야 함
모든 consumer를 update 해야 함

 

위 처럼 분리되었을 경우의 아래와 같은 장점을 얻을 수 있게 된다.

  • 각각의 상황에 맞게 optimize가 가능(ex. read - view with cache)
  • read와 write가 서로 독립적(즉 서로 영향을 미치지 않는다; 조회하다 부하가 걸려 업데이트가 안 될 일이 없다).
  • 독립적인 scaling(조회가 많으면 조회 쪽 성능에 대한 개선만 하면 됨/ 관련 인프라의 scale out)이 가능

우리가 만든 프로젝트를 cqrs화 하자면..

이를 공부하면서 의문이 생기는 것은 배달 주문 메뉴 수정/실시간 잔액 조회와 같이 실시간 반영이 필요할 때 event driven의 경우 아무래도 write -> read로 가는 약간의 지연이 있을 것 같은데 이는 어떻게 해결할 수 있는지, 무시할만한 수준인건지 궁금하다..(왠지 진짜 실시간은 write 테이블을 보고 약간의 지연을 허용하면 read 테이블을 보고 그런식으로 할 것 같다..는 과거의 나ㅋㅋ)

+ 후에 실습을 해보니 write -> read 로 이벤트 전파가 거의 동시에 이루어지는 것을 확인할 수 있었다. 다만 실 운영 환경에서는 트래픽이나 시스템의 환경에 따라 약간씩 다르지 않을까 생각된다.


참고

https://threedots.tech/post/basic-cqrs-in-go/

 

Introducing basic CQRS by refactoring a Go project

It’s highly likely you know at least one service that: has one big, unmaintainable model that is hard to understand and change, or where work in parallel on new features is limited, or can’t be scaled optimally. But often, bad things come in threes. It

threedots.tech

https://www.youtube.com/watch?v=qJA6MaQ90YY 

 

728x90
반응형