티스토리 뷰

CQRS 패턴을 실제 서비스 환경에 적용하기 전까지는 읽기와 쓰기를 분리한다는 개념이 비교적 단순하게 느껴질 수 있습니다. 그러나 실제 트래픽이 증가하고, 서비스 기능이 확장되며, 조회 요구사항이 빠르게 변화하기 시작하면 이 패턴이 단순한 코드 구조상의 분리가 아니라 데이터 저장 전략 전체를 다시 고민하게 만드는 설계 문제라는 사실을 마주하게 됩니다.

 

이번 글에서는 Aurora(MySQL 호환)를 쓰기 저장소로 사용하고, MongoDB Atlas를 읽기 저장소로 분리하며, 두 데이터소스를 Kafka 이벤트 스트림으로 동기화하는 구조가 왜 필요했는지, 그리고 이 선택이 어떤 설계적 의미를 갖는지를 정리합니다. 특정 기술 스택을 권장하기보다는, 이러한 구조가 등장하게 된 맥락과 판단 과정을 공유하는 것이 목적입니다.

 


 

읽기와 쓰기의 요구사항은 처음부터 달랐습니다

 

서비스 초기에는 대부분 하나의 관계형 데이터베이스를 중심으로 읽기와 쓰기를 함께 처리합니다. 개발 속도도 빠르고 운영 복잡성도 낮기 때문입니다. 그러나 서비스가 성장하면서 읽기 요청의 형태와 양은 쓰기 요청과 다른 방향으로 확대되기 시작합니다.

 

쓰기 경로는 주로 상태 변경과 관련되어 있으며, 트랜잭션 일관성과 제약 조건 검증이 핵심 요구사항이 됩니다. 반면 읽기 경로는 대량 조회, 복합적인 화면 구성, 다양한 필터와 정렬 조건을 빠르게 처리해야 하는 요구로 확장됩니다. 이 두 요구를 하나의 데이터 모델에서 동시에 만족시키려는 시도는 시간이 지날수록 점점 더 어려워집니다.

 

읽기 성능을 개선하기 위해 인덱스를 추가하고 조회 최적화를 진행하면 쓰기 성능에 영향을 주기 시작하고, 쓰기 중심의 정규화된 구조를 유지하면 조회 쿼리는 점점 복잡해집니다. 결국 하나의 데이터 모델이 서로 다른 목적을 동시에 만족시키지 못하는 상황이 발생합니다.

 

이 시점에서 CQRS는 단순한 패턴 적용이 아니라, 데이터 저장 전략을 분리해야 한다는 설계적 요구로 나타납니다.

 


 

쓰기 모델을 Aurora 중심으로 유지한 이유

 

쓰기 모델은 시스템에서 발생하는 상태 변경의 출발점이며, 이 경로에서 발생하는 오류는 시스템 전체 정합성에 직접적인 영향을 미칩니다. 따라서 쓰기 모델의 저장소는 강한 트랜잭션 보장과 데이터 무결성 검증이 가능해야 했습니다.

 

Aurora(MySQL 호환)는 이러한 요구사항을 충족시키는 선택이었습니다. 기존 MySQL 생태계와의 호환성을 유지하면서도 관리형 환경에서 안정적인 트랜잭션 처리를 제공하고, 장애 상황에서도 빠른 복구가 가능하다는 점이 중요한 요소였습니다.

 

하지만 Aurora를 모든 조회 요구까지 담당하게 만드는 구조는 장기적으로 유지하기 어려웠습니다. 읽기 트래픽이 증가하면서 조회 최적화를 위한 인덱스와 쿼리 복잡도가 증가했고, 이는 곧 쓰기 처리 성능에 영향을 미치기 시작했습니다. 쓰기 모델이 조회 요구에 의해 구조적으로 제약받기 시작한 것입니다.

 

결국 Aurora는 쓰기 모델의 단일 진실 공급원으로서 역할을 명확히 하고, 조회 요구는 다른 방식으로 해결하는 방향이 필요했습니다.

 


 

읽기 모델을 MongoDB Atlas로 분리한 배경

 

읽기 모델의 요구는 쓰기 모델과 다른 기준으로 평가되었습니다. 읽기 요청은 이미 확정된 데이터를 빠르게 조회하는 것이 목적이며, 일부 데이터 동기화 지연은 허용 가능한 범위였습니다.

 

MongoDB Atlas는 문서 기반 데이터 모델을 통해 조회에 필요한 데이터를 사전에 구성할 수 있는 장점을 갖고 있습니다. 관계형 모델처럼 복잡한 조인 없이도 하나의 문서 단위로 조회 결과를 구성할 수 있으며, 조회 요구가 변경되더라도 데이터 모델을 상대적으로 유연하게 조정할 수 있습니다.

 

특히 화면 단위 조회나 집계 데이터 제공이 많아지는 환경에서는 정규화된 데이터 구조보다 조회 중심으로 구성된 데이터 구조가 더 효과적으로 동작합니다. 읽기 모델을 별도의 저장소로 분리하면서 쓰기 모델과 다른 최적화 전략을 적용할 수 있게 되었습니다.

 

중요한 점은 읽기 모델이 쓰기 모델과 동일한 정합성을 실시간으로 유지해야 한다는 전제를 처음부터 두지 않았다는 사실입니다. 읽기 모델은 조회 성능과 확장성을 우선하는 구조로 설계되었습니다.

 


 

데이터 동기화 문제는 피할 수 없었습니다

 

쓰기 저장소와 읽기 저장소를 분리한 이후 반드시 해결해야 하는 문제가 데이터 동기화였습니다. 쓰기 저장소에서 발생한 상태 변경을 읽기 모델로 어떻게 전달할 것인지가 핵심 과제가 되었습니다.

 

처음 검토된 방법 중 하나는 데이터베이스 복제였습니다. 그러나 서로 다른 종류의 데이터베이스 간 복제를 안정적으로 구성하는 것은 현실적으로 쉽지 않았고, 읽기 모델의 구조를 자유롭게 변경하기 어려워지는 문제도 있었습니다.

 

또 다른 방법은 애플리케이션 레벨에서 두 저장소에 동시에 쓰기를 수행하는 방식이었지만, 이 방식은 트랜잭션 경계 관리와 장애 상황에서의 데이터 불일치 문제를 해결하기 어려웠습니다.

 

이러한 이유로 데이터 동기화를 다른 방식으로 접근할 필요가 있었습니다.

 


 

Kafka 기반 이벤트 동기화가 선택된 이유

 

쓰기 모델에서 발생한 상태 변경을 이벤트로 외부에 전달하고, 이를 읽기 모델이 소비하여 자체 데이터를 구성하는 방식이 최종적으로 선택되었습니다. 이 구조에서 Kafka는 데이터 변경 이벤트를 안정적으로 전달하는 역할을 수행합니다.

 

Kafka는 쓰기 트랜잭션 자체를 처리하지 않으며, 쓰기 성공 이후 발생한 상태 변경 사실을 스트림 형태로 전달하는 역할에 집중합니다. 읽기 모델은 이 이벤트를 소비하여 조회에 최적화된 데이터를 구성합니다.

 

이 구조의 중요한 특징은 쓰기 모델과 읽기 모델 간의 결합도가 낮아진다는 점입니다. 읽기 모델을 다시 구성해야 할 경우에도 이벤트 스트림을 재처리함으로써 데이터를 재구성할 수 있습니다.

 

다만 이 방식은 완전한 실시간 동기화를 목표로 하지 않습니다. 읽기 모델은 최종적 일관성을 기반으로 하며, 일정 수준의 동기화 지연은 허용되는 구조입니다. 대신 시스템 전체의 확장성과 안정성을 확보할 수 있게 됩니다.

 


 

CQRS 철학과 데이터소스 분리의 연결

 

이 구조는 CQRS의 핵심 개념인 책임 분리를 데이터 저장소 수준까지 확장한 사례라고 볼 수 있습니다. 쓰기 모델은 정합성과 트랜잭션을 중심으로 설계되고, 읽기 모델은 조회 성능과 확장성을 중심으로 설계됩니다.

 

Kafka 기반 이벤트 동기화는 이 두 모델을 느슨하게 연결함으로써 각 영역이 자신의 책임에 집중할 수 있도록 합니다. 이는 운영 복잡성을 일부 증가시키지만, 장기적으로는 시스템 변화에 대응하기 쉬운 구조를 제공합니다.

 

결과적으로 데이터 저장소 분리는 단순한 기술 선택이 아니라, 시스템 책임을 명확히 나누기 위한 설계 판단이었습니다.

 


 

설계 선택보다 중요한 것은 판단의 맥락이라고 생각합니다

 

Aurora, MongoDB Atlas, Kafka를 조합한 이 구조는 모든 시스템에서 동일하게 적용할 수 있는 정답은 아닙니다. 특정 트래픽 규모, 조회 패턴, 조직의 운영 역량이라는 조건 위에서 선택된 결과입니다.

 

이번 구조를 정리하면서 느낀 점은, 기술 선택 자체보다 중요한 것은 왜 그런 선택을 하게 되었는지를 이해하는 과정이라는 생각이었습니다. 읽기와 쓰기의 요구가 어떻게 달랐는지, 기존 방식이 어떤 한계를 가졌는지, 그리고 어떤 제약을 받아들이면서 구조를 변경했는지를 돌아보는 과정이 더 중요한 학습이었습니다.

 

비슷한 문제를 마주하게 될 때도 동일한 기술 구성을 따라야 한다기보다, 어떤 조건에서 어떤 판단을 내려야 하는지를 고민하는 데 도움이 되기를 바랍니다. 이러한 고민의 과정 자체가 개발자로서 성장하는 데 중요한 경험이 된다고 느끼고 있습니다.