프로젝트 간단 소개
평소 개발자들이 코드 리뷰를 위해 사용하던 깃허브나 슬랙보다 조금 더 새내기 개발자들이나 코드 리뷰에 있어서 좋은 경험을 해보고 싶은 개발자들을 위해 지원하는 코드리뷰 원페이지 플랫폼으로 하이라이트, 댓글 등등 다양한 방법으로 코드 리뷰를 진행할 수 있는 플랫폼!
우리 프로젝트에 맞는 기술일까?
실무에서 진행하는 프로젝트가 아닌 어떻게 보면 성장을 하기 위한 프로젝트이기 때문에 하면 당연히 좋겠지라고 생각 할 수 있지만, 이 기술이 우리 프로젝트에 맞는 기술인지 따져보고 과도한 오버 엔지니어링이 아닌지 검증해 볼 필요도 있다고 생각했다.
기획 단계에서 부터 요구사항 부분으로 많은 조회 요청을 사용자들이 불편함을 느끼지 않도록 빠르게 처리할 수 있는 시스템이 필요로 했었는데, 설계 단계에서 이런 고민을 하다보니 어느덧 클라이언트 분들의 와이어 프레임을 구경할 수 있었고, 꼭 좋은 서비스를 만들어 사용자들에게 배포까지 할 수 있었으면 좋겠다 라는 생각을 했다.
다시 돌아와서 이 프로젝트는 코드리뷰가 생성되면 여러 사람들에게 리뷰를 요청하고, 거기에 따른 리뷰, 댓글 등등 수많은 조회 요청이 생길 것으로 예상되었다. 따라서, 효율적으로 조회 요청을 처리하기 위해서 Create Update Delete의 책임이 있는 Command 모델과 Read의 책임이 있는 Query 모델을 분리하여 Query 모델을 Scale-out 한다면 이러한 요구사항들이 충족 될 것이라고 생각했다.
CQRS 설계
이전글에서 언급했다시피, CQRS 패턴은 다양한 방법으로 책임을 분리하고 이점을 가져갈 수 있지만 크게 3가지로 나눠 볼 수 있다. 그 중 첫번째 단계인 모델의 분리를 통한 개념적 분리는 앞서 이야기한 성능적인 이점을 챙기기에는 어려운 구조이다. 따라서 두번째 단계인 데이터베이스의 물리적 분리와 세번째 단계인 이벤트 소싱을 통한 이벤트 기반 아키텍처, 이 두가지 구조를 생각해 볼 수 있었다.
먼저 데이터베이스는 정규화된 데이터를 저장하기 위한 데이터베이스와 코드 리뷰의 검색을 위한 검색 엔진이 필요 했기 때문에 물리적 분리가 필요했다. 따라서 RDBMS와 NoSQL DB 두가지를 채택했고, RDBMS로는 MySQL, NoSQL로는 검색 엔진으로서 용이한 Elasticsearch를 채택했다.
데이터베이스를 정하고 나니 자연스럽게 CQRS의 두번째 단계인 데이터베이스의 물리적 분리 구조가 프로젝트에 더 알맞다고 생각했다. 그렇다면 이 두 데이터베이스의 데이터 정합성을 보장하기 위한 메세지 브로커를 정해야 하는데...
메세지 브로커로 Kafka와 RabbitMQ 두가지 후보를 두었다.
Kafka
카프카(Kafka)는 파이프라인, 스트리밍 분석, 데이터 통합 및 미션 크리티컬 애플리케이션을 위해 설계된 고성능 분산 이벤트 스트리밍 플랫폼이다.
Pub-Sub 모델의 메시지 큐 형태로 동작하며 분산환경에 특화되어 있다.
RabbitMQ
AMQP를 구현한 오픈소스 메세지 브로커이다.
Producers에서 Consumers로 메세지(요청)를 전달할 때 중간에서 브로커 역할을 한다.
이 두가지 후보 중 채택한 것은 RabbitMQ였다.
Kafka를 사용하는 기업이 많아지는 추세라 써보고 싶은 마음은 굴뚝 같았지만 Kafka는 단순한 메세지 브로커라기에는 동작 방식이 어려워 러닝 커브가 있을 것이라고 생각했고, Kafka는 RabbitMQ에 비해 상대적으로 무거워 메세지 브로커로만 사용하는 우리 프로젝트에는 아쉽지만 맞지 않다고 생각했다. (다른 캠프원들이 Kafka를 사용하고 세미나를 열어주어 사용하지 못해도 많은걸 배웠다!)
이렇게 데이터베이스와 메세지 브로커를 결정한 후 CQRS 아키텍처는 다음과 같다.
앞서 데이터베이스 분리를 통해 책임 CQRS의 본질인 Command와 Query는 분리했지만 효과적인 Scale-out과 부하 분산을 위해 Command를 담당하는 어플리케이션과 Query를 담당하는 어플리케이션을 아예 분리해버렸다. 그리고 각 어플리케이션은 정규화된 데이터 전용 모델과 읽기 전용 모델을 각 DB에 저장하고, 데이터의 정합성을 위하여 메세지 브로커로 RabbitMQ가 들어가있는 형태가 되었다.
더 고민 해보자!
꽤 그럴듯한 이유로 데이터베이스와 메세지 브로커(이벤트 큐)를 정했지만, 효과적인 유지보수를 위해서는 언제든지 바뀔수 있는 준비를 해야한다고 생각했고, 여기서 주목한 것은 RabbitMQ였다. 앞서 말한대로 RabbitMQ는 가볍고 효율적인 메세지 브로커이지만 처리속도가 상당히 느리다. 그렇기 때문에 언제든지 Kafka를 익히면 바꾸기 위하여 최대한 이 아키텍처에서 메세지 브로커의 결합도를 줄여야 한다.
이를 위해 생각한 것이 메세지 브로커로 이벤트를 발행하기 전에 어플리케이션 내에서 이벤트를 발급 하는 것이다.
무슨 소리야?
Spring 에서 이벤트를 발행해 메세지 브로커로 전송한다면 필연적으로 비즈니스 로직에 해당 메세지 브로커에 대한 로직이 남게 될 수 밖에 없다. 이러한 강한 결합을 줄이기 위해 이벤트를 비즈니스 로직에서 곧바로 메세지 브로커로 전달하지 않고, Spring Event를 이용하여 내부에서 이벤트를 발행한다. 이후 메세지 브로커와 연결되어 있는 Publish Listener가 해당 이벤트를 받은 후 메세지 브로커에게 전달하여 느슨한 결합으로 결합도를 낮춘다.
이벤트를 전달 받아 전파되는 곳 역시 마찬가지이다. 이벤트를 받은 후 동작하는 Task가 메세지 브로커와 강한 결합을 띄고 있기 때문에 이를 느슨하게 결합도를 낮추기 위해 중간에 Spring Event를 두어 전파하는 방식으로 동작한다.
실제로 이렇게 결합도를 낮추어 느슨한 결합을 한 결과, 메세지 브로커가 변경되어도 비즈니스 로직을 건드리지 않기 때문에 유지보수가 매우 편했다.
'Tech > Develop' 카테고리의 다른 글
[Spring Boot] '멀티 모듈이.. 뭐지..?' - 멀티 모듈 적용기(1) (0) | 2025.02.26 |
---|---|
MSA 환경 CQRS 패턴 적용기 - (1) CQRS (0) | 2024.08.04 |