CQRS: 더 나은 성능을 위한 명령과 쿼리 분리

CQRS: 더 나은 성능을 위한 명령과 쿼리 분리
Command Query Responsibility Segregation (CQRS)란 무엇인가요?
Command Query Responsibility Segregation (CQRS)는 데이터를 변경하는 작업(명령)과 데이터를 조회하는 작업(쿼리)을 분리하는 디자인 패턴입니다. 두 작업에 동일한 모델을 사용하는 대신, CQRS는 이를 두 개의 별도 경로로 나눕니다. 명령과 쿼리를 분리하면 시스템이 각 작업을 더 효율적으로 처리할 수 있어, 애플리케이션이 성장함에 따라 더 나은 성능, 더 명확한 코드, 더 쉬운 확장을 이끌어냅니다.
CQRS의 핵심 개념
명령
명령은 시스템의 데이터를 변경하는 작업으로, 데이터베이스의 쓰기 작업과 같습니다. 이는 무언가를 생성, 업데이트 또는 삭제하는 것과 같은 특정 작업을 수행하라는 요청을 나타냅니다. 명령은 데이터를 반환하지 않으며, 시스템에 "무언가를 수행하라"고 지시합니다.
예시: 사용자가 자신의 프로필 정보를 업데이트할 때, “UpdateUserProfile”과 같은 명령이 전송되어 이름, 이메일 또는 기타 시스템 세부 정보를 변경합니다.
쿼리
쿼리는 데이터를 변경하지 않고 조회하기 위한 요청으로, 즉 데이터베이스의 읽기 작업입니다. 쿼리는 데이터를 검색하거나 표시하는 것처럼 정보를 가져오는 데 사용됩니다. 명령과 달리, 쿼리는 데이터만 반환하며 어떤 것도 변경하지 않습니다.
예시: 사용자가 자신의 프로필 세부 정보를 보려고 할 때, “GetUserProfile”과 같은 쿼리가 시스템에서 관련 정보를 가져옵니다.
CQRS는 어떻게 작동하나요?
CQRS의 작동 방식을 이해하기 위한 현실 세계의 비유는 고속도로에 자동차와 트럭을 위한 별도 차선이 있다고 생각하는 것입니다. 트럭은 무거운 짐(명령)을 처리하고, 자동차는 빠르고 원활하게 이동합니다(쿼리). 각자의 차선에 머물게 하면 교통 흐름이 더 좋아지고, 두 작업 모두 더 효율적으로 수행됩니다.
CQRS는 명령(데이터 수정)과 쿼리(데이터 조회)의 처리를 명확히 분리하여 작동합니다. 데이터 삽입이나 업데이트와 같은 명령이 발행되면, 변경 사항을 처리하도록 설계된 전용 경로를 통해 흐릅니다. 시스템은 이러한 명령을 정확하게 처리하며, 종종 관련 비즈니스 로직이나 이벤트를 트리거합니다. 반면 쿼리는 빠르고 효율적인 데이터 읽기에 최적화된 다른 경로를 통해 전달됩니다.
CQRS에서 명령과 쿼리를 분리한다고 해서 반드시 각각에 대해 다른 데이터베이스를 사용해야 하는 것은 아니라는 점이 중요합니다. 핵심은 동작과 책임을 분리하는 것이며, 이는 필요에 따라 동일한 데이터베이스 내에서 또는 여러 데이터베이스에 걸쳐 수행될 수 있습니다.
그림- CQRS 고수준 아키텍처.png
그림: CQRS 고수준 아키텍처
예시: 사용자 등록 시스템
사용자가 등록하면 RegisterUserCommand라는 명령이 트리거됩니다. 이 명령은 새 사용자를 생성하고, 세부 정보를 검증하며, 데이터베이스에 추가합니다. 내부적으로는 이 변경 사항을 기록하는 `UserRegisteredEvent`와 같은 이벤트가 저장됩니다.
나중에 시스템의 다른 부분(또는 사용자)이 사용자의 세부 정보를 확인해야 할 때, GetUserProfileQuery라는 쿼리가 사용됩니다. 이 쿼리는 어떤 수정도 하지 않고 데이터베이스에서 사용자의 프로필 데이터를 조회합니다.
CQRS와 Event Sourcing
Event Sourcing은 CQRS와 함께 자주 사용되는 기법입니다. 기존 시스템에서는 데이터가 변경될 때(예: 사용자가 프로필을 업데이트할 때) 데이터의 최종 상태만 저장합니다. 예를 들어 사용자가 이메일 주소를 변경하면, 데이터베이스에는 새 이메일만 저장되고 이전 이메일은 사라집니다.
Event Sourcing에서는 최종 상태만 저장하는 대신, 모든 변경 사항을 별도의 이벤트로 저장합니다. 따라서 사용자 프로필 업데이트의 경우 "User created", "Email updated", 또는 "Profile picture changed"와 같은 이벤트를 저장합니다. 각 이벤트는 무엇이 언제 발생했는지를 기록합니다. 이러한 이벤트는 시스템의 모든 것에 대한 이력 로그처럼 순서대로 저장됩니다.
이러한 이벤트는 시스템의 단일 진실 공급원이 됩니다. 명령이 실행되면(예: 사용자의 프로필 업데이트), 새로운 이벤트가 생성되어 이 이벤트 목록에 추가됩니다. 시간이 지나면서 이러한 모든 이벤트는 변경 사항의 전체 기록을 나타내도록 쌓입니다.
시스템의 현재 상태를 쿼리하거나 확인해야 할 때(예: 사용자 프로필 가져오기), 시스템은 이러한 이벤트를 올바른 순서로 재생하여 현재 상태를 재구성할 수 있습니다. 예를 들어, 현재 이메일과 프로필 세부 정보를 표시하기 위해 "User created" 및 "Email updated" 이벤트를 재생할 수 있습니다.
CQRS와 CQS의 차이점
CQRS (Command Query Responsibility Segregation)와 CQS (Command Query Separation)는 모두 명령과 쿼리의 구분을 강조하는 설계 원칙이지만, 몇 가지 측면에서 약간 다릅니다.
| Aspect | CQS (Command Query Separation) | CQRS (Command Query Responsibility Segregation) |
|---|---|---|
| Definition | 메서드는 명령(데이터 수정)이거나 쿼리(데이터 조회) 중 하나입니다. | 시스템은 명령과 쿼리를 별도의 모델 또는 경로로 분리합니다. |
| Complexity | 메서드 내에서 적용되는 단순한 원칙입니다. | 더 복잡하며, 명령과 쿼리를 위한 별도의 모델 또는 계층을 포함합니다. |
| Use Case | 명령과 쿼리가 동일한 구조 내에서 처리되는 더 단순한 시스템에 적합합니다. | 읽기와 쓰기를 별도로 최적화할 수 있는 크고 복잡한 시스템에 이상적입니다. |
| Data Handling | 명령과 쿼리 모두에 동일한 데이터 모델이 사용됩니다. | 명령과 쿼리는 서로 다른 데이터 모델이나 심지어 데이터베이스를 사용할 수 있습니다. |
| Focus | 주로 메서드가 단일 책임을 가진다는 사실에 중점을 둡니다. | 시스템 전반의 확장성, 성능, 책임 분리에 중점을 둡니다. |
| Scalability | 둘 다 하나의 모델에서 처리되므로 시스템 확장성에 미치는 영향이 적습니다. | 명령과 쿼리가 독립적으로 처리되므로 높은 확장성을 제공합니다. |
| Flexibility | 두 작업이 하나의 구조에 묶여 있으므로 유연성이 제한적입니다. | 더 큰 유연성을 제공합니다. 명령과 쿼리에 서로 다른 데이터베이스나 저장 메커니즘을 사용할 수 있습니다. |
| Event Sourcing | 일반적으로 사용되지 않습니다. | 시간이 지남에 따른 모든 변경 사항을 추적하기 위해 이벤트 소싱과 함께 자주 사용됩니다. |
표: CQS와 CQRS의 차이점
CQRS의 사용 사례
다음은 CQRS가 유용한 몇 가지 일반적인 시나리오입니다.
높은 읽기/쓰기 워크로드
읽기 작업과 쓰기 작업 수 사이에 상당한 불균형이 있는 시나리오에서는 CQRS가 이상적입니다. 예를 들어, 전자상거래 플랫폼에는 제품 정보를 조회하는 사용자(읽기)가 많지만 장바구니를 업데이트하거나 구매하는 사용자(쓰기)는 더 적을 수 있습니다. CQRS를 사용하면 시스템이 쓰기 작업에 영향을 주지 않고 높은 읽기 요청량을 처리하도록 쿼리 측을 독립적으로 확장할 수 있습니다.
복잡한 비즈니스 로직
데이터 수정(명령)에만 적용되는 복잡한 비즈니스 규칙을 처리할 때, CQRS는 이러한 복잡성을 격리함으로써 도움이 됩니다. 예를 들어, 금융 시스템에서 거래가 엄격하게 규제되는 경우 명령 측은 엄격한 검증 및 처리 규칙을 적용할 수 있습니다. 반면, 쿼리 측은 단순하게 유지되며 계좌 잔액이나 거래 내역을 조회하는 데 집중합니다.
이벤트 소싱과 감사 추적
모든 변경 사항에 대한 전체 감사 추적을 유지해야 하는 시스템에서 CQRS는 이벤트 소싱과 잘 어울립니다. 예를 들어, 의료 애플리케이션에서는 환자의 의료 기록에 대한 모든 변경 사항을 이벤트로 저장할 수 있어, 시스템이 누가 언제 변경했는지 쉽게 추적할 수 있으며, 쿼리는 기록의 현재 상태에 빠르게 접근할 수 있습니다.
실시간 분석
소셜 미디어 플랫폼과 같이 실시간 분석이 필요한 애플리케이션의 경우, CQRS는 사용자 데이터(예: 좋아요, 공유)에 대한 지속적인 업데이트를 처리하는 동시에 실시간 인사이트(예: 트렌딩 토픽)를 위한 쿼리를 처리할 수 있습니다. 쿼리 측은 명령 측에서 진행 중인 업데이트의 영향을 받지 않고 속도에 맞게 최적화될 수 있습니다.
마이크로서비스 아키텍처
마이크로서비스 기반 시스템에서 CQRS는 명령과 쿼리를 독립적으로 처리하기 위해 서로 다른 서비스를 제공합니다. 예를 들어, 물류 회사에서는 한 서비스가 주문 처리(명령)를 관리하고, 다른 서비스가 추적 및 보고(쿼리)를 처리하여 시스템의 유연성과 유지보수성을 향상시킬 수 있습니다.
CQRS를 지원하는 도구 및 프레임워크
여러 도구와 프레임워크는 CQRS를 더 쉽게 구현할 수 있게 해줍니다. 다음은 몇 가지 인기 있는 옵션에 대한 간략한 개요입니다:
Axon Framework (Java)는 CQRS와 이벤트 소싱을 지원하는 완전한 기능을 갖춘 프레임워크입니다. 명령, 쿼리, 이벤트를 처리하기 위한 내장 컴포넌트를 제공하므로 복잡한 Java 애플리케이션에서 인기 있는 선택지입니다.
MediatR는 명령과 쿼리의 분리를 단순화하는 .NET 애플리케이션용 또 다른 경량 라이브러리입니다. 요청을 깔끔하고 체계적인 방식으로 라우팅하는 데 도움이 되며, 이는 소규모에서 중간 규모 시스템의 CQRS에 적합합니다.
Laravel Command Bus는 Laravel 프레임워크의 일부입니다. 개발자는 이 도구를 사용하여 명령과 쿼리를 별도로 정의합니다. CQRS를 구현하려는 PHP 기반 애플리케이션에 훌륭한 옵션입니다.
EventStore는 이벤트 소싱을 위해 설계된 특수 데이터베이스로, CQRS와 함께 사용되는 경우가 많습니다. 단순히 데이터만 저장하는 것이 아니라 이벤트를 저장하여, 과거 이벤트를 쉽게 재생해 시스템 상태를 재구성할 수 있게 합니다.
NServiceBus는 분산 시스템 전반에서 명령, 이벤트, 메시지를 처리함으로써 CQRS와 잘 작동하는 메시징 프레임워크입니다. CQRS를 사용하는 마이크로서비스 아키텍처에 이상적입니다.
CQRS의 장점
향상된 확장성과 성능
명령과 쿼리를 분리함으로써 CQRS는 각 부분을 독립적으로 확장할 수 있습니다. 예를 들어, 애플리케이션이 쓰기 작업(명령)보다 훨씬 더 많은 읽기 작업(쿼리)을 처리한다면, 명령 측에 영향을 주지 않고 쿼리 측을 확장하여 전체 성능을 향상시킬 수 있습니다.
더 명확한 코드 구성
CQRS는 관심사의 깔끔한 분리를 촉진합니다. 명령은 변경을 수행하고, 쿼리는 데이터 조회를 처리합니다. 각 시스템 부분에 명확한 책임이 있기 때문에 코드를 이해하고 유지보수하기가 더 쉬워집니다.
복잡한 비즈니스 규칙의 더 나은 처리
CQRS를 사용하면 데이터 업데이트와 관련된 복잡한 비즈니스 규칙은 명령 측에서 처리하고, 데이터를 읽는 데는 더 단순하고 효율적인 쿼리를 사용할 수 있습니다. 이러한 분리는 시스템에 과부하를 주거나 코드를 지나치게 복잡하게 만들지 않고도 복잡한 로직을 관리하는 데 도움이 됩니다.
읽기 데이터와 쓰기 데이터에 대한 데이터베이스 선택의 유연성
CQRS는 명령과 쿼리에 서로 다른 데이터베이스를 사용할 수 있는 열린 선택권을 제공합니다. 예를 들어, 빠른 읽기(쿼리)를 위해 고도로 최적화된 NoSQL 데이터베이스를 사용하고, 쓰기(명령) 중에는 관계형 데이터베이스를 사용할 수 있습니다. 이러한 유연성 덕분에 각 작업에 가장 적합한 도구를 선택할 수 있습니다.
CQRS의 과제와 고려 사항
증가된 복잡성
CQRS를 사용하면 두 개의 별도 모델을 관리해야 합니다. 하나는 명령(쓰기)을 처리하기 위한 모델이고, 다른 하나는 쿼리(읽기)를 처리하기 위한 모델입니다. 두 모델을 모두 유지 관리하고 업데이트해야 하므로 코드베이스의 복잡성이 증가하며, 시스템을 개발하고 유지 관리하는 데 필요한 노력이 늘어납니다.
동기화 과제
명령 측과 쿼리 측을 동기화된 상태로 유지하는 것은 까다로울 수 있습니다. 명령을 통해 데이터가 기록되면 변경 사항이 쿼리 측에 반영되기까지 시간이 걸릴 수 있어 잠재적인 지연이 발생할 수 있습니다. 시스템 전반에서 정확한 데이터를 보장하려면 이러한 동기화를 관리하는 것이 중요합니다.
CQRS가 과한 선택인지 판단하기
CQRS는 단순한 CRUD(Create, Read, Update, Delete) 애플리케이션에는 필요하지 않습니다. 데이터 요구사항이 단순한 시스템에서는 CQRS의 추가 복잡성이 그만한 가치가 없을 수 있습니다. CQRS는 읽기와 쓰기 양이 많거나 복잡한 비즈니스 로직을 가진 복잡한 시스템에 가장 적합합니다.
비용
CQRS를 구현하려면 명령과 쿼리를 위한 별도 데이터베이스를 유지 관리하는 등 추가 인프라가 필요한 경우가 많습니다. 이는 특히 양쪽을 모두 확장할 때 하드웨어 및 클라우드 서비스 비용을 증가시킬 수 있습니다. 이러한 작업을 처리하기 위해 여러 기술이 필요해지면 비용이 상승할 수 있습니다.
CQRS가 벡터 데이터베이스와 빅데이터에서 수행하는 역할
Milvus와 같은 벡터 데이터베이스에서 CQRS는 다양한 유형의 작업을 처리하는 구조화된 방식을 제공합니다.
명령: 여기에는 대량의 벡터(예: 머신 러닝 모델이 생성한 벡터 임베딩)를 삽입하거나 업데이트하는 작업이 포함됩니다. Milvus에서 명령은 데이터베이스의 상태를 업데이트합니다.
쿼리: 이러한 작업은 유사도 검색을 사용해 대규모 데이터셋에서 유사한 항목을 찾는 것처럼 벡터를 검색합니다. Milvus 쿼리는 특정 기준과 일치하는 고차원 벡터를 반환할 수 있으며, 추천 시스템 및 검색 증강 생성(RAG)과 같은 실시간 분석 또는 AI 애플리케이션에서 자주 사용됩니다.
예시: 실시간 분석 쿼리 처리 vs. 벡터 삽입/업데이트
Zilliz Cloud(Milvus의 관리형 버전)에서 AI 기반 추천 시스템을 운영한다고 가정해 보겠습니다. 새로운 사용자 데이터나 제품 벡터가 생성되면 시스템은 최신 임베딩으로 벡터 데이터베이스를 업데이트하기 위한 명령을 실행합니다. 이러한 명령은 최신 벡터가 Milvus에 삽입되거나 업데이트되도록 보장합니다.
동시에 추천을 위해 가장 유사한 벡터를 검색하는 쿼리를 사용하여 실시간 분석이 수행됩니다. 쿼리 측은 명령 측에서 지속적으로 발생하는 벡터 삽입의 영향을 받지 않고 관련 데이터를 빠르게 찾도록 최적화되어 있습니다.
결론
CQRS는 명령(데이터 변경)과 쿼리(데이터 검색)를 분리하여 복잡한 시스템을 관리하는 강력한 방법을 제공합니다. 이러한 분리는 특히 복잡한 비즈니스 로직을 가진 대규모 애플리케이션에서 확장성, 성능, 코드 명확성을 향상시킵니다. CQRS는 어느 정도 복잡성을 추가하지만, 실시간 분석, 높은 읽기/쓰기 워크로드 또는 이벤트 소싱을 처리하는 시스템과 같은 적절한 시나리오에서 사용될 때 상당한 이점을 제공합니다. 그러나 더 단순한 애플리케이션에서는 추가 복잡성이 필요하지 않을 수 있습니다. 결론적으로 CQRS는 적절하게 적용될 때 시스템 효율성과 유연성을 향상시키는 가치 있는 도구입니다.
CQRS FAQ
- CQRS 사용의 주요 이점은 무엇인가요?
CQRS는 데이터 업데이트(명령)와 데이터 검색(쿼리)의 책임을 분리하여 시스템 확장성, 성능, 코드 명확성을 향상시킵니다. 각 측면을 독립적으로 최적화할 수 있으므로, 대용량 또는 복잡한 시스템에 유용합니다.
- 언제 CQRS 사용을 피해야 하나요?
CQRS는 읽기와 쓰기를 별도로 처리할 필요가 없는 단순한 CRUD 애플리케이션에는 불필요한 복잡성을 추가할 수 있습니다. 애플리케이션에 중요한 성능 요구사항이나 복잡한 비즈니스 로직이 없다면, CQRS는 과도한 설계일 수 있습니다.
- CQRS에서 명령과 쿼리에 별도의 데이터베이스를 사용해야 하나요?
아니요, 명령과 쿼리에 별도의 데이터베이스를 사용하는 것은 필수가 아닙니다. CQRS에 서로 다른 데이터베이스를 사용할 수는 있지만, 핵심은 코드에서 동작과 책임을 분리하는 것입니다. 애플리케이션의 요구에 적합하다면 단일 데이터베이스를 계속 사용할 수도 있습니다.
- CQRS는 이벤트 소싱과 어떻게 함께 작동하나요?
이벤트 소싱에서는 시스템의 모든 변경 사항이 이벤트로 저장됩니다. CQRS는 이벤트를 생성하는 명령을 처리하고, 쿼리를 사용해 해당 이벤트를 재생하여 시스템의 상태를 재구성함으로써 이를 보완합니다. 따라서 변경 사항의 전체 이력이 유지되어 감사하거나 디버깅하기가 더 쉬워집니다.
- CQRS가 실시간 분석 애플리케이션의 성능을 향상시킬 수 있나요?
네, CQRS는 실시간 분석에 유용합니다. 시스템이 지속적인 업데이트(명령)를 실시간 데이터 쿼리와 별도로 처리할 수 있게 하여, 각각을 목적에 맞게 최적화하고 하나가 다른 하나를 느리게 만드는 것을 방지합니다.
- CQRS 구현의 어려움은 무엇인가요?
주요 어려움에는 복잡성 증가, 명령 모델과 쿼리 모델 간 동기화 관리, 최종적 일관성 문제 처리, 별도의 경로나 데이터베이스 유지로 인해 발생할 수 있는 더 높은 인프라 비용 등이 포함됩니다.


