[아키텍처] CQRS 패턴

2022. 12. 20. 23:20아키텍처

CQRS 패턴에 대해서 간략하게 정리를 하려합니다. MySQL Replication 구성 및 TypeScript의 CQRS 모듈 사용을 경험해보면서 아주 깊이는 없지만, 대략적인 개념들을 정리하였습니다. 이 패턴을 처음 접한것은 '2021년 B마트 전시 도메인 CQRS 적용하기'라는 우아콘 세션이었습니다. 왜 CQRS를 적용해야하는지, 그리고 왜 이렇게 하면 좋은지에 대해서 알게되었고 저도 간단하게나마 한번 적용을 해보았습니다. 사실 '만들어 보았다.'인데 실제 해당 패턴의 장점을 극대화하려면 어느정도 규모가 있는 서비스에서 운영경험이 있어야 되지 않을까 라는 생각이 들었습니다.


 

개념과 필요성

CQRS는 Command and Query Responsibility Segregation의 약자로 데이터 저장소에 대한 읽기와 업데이트(쓰기) 작업을 구분하는 패턴입니다. 어플리케이션에서 CQRS를 구현하면 성능, 확장성 및 보안을 최대화할 수 있다고하며, CQRS로 마이그레이션하면 유연성이 생기므로 시스템이 점점 진화하고 업데이트 명령이 도메인 수준에서 병합, 충돌을 일으키지 않도록 할 수 있다고 합니다.

 

무슨 소리인가하면... 실제로 '읽기' 로직의 실행 흐름과 '쓰기'로직의 실행 흐름은 대칭형 구조가 아닙니다. 대게 서비스에서 '읽기' 워크로드의 비중이 더 높다는 이야기를 언뜻 들었던 기억이 납니다. 위 배민 세션에서는 '읽기'는 주로 고객이 조회를 진행하거나, 비즈니스 로직 처리를 위한 외부 정책, 외부 주입 데이터에 대한 조회 절차 등에 의해서 발생하고, '쓰기'는 내부 관리 목적의 데이터 생성, 업데이트, 삭제가 주로 발생한다고 제시하며 워크로드가 다르다고 하였습니다.

 

이럴경우 쓰기가 일어나는 동안 데이터를 읽어야 하는 로직에서는 Lock 매커니즘에 의해서 대기를 타야된다던지 등의 문제로 인해서 서비스의 지연이 발생할 수 있습니다. 또한 비즈니스 요구 사항이 복잡해지거나 시스템을 확장해야 한다면, 읽기와 쓰기 두 워크로드 모두를 고려해서 확장을 진행해야 할 수 있습니다. 피곤한 상황입니다.

 

제가 자료들을 보면서 이해한 바를 2가지 정도로 풀어내었는데, 핵심은 '읽기'와 '쓰기'의 워크로드에 따라서 이를 분리했을 때 더 이득이 크다면 분리하는 것이 좋다는 것입니다.

 


언제 CQRS를 적용하면 좋을까? 장점, 단점

배달의 민족 영상에서는 적용이 필요한 상황으로 아래 케이스를 제시합니다.

  • UX와 비즈니스 요구 사항이 복잡해질 때
  • 조회 성능을 보다 높이고 싶을 때
  • 데이터를 관리하는 영역과 이를 뷰로 전달하는 영역의 책임이 나뉘어 질 때
  • 시스템 확장성을 높이고 싶을 때

그리고 장점과 단점으로는 아래 내용이 있습니다.

장점 단점
서비스에 부분적인 적용 가능 -> 전체 서비스가 아닌 필요한 부분에만 적용을 하여 개선 효과를 이끌어 낼 수 있음. 구현의 복잡성 증가.
명령과 조회의 분리를 통해서 비즈니스 관점에서 유연한 모델을 생성 할 수 있음. 명령과 조회가 구분되었기에, 이에 따른 데이터 일관성을 유지하는 부분에서 이슈가 발생 할 수 있음.

 


적용하는 방법

자료들을 찾아보니 일반적으로 3단계가 있다고 합니다. ( 적용하는 방법에 대해서는 자료마다 조금씩 차이가 존재하였기에 1단계는, 제가 경험해본 내용 위주로 작성을 하려고하며, 2단계와 3단계는 배민 세션을 따라 진행하고자 합니다. )

 

1단계 : 단일 Data Store에 Command와 Query Mode을 단일 어플리케이션 내에서 분리된 계층으로 나누는 방식

 

가장 심플한 구조입니다. NestJS에서 TS로 코드를 구현할 때는 CQRS 모듈이 있어서 Query와 Command로 구분하여 로직 작성이 편리하도록 되어있기도 했습니다. 처음에 조회와 명령에 대해서 도메인을 나누는 작업이 힘들었습니다. CQRS 패턴을 적용하기 전 이미 읽기와 쓰기에 대해서 특별한 구분없이 dto를 사용했기도 하고 비즈니스 로직이 매우 간단하였기 때문에... 억지로 뭔가를 만들어내다 보니 제대로 구현이 힘들었습니다. 제가 찾아본 자료에서도 비즈니스 로직이 매우 간단한 서비스라면 분리로 인해 발생하는 서비스 구조 복잡도가 오히려 더 클 수 있으므로 도입할 필요가 없다고 했는데, 딱 그런 꼴이었습니다.

하지만 문제는 동일 DB를 사용한다는 점이었습니다. 동일 DB 사용에 대한 성능상 문제점을 개선하지 못하는 점을 보완하기 위해서 아래와 같이 DB를 이중화하여 적용이 가능하다고 생각했습니다. 하지만 여기까지 생각이 되었다면, 2단계 패턴 적용이 바람직하지 않을까 싶었습니다.

 

2단계 : 단일 어플리케이션과 명령용 데이터베이스와 조회용 데이터베이스를 분리하고 별도의 Broker를 통해서 이중화 된 데이터베이스를 동기화 하는 방식입니다.

 

이 단계부터는 저도 적용을 해본 경험이 없기에 배민 세션을 좀 따라가 보겠습니다. 배민 세션에 따르면 아래 내용으로 모델을 분리하였다고 합니다.

  1. 모델을 분리하기 : 조회 모델을 별도의 어플리케이션을 분리하여 만든다. 이 과정에서 엔티티를 사용하는 것보다, 조회를 실행할 때 엔티티의 일부만 조회를 하기 때문에 dto를 사용하는 것을 추천한다고 한다. ( 명령과 조회는 반드시 대칭적인 구조를 가지는 것이 아니기 때문)
  2. 명령 모델을 통해 데이터가 변경되는 시점에서 조회 모델 만들기: 정규화 된 데이터에서 조회 모델이 조회에 필요한 정보만을 모아서 비정규화 된 데이터를 생성하고 DB에 저장을 한다.
  3. 비정규화 된 데이터를 조회 모델의 어플리케이션에서는 가급적 join과 같은 절차를 제한하기 때문에 DB에 저장을 할 때는 NoSql을 사용하기도 한다. 데이터 포맷은 주로 json 타입을 쓰게됨
  4. 성능개선 : 비정규화 된 데이터를 조회하기 때문에 조회가 많은 경우 성능의 이슈가 발생할 수 있게 되고 캐시를 사용하여 개선 할 수 있다.

그러나 이런 구조가 되었을 경우에도 성능 이슈가 존재한다고 합니다. 하나는 조회 모델이 증가하게 될 경우 힙 메모리에 올라간 데이터가 증가하면서 성능이 저하 될 수 있다는 점입니다. 두번째는 조회가 증가하게 될 경우 캐시 트래픽도 증가 할 수 있다는 점입니다. 이런 이슈를 해결하기 위해서 아래와 같이 DB를 분리 할 수 있다고 합니다.

 

 

2단계까지 적용을 하게될 경우 어플리케이션 A에서 조회 모델을 생성하는 쪽의 리소스가 증가 할 수 있다고 합니다. 이를 해결하기 위해서 3단계를 적용하게 됩니다.

 

3단계 : 이벤트 소싱(Event Sourcing) 패턴을 적용

이벤트 소싱이란 어플리케이션 내의 모든 처리 내용을 이벤트로 전환해서 이벤트 스트림을 별도의 DB에 저장하는 방식입니다. 제가 업무를 했던 경험에서도 돈과 관련된 내용들은 history 기반으로 replay를 가능하게 하고자 명령을 하나의 이벤트로 먼저 저장을하고, 그 저장 데이터를 베이스로 뒷단의 비즈니스로직을 처리해나간 경험이 있는데, 대략 그런 내용이었습니다.

위 내용에서는 어플리케이션 A에서 발생한 이벤트를 이벤트 스토어에 저장하고, 어플리케이션 C가 그 데이터들을 가져와서 모델을 생성한 뒤 read model DB에 저장하는 구조로 분산을 한 형태라고 합니다. 여기서도 Event Driven 구조가 적용된 모습을 찾아 볼 수 있었네요.

 

 


마무리

개인프로젝트와 백엔드 직무 전환을 위한 공부를 하면서 학습한 내용을 정리했습니다. 실무에서 사용경험이 전무하기에 이렇게 글을 남기는 것만으로 차후에 도움이 되어보고자 합니다.

 

일단 가장 낮은 단계를 적용해보면서 생각했던 점은 결국 Domain 설계가 가장 중요하다고 생각했습니다. 제대로 Domain 설계가 되지 않으면 Command와 Query를 구분하는 것 부터가 고역이 된 경험을 했기에, 이런 저런 패턴을 기술적으로만 적용하는 것이 아닌 정말 비즈니스에 맞는 뭔가가 필요하구나를 느꼈습니다.

 

그리고 언젠가는 반드시 적용을 해봐야 할 패턴이라는 생각이 들었습니다. 결국 이런 패턴을 고민해보지 않는 환경이라는 것은 그만큼 규모가 작은 서비스라는 것이고 시장 경쟁력을 잃을 수도 있기 때문입니다.

 

그리고 마지막 일단 '어플리케이션 하나라도 좀 잘 만들자.'는 생각!!

 

 

 

 


참고자료