CQRS를 이용한 매칭 쿼리 최적화
들어가기에 앞서
이 글은 제가 재직 중인 회사 ‘이지태스크’의 동료에게 공유한 글입니다. 이지태스크는 고객의 업무요청서를 토대로 프리랜서를 매칭하는 플랫폼으로 언젠가 서비스가 성장할 경우 CQRS 적용이 필수적이라 판단되어 저의 학습 내용을 정리 및 공유하였습니다.
개요
현재 이지태스크는 고객의 업무 요청에 맞는 프리랜서 목록을 추출하기 위해 RDB(MariaDB)에 저장된 프리랜서의 업무 가능 시간, 가능 업무, 등급, 나이, 성별 등 다양한 정보를 활용하고 있습니다.
하지만 기존 RDB 기반의 프리랜서 목록 추출 방식은 성능과 유지보수 측면에서 여러 한계를 가지고 있으며ElasticSearch 등의 NoSQL과 CQRS 패턴을 도입함으로써 해결할 수 있습니다. 다만, 이러한 접근은 비용 증가와 시스템 복잡도 상승이라는 trade-off가 존재하기 때문에, 하술할 이유로 현재 이지태스크의 상황에서는 즉시 적용하기에는 무리가 있습니다.
따라서 저는 CQRS를 활용한 매칭 쿼리 최적화를 직접 구현하는 대신 기초적인 개념을 간략하게 제시하고자 합니다. 향후 서비스가 성장하여 시스템 고도화가 진행되기를, 그리고 제 글이 소소하게나마 도움이 되기를 기원하며 글을 남깁니다.
기존 조회 모델의 한계
기존 RDB를 이용한 조회모델은 아래의 한계를 가지고 있습니다.
- 최적화의 어려움
1)반정규화의 한계
- (1) 반정규화 모델링의 어려움
- RDB의 특성 상 반정규화의 설계 자체에 한계가 있습니다.
- 예를 들어 업무가능시간의 경우 요일과 시간으로 조합되어 있습니다. 이러한 정보는 단일한 필드로 정규화하기에는 성능 상의 단점이 큽니다.
- 업무가능시간 데이터 예시
| 프리랜서 이름 | 월 | 화 | 수 | 목 | … |
|---|---|---|---|---|---|
| 철수 | 9:00 ~ 10:00 | 9:00 ~ 22:00 | 9:00 ~ 15:00 | x | |
| 영희 | 8:00 ~ 23:00 | x | x | 9:00 ~ 12:00 |
- (2)인덱싱의 한계
- 설령 반정규화에 성공하였더라도 다양한 알고리즘을 적용하기에는 인덱싱에 한계가 있습니다.
- InnoDB 계열의 특성 상 복합인덱스를 사용하려면 조회 쿼리에서 인덱스의 컬럼을 순차적으로 사용해야 합니다.
- 그러므로 다양한 알고리즘을 사용하려면 여러 개의 복합인덱스를 생성하거나 여러 알고리즘이 하나의 복합인덱스에 의존해야 합니다. 전자의 경우 INSERT, UPDATE의 성능이 저하되며 후자의 경우 조회 성능 최적화를 포기해야 합니다.
2)정규화의 한계
- 반정규화를 하지 않을 경우 여러 테이블을 JOIN 해야 하므로 이 또한 DB에 부하가 발생합니다.
- 또한 조회 쿼리가 매우 복잡해지는 단점이 있습니다.
- 생성모델과 조회모델이 강하게 결합되어 있음
기존의 구조는 CRUD 로직이 모두 동일한 도메인 엔티티를 바라보고 있습니다. 이런 구조는 단순 CRUD에는 적합하지만 비즈니스 로직이 복잡해질 수록 단일 도메인 엔티티에게 과도한 책임을 부여하게 되며 기술부채로 이어질 수 있습니다.
CQRS의 장점과 타당성
CQRS란?
CQRS(Command Query Responsibility Segregation)란 명령(Command)과 조회(Query)의 책임을 분리하는 아키텍처 패턴입니다.
CQRS 패턴을 적용하여 기존의 RDB는 Command 모델로 유지하고 비동기로 프리랜서 목록 조회에 사용될 Query 모델을 별도로 사용할 수 있습니다. 이 경우 Query 모델을 구현하기 위해 ElasticSearch 등의 NoSQL을 사용하여 성능최적화와 간결한 쿼리 구현이 가능합니다.
CQRS의 장단점
그러나 CQRS가 만능의 아키텍쳐는 아닙니다. (사실 어떤 아키텍쳐든 만능은 아닙니다)
일반적으로 알려진 CQRS의 장단점은 아래와 같습니다.
CQRS의 장단점
장점
- 책임 분리로 인한 코드 간결성
- Command와 Query가 서로 독립된 모델/서비스로 구성되므로 각 책임이 명확해지고, 코드의 복잡도가 줄어듭니다.
- 성능 최적화 가능
- 조회(Query) 시스템은 읽기 성능을 극대화하도록 별도로 튜닝할 수 있고, Command 시스템은 일관성과 검증에 집중할 수 있어 각 목적에 맞는 최적화가 가능합니다.
- 확장성 (Scalability)
- 읽기와 쓰기 트래픽의 비중이 다를 때, 각 측면을 독립적으로 확장할 수 있습니다.
- 예: 읽기 요청이 많은 시스템에서 Query만 수평 확장
- 이벤트 소싱과의 궁합
- CQRS는 Event Sourcing과 잘 어울려, 변경 이력을 저장하고 재현할 수 있는 시스템을 구축하기에 적합합니다.
단점
- 시스템 복잡도 증가
- Command/Query 모델을 따로 관리해야 하므로 인프라 구성과 개발, 운영이 복잡해집니다.
- 데이터 동기화 문제
- Command에서 발생한 변경이 Query 모델에 반영되기까지 지연 시간이 생길 수 있으며, 결과적 일관성을 고려해야 합니다.
- 개발 및 학습 비용 증가
- CQRS를 제대로 적용하려면 아키텍처에 대한 충분한 이해가 필요하고, 개발자에게 학습 부담이 될 수 있습니다.
- 적절하지 않은 과도한 도입
- 단순한 CRUD 위주의 시스템에서는 오히려 CQRS가 과도한 설계가 되어 유지보수에 방해가 될 수 있습니다.
CQRS 도입의 타당성
현시점에서 CQRS 도입은 적절하지 않습니다. 이유는 크게 아래 3가지를 꼽을 수 있습니다.
- ElasticSearch 등 신규 인프라 도입으로 인한 비용 증가
- 가장 중요한 이유입니다. 현재 상황 상 비용을 최대한 절감해야 합니다.
- 성능 비효율 감수 가능
- 현재 데이터의 수와 트래픽이 크지 않아 당장은 성능 비효율을 감수할 수 있습니다.
- 시스템 복잡도 증가
- 인원 변동 가능성을 감안하면 러닝커브를 감수할 만큼 인력 풀이 충분하지 않습니다.
구현 개요
상술한대로 당장 CQRS를 도입하는 것은 부적절합니다. 따라서 CQRS 적용 시 사용할 기술과 개발적으로 고려할 요소에 대해 간략하게 제안하는 선에서 글을 마치고자 합니다.
사용할 기술
1)ElasticSearch vs MongoDB
ElasticSearch 와 MongoDB 모두 반정규화 및 고속검색에 적합합니다. 다만 아래 2가지 이유로 ElasticSearch가 더 적합한 것으로 보입니다
- ElasticSearch가 더 복잡한 검색에 특화되어 있으며 특히 Score 알고리즘을 적용하여 매칭 알고리즘에 적합합니다.
- 로그 모니터링 시스템으로 ELK Stack을 사용할 경우 ElasticSearch 인스턴스를 공유할 수 있어 인프라 비용을 절감할 수 있습니다.
Transaction Outbox vs CDC
- 아래 2가지 이유로 Transaction Outbox만 사용하여 구현하는 것이 더 적합한 것으로 보입니다.
- 별도의 Connector와 메시지 큐를 필요로 하는 CDC와 달리 Transaction Outbox는 RDB 만으로 구현할 수 있어 러닝 커브가 작습니다.
- Transaction Outbox는 별도의 메시지 큐가 필요 없으므로 인프라 비용을 절약할수 있습니다.
- 단 서비스의 성장 속도와 비즈니스의 복잡도를 고려하여 도입 시점에 종합적인 판단이 필요합니다.
개발적으로 고려할 요소
1)이벤트 리스너를 이용한 이벤트 기반 아키텍쳐
- 도메인 엔티티 생성, 업데이트, 삭제 등의 이벤트 발생 시 Query Model 관련 메소드를 직접 호출하기 보다는 이벤트 리스너를 이용하여 구현할 경우 도메인 간 결합도를 낮출 수 있습니다.
2)Transaction Outbox 패턴으로 결과적 일관성 보장
- RDB와 NoSQL을 동기적으로 트랜잭션을 보장하는 것은 어렵습니다. 따라서 Transaction Outbox 혹은 CDC 등의 방법을 이용하여 비동기로 결과적 일관성을 보장해야 합니다.
3)타임스탬프 기반 변경 이벤트 반영
쿼리 모델을 처음 적용하거나 큰 변경이 있을 경우 특히 데이터의 순서를 고려해야 합니다.
(1)시나리오
프리렌서의 레벨이 변경되는 시나리오를 가정합니다.
| 이벤트 발생 시각 | 프리랜서 이름 | 레벨 변경 | 이벤트 ID | 비고 |
|---|---|---|---|---|
| 9:00 | 철수 | 1 | 1 | CREATE |
| 10:00 | 영희 | 1 → 2 | 2 | UPDATE |
| 13:00 | 민수 | 2 → 3 | 3 | UPDATE |
| 14:00 | 민수 | 3 → 4 | 4 | UPDATE |
| 15:00 | 영희 | 2 → 3 | 5 | UPDATE |
CQRS 파이프라인 구현이 12:00시에 완료되었을 경우 Transaction Outbox (혹은 메시지 큐)에 이벤트 ID 3,4,5만 발행됩니다.
(2)고려사항
Query 모델을 온전하게 구현하려면 아래 2가지 case를 고려해야 합니다.
1)파이프라인 구현 이전의 데이터를 반영하지 않는 case
- 만약 발행된 이벤트만 소비할 경우 철수의 데이터(이벤트 ID 1)가 누락되는 문제가 발생합니다.
2)특정 시점의 스냅샷과 발행된 이벤트를 동시에 적용하는 case
- 이벤트 순서로 인해 일관성에 문제가 발생할 수 있습니다.
- 예를 들어 발행된 이벤트의 소비가 더 빨랐을 경우 이벤트 ID 5 가 3보다 먼저 적용되었을 경우 최종적으로 영희의 레벨이 2가 되는 문제가 발생합니다.
(3)해결방안
타임스탬프 기반 변경 이벤트 반영으로 안전하게 데이터를 반영할 수 있습니다. 구체적인 방법은 아래와 같습니다.
1)파이프라인을 구축하여 이벤트를 적재하되 당장 소비하지 않고
2)파이프라인 구축 이후의 특정 시점 의 스냅샷을 Query 모델에 선반영 후
3)특정 시점 이후의 이벤트만 소비하면 됩니다.
예를 들어 13:30 시점의 스냅샷을 얻었을 경우
- 스냅샷을 Query 모델에 적용하여 이벤트 ID 1~ 3을 반영한 후
- 타임스탬프를 고려하여 Transaction Outbox에 적재된 이벤트 중 이벤트 ID 3은 무시하고 4,5만 반영하면 됩니다.
4)모델간 동기화 상태 및 지연 모니터링 시스템 구축
CQRS의 복잡한 아키텍쳐를 고려하여 특히 모니터링에 신경써야 합니다.
예를 들어 버그로 인해 데이터 정합성이 깨지거나 이벤트 발행, 소비가 이루어지지 않을 수 있습니다. 또한 대량의 트래픽으로 인해 이벤트 발행/소비 속도의 균형이 깨질 경우 이벤트 반영에 허용할 수 없는 수준의 지연이 발생할 수 있습니다.
이를 방지하기 위해 주기적으로 정합성 및 지연을 모니터링하는 시스템을 꼼꼼하게 구현해야 합니다.
5)멱등성
Transaction Outbox 패턴은 At-least-once delivery만 보장하므로 ElasticSearch의 Document를 멱등하게 구현해야 합니다.
마무리
저는 아직 CQRS를 실무에서 구현해본 경험은 없습니다. 따라서 글에 보완할 점이 있을 경우 언제든지 피드백 주시면 감사하게 받겠습니다.