CQRS Pattern
Command-Query Responsibility Segregation
Last updated
Was this helpful?
Command-Query Responsibility Segregation
Last updated
Was this helpful?
CQRS는 데이터 저장소에 대한 읽기 및 업데이트 작업을 구분하는 패턴인 Command and Query Responsibility Segregation의 약자입니다. 애플리케이션에 CQRS를 구현하면 성능, 확장성 및 보안을 극대화할 수 있습니다. CQRS로 마이그레이션하여 생성된 유연성을 통해 시간이 지남에 따라 시스템이 더 잘 발전하고 업데이트 명령이 도메인 수준에서 병합 충돌을 일으키는 것을 방지할 수 있습니다.
기존 아키텍처에서는 데이터베이스를 쿼리하고 업데이트하는 데 동일한 데이터 모델이 사용됩니다. 간단하고 기본 CRUD 작업에 적합합니다. 그러나 더 복잡한 응용 프로그램에서는 이 접근 방식이 다루기 어려워질 수 있습니다. 예를 들어 읽기 측면에서 응용 프로그램은 다양한 쿼리를 수행하여 모양이 다른 DTO(데이터 전송 개체)를 반환할 수 있습니다. 개체 매핑이 복잡해질 수 있습니다. 쓰기 측면에서 모델은 복잡한 유효성 검사 및 비즈니스 논리를 구현할 수 있습니다. 결과적으로 너무 많은 일을 하는 지나치게 복잡한 모델로 끝날 수 있습니다.
읽기 및 쓰기 워크로드는 성능 및 확장 요구 사항이 매우 다른 비대칭인 경우가 많습니다.
작업의 일부로 필요하지 않더라도 올바르게 업데이트해야 하는 추가 열 또는 속성과 같이 데이터의 읽기 및 쓰기 표현 간에 불일치가 있는 경우가 많습니다.
동일한 데이터 집합에서 작업이 병렬로 수행될 때 데이터 경합이 발생할 수 있습니다.
기존 접근 방식은 데이터 저장소 및 데이터 액세스 계층의 로드와 정보 검색에 필요한 쿼리의 복잡성으로 인해 성능에 부정적인 영향을 미칠 수 있습니다.
보안 및 권한 관리가 복잡해질 수 있습니다. 각 엔터티가 잘못된 컨텍스트에 데이터를 노출할 수 있는 읽기 및 쓰기 작업의 영향을 받기 때문입니다.
예를 들어 마이크로 서비스 아키텍처에서 각 마이크로 서비스는 자체 데이터를 저장하고 관리해야 하지만 사용자 인터페이스는 여러 마이크로 서비스의 데이터를 표시해야 할 수 있습니다. 여러 소스에서 약간의 데이터를 수집하는 쿼리는 비효율적일 수 있으며(여러 데이터 소스에 액세스하는 데 소요되는 시간 및 대역폭, 데이터 변환에 사용되는 CPU, 중간 개체에서 사용되는 메모리) 데이터에 액세스할 때마다 반복해야 합니다.
또 다른 예는 여러 애플리케이션에 필요한 레코드 관리 데이터의 엔터프라이즈 데이터베이스입니다. 너무 많은 트랜잭션을 수행하는 너무 많은 스레드를 실행하기 위해 너무 많은 연결을 필요로 하는 너무 많은 클라이언트로 인해 오버로드될 수 있습니다. 데이터베이스가 성능 병목 현상을 일으키고 심지어 충돌할 수도 있습니다.
또 다른 예는 클라이언트가 동시에 데이터를 독립적으로 업데이트하는 동안 데이터의 일관성을 유지하는 것입니다. 각 업데이트는 일관성이 있을 수 있지만 서로 충돌할 수 있습니다. 데이터베이스 잠금은 업데이트가 동일한 데이터를 동시에 변경하지 않도록 보장하지만 여러 개의 독립적인 변경으로 인해 일관된 데이터 모델이 생성되도록 보장하지 않습니다.
CQRS는 데이터를 업데이트하는 명령과 데이터를 읽는 쿼리를 사용하여 읽기와 쓰기를 다른 모델로 분리합니다.
명령은 데이터 중심이 아니라 작업 기반이어야 합니다. ("Book hotel room", not "set ReservationStatus to Reserved")
명령은 동기식으로 처리되지 않고 비동기식 처리를 위해 대기열에 배치될 수 있습니다.
쿼리는 데이터베이스를 수정하지 않습니다. 쿼리는 도메인 지식을 캡슐화하지 않는 DTO를 반환합니다.
그런 다음 절대적인 요구 사항은 아니지만 다음 다이어그램에 표시된 것처럼 모델을 격리할 수 있습니다.
읽기 저장소는 쓰기 저장소의 읽기 전용 복제본이거나 읽기 및 쓰기 저장소가 모두 다른 구조를 가질 수 있습니다. 여러 읽기 전용 복제본을 사용하면 특히 읽기 전용 복제본이 애플리케이션 인스턴스에 가까운 분산 시나리오에서 쿼리 성능을 향상시킬 수 있습니다.
읽기 및 쓰기 저장소를 분리하면 각 저장소를 로드에 맞게 적절하게 확장할 수 있습니다. 예를 들어 읽기 저장소는 일반적으로 쓰기 저장소보다 훨씬 더 많은 부하가 발생합니다.
기존 코드에서 점진적으로 단계적으로 적용
도메인 모델에는 최소한 클라이언트가 모델 내의 도메인 개체에 대해 CRUD 작업을 수행할 수 있도록 하는 API가 있습니다.
이상적으로는 도메인 모델의 API는 단순히 데이터를 CRUD하는 것보다 더 도메인에 특화되어야 합니다. findCustomer()
대신 , placeOrder()
, transferFunds()
등과 같은 비즈니스 기능을 나타내는 상위 수준 작업을 노출해야 합니다 . 이러한 작업은 필요에 따라 데이터를 읽고 업데이트하며 때로는 단일 작업에서 두 작업을 모두 수행합니다. 비즈니스가 작동하는 방식에 맞는 한 정확합니다.
CQRS 패턴을 적용하는 첫 번째이자 가장 눈에 띄는 단계는 CRUD API를 별도의 읽기 및 쓰기 API로 분할하는 것입니다. 이 다이어그램은 이전과 동일한 도메인 모델을 보여 주지만 단일 CRUD API는 검색 및 수정 API로 분할됩니다.
두 API는 기존 도메인 모델을 공유하지만 동작을 분할합니다.
읽기 : 검색 API는 해당 상태를 변경하지 않고 도메인 모델에서 개체의 기존 상태를 읽는 데 사용됩니다. API는 도메인 상태를 읽기 전용으로 취급합니다.
쓰기 : 수정 API는 도메인 모델의 개체를 변경하는 데 사용됩니다. CUD 작업을 사용하여 변경합니다. 새 도메인 개체 만들기, 기존 개체의 상태 업데이트, 더 이상 필요하지 않은 개체 삭제. 이 API의 작업은 결과 값을 반환하지 않고 성공(ack 또는 void) 또는 실패(nak 또는 예외 발생)를 반환합니다. 생성 작업은 도메인 모델 또는 데이터 소스에서 생성될 수 있는 엔터티 키의 기본 키를 반환할 수 있습니다.
쿼리 : 결과를 반환합니다. 시스템의 상태를 변경하거나 상태를 변경하는 부작용을 일으키지 않습니다.
명령 (일명 수정자 또는 뮤테이터): 시스템의 상태를 변경합니다. 값을 반환하지 않고 성공 또는 실패의 표시만 반환합니다.
CQRS 패턴을 적용하는 두 번째 단계는 도메인 모델을 별도의 읽기 및 쓰기 모델로 분할하는 것입니다. 이는 도메인 기능에 액세스하기 위한 API만 변경하는 것이 아니라 해당 기능이 구조화되고 구현되는 방식의 디자인도 변경합니다. 이 다이어그램은 도메인 모델이 앱 상태에 액세스하는 데 사용되는 별도의 읽기 모델과 함께 도메인 개체에 대한 변경 사항을 처리하는 쓰기 모델의 기초가 됨을 보여줍니다.
쓰기 모델
상태를 변경할 때 도메인 개체의 유효한 구조를 유지하는 데에만 집중하고 비즈니스 규칙을 적용하는 데만 집중하도록 도메인 모델을 특수화하여 구현됩니다.
읽기 모델
완전한 CQRS 패턴 솔루션을 구현하는 CQRS 패턴을 적용하는 세 번째 단계는 레코드 데이터베이스를 별도의 읽기 및 쓰기 데이터베이스로 분할하는 것입니다. 이 다이어그램은 각각 자체 데이터베이스에서 지원하는 쓰기 모델과 읽기 모델을 보여줍니다. 전체 솔루션은 데이터 업데이트를 지원하는 쓰기 솔루션과 데이터 쿼리를 지원하는 읽기 솔루션의 두 가지 주요 부분으로 구성됩니다. 두 부분은 이벤트 버스로 연결됩니다.
이 단계의 설계는 이전 단계의 설계보다 훨씬 더 복잡합니다. 동일한 데이터의 복사본이 있는 별도의 데이터베이스는 데이터 모델링 및 데이터 사용을 더 쉽게 만들 수 있지만 데이터를 동기화하고 복사본의 일관성을 유지하려면 상당한 오버헤드가 필요합니다.
CQRS는 데이터베이스 동기화 유지를 지원하는 몇 가지 디자인 기능을 사용합니다.
업데이트 이벤트 게시를 위한 이벤트 버스 (필수): 쓰기 데이터베이스가 업데이트될 때마다 변경 알림이 이벤트 버스에 이벤트로 게시됩니다. 관심 있는 당사자는 이벤트 버스에 가입하여 데이터베이스가 업데이트될 때 알림을 받을 수 있습니다. 이러한 당사자 중 하나는 업데이트 이벤트를 수신하고 이에 따라 쿼리 데이터베이스를 업데이트하여 이벤트를 처리하는 쿼리 데이터베이스용 이벤트 프로세서입니다. 이러한 방식으로 쓰기 데이터베이스가 업데이트될 때마다 읽기 데이터베이스에 해당 업데이트가 수행되어 동기화를 유지합니다.
Independent scaling
CQRS를 사용하면 읽기 및 쓰기 워크로드를 독립적으로 확장할 수 있으며 잠금 경합이 줄어들 수 있습니다
쿼리 부하가 쓰기 데이터베이스에서 읽기 데이터베이스로 이동됩니다. 레코드 데이터베이스가 확장성 병목 현상이고 많은 부하가 쿼리로 인해 발생하는 경우 이러한 쿼리 책임을 언로드하면 결합된 데이터 액세스의 확장성이 크게 향상될 수 있습니다.
Optimized data schemas
읽기 측은 쿼리에 최적화된 스키마를 사용할 수 있고 쓰기 측은 업데이트에 최적화된 스키마를 사용할 수 있습니다.
두 데이터베이스의 스키마가 다를 수 있으므로 더 나은 성능을 위해 독립적으로 설계하고 최적화할 수 있습니다. 쓰기 데이터베이스는 쓰기 모델에 적합하고 데이터 업데이트를 지원하는 저장 프로시저와 같은 기능을 통해 데이터 일관성 및 정확성을 위해 최적화할 수 있습니다. 읽기 데이터베이스는 읽기 모델에 더 잘 맞고 쿼리에 더 잘 최적화된 단위로 데이터를 저장할 수 있으며 행이 클수록 더 적은 조인이 필요합니다.
Security
올바른 도메인 엔터티만 데이터에 쓰기를 수행하도록 하는 것이 더 쉽습니다.
Separation of concerns
읽기 및 쓰기 측면을 분리하면 유지 관리가 더 쉽고 유연한 모델이 될 수 있습니다. 대부분의 복잡한 비즈니스 로직은 쓰기 모델로 들어갑니다. 읽기 모델은 비교적 단순할 수 있습니다.
Simpler queries
읽기 데이터베이스에 구체화된 뷰를 저장함으로써 응용 프로그램은 쿼리할 때 복잡한 조인을 피할 수 있습니다.
Complexity
CQRS의 기본 아이디어는 simple입니다. 그러나 애플리케이션 디자인이 더 복잡(특히 Event Sourcing pattern을 포함하는 경우)해질 수 있습니다.
Messaging
Eventual consistency
읽기 데이터베이스와 쓰기 데이터베이스를 분리하면 읽기 데이터가 오래될 수 있습니다. 읽기 모델 저장소는 쓰기 모델 저장소에 대한 변경 사항을 반영하도록 업데이트되어야 하며 사용자가 오래된 읽기 데이터를 기반으로 요청을 발행한 시기를 감지하기 어려울 수 있습니다.
Consider CQRS for the following scenarios:
많은 사용자가 동일한 데이터에 병렬로 액세스하는 Collaborative domains
CQRS를 사용하면 도메인 수준에서 병합 충돌을 최소화하기 위해 충분한 세분화 명령을 정의할 수 있으며 발생하는 충돌은 명령으로 병합할 수 있습니다.
복잡한 도메인 모델을 사용하거나 일련의 단계로 복잡한 프로세스를 통해 사용자를 안내하는 작업 기반 사용자 인터페이스
쓰기 모델에는 비즈니스 논리, 입력 유효성 검사 및 비즈니스 유효성 검사가 포함된 전체 명령 처리 스택이 있습니다. 쓰기 모델은 연결된 개체 집합을 데이터 변경의 단일 단위(DDD 용어로는 aggregate)로 처리하고 이러한 개체가 항상 일관된 상태에 있도록 보장할 수 있습니다. 읽기 모델에는 비즈니스 논리 또는 유효성 검사 스택이 없으며 보기 모델에서 사용할 DTO만 반환합니다. 읽기 모델은 결국 쓰기 모델과 일치합니다.
특히 읽기 수가 쓰기 수보다 훨씬 많은 경우 데이터 읽기 성능을 데이터 쓰기 성능과 별도로 미세 조정해야 하는 시나리오
이 시나리오에서는 읽기 모델을 확장할 수 있지만 몇 가지 인스턴스에서만 쓰기 모델을 실행할 수 있습니다. 적은 수의 쓰기 모델 인스턴스도 병합 충돌 발생을 최소화하는 데 도움이 됩니다.
한 팀의 개발자가 쓰기 모델의 일부인 복잡한 도메인 모델에 집중하고 다른 팀은 읽기 모델과 사용자 인터페이스에 집중할 수 있는 시나리오.
시스템이 시간이 지남에 따라 발전할 것으로 예상되고 모델의 여러 버전을 포함할 수 있거나 비즈니스 규칙이 정기적으로 변경되는 시나리오.
다른 시스템과의 통합, 특히 이벤트 소싱과 결합하여 한 하위 시스템의 일시적 오류가 다른 시스템의 가용성에 영향을 미치지 않아야 합니다.
다음과 같은 경우에 이 패턴을 권장되지 않습니다.
도메인 또는 비즈니스 규칙은 간단합니다.
간단한 CRUD 스타일의 사용자 인터페이스와 데이터 액세스 작업이면 충분합니다.
CQRS가 가장 가치 있는 시스템의 제한된 섹션에 적용하는 것을 고려하십시오.
클라이언트 영향 : CQRS를 적용하면 데이터 저장 및 액세스 방식이 변경될 뿐만 아니라 클라이언트가 데이터에 액세스하는 데 사용하는 API도 변경됩니다. 즉, 새 API를 사용하려면 각 클라이언트를 다시 설계해야 합니다.
위험성 : 패턴 솔루션의 많은 복잡성은 두 개의 데이터베이스에서 데이터를 복제하고 동기화를 유지하는 것과 관련됩니다. 위험은 동기화 문제로 인해 오래되었거나 완전히 잘못된 데이터를 쿼리할 때 발생합니다.
최종 일관성 : 데이터를 쿼리하는 클라이언트는 업데이트에 대기 시간이 있을 것으로 예상해야 합니다. 마이크로서비스 아키텍처에서 궁극적인 데이터 일관성은 많은 경우에 주어지고 허용됩니다.
명령 큐잉 : 쓰기 솔루션의 일부로 명령 버스를 사용하여 명령을 큐에 넣는 것은 선택 사항이지만 강력합니다. 대기열의 이점 외에도 명령 개체를 변경 로그에 쉽게 저장하고 알림 이벤트로 쉽게 변환할 수 있습니다.
변경 로그 : 레코드 데이터베이스에 대한 변경 로그는 명령 버스의 명령 목록이거나 이벤트 버스에 게시된 이벤트 알림 목록일 수 있습니다. 이벤트 소싱 패턴은 이벤트 로그라고 가정하지만 해당 패턴에는 명령 버스가 포함되지 않습니다. 이벤트 목록은 기록으로 스캔하기가 더 쉬울 수 있지만 명령 목록은 재생하기가 더 쉽습니다.
키 생성 : CQS(Command Query Separation) 패턴의 Strick 해석에 따르면 명령 작업에는 반환 유형이 없습니다. 가능한 예외는 데이터를 생성하는 명령입니다. 새 레코드 또는 문서를 생성하는 작업은 일반적으로 클라이언트에게 편리한 새 데이터에 액세스하기 위한 키를 반환합니다. 그러나 명령 버스의 명령에 의해 생성 작업이 비동기적으로 호출되는 경우 쓰기 모델은 키를 반환하기 위해 클라이언트에서 콜백을 수행해야 합니다.
메시징 큐 및 주제 : 메시징은 명령 버스와 이벤트 버스를 모두 구현하는 데 사용되지만 두 버스는 메시징을 다르게 사용합니다. 명령 버스는 정확히 한 번 전달을 보장합니다. 이벤트 버스는 관심 있는 모든 이벤트 프로세서에 각 이벤트를 브로드캐스트합니다.
쿼리 데이터베이스 지속성 : 레코드 데이터베이스는 항상 영구적입니다. 쿼리 데이터베이스는 영구 캐시 또는 메모리 내 캐시가 될 수 있는 캐시입니다. 캐시가 메모리에 있고 손실된 경우 레코드 데이터베이스에서 완전히 다시 작성해야 합니다.
보안 : 솔루션의 두 부분을 사용하여 데이터 읽기 및 업데이트에 대한 제어를 별도로 적용할 수 있습니다.
별도의 쿼리 및 업데이트 모델을 사용하면 설계 및 구현이 간소화됩니다. 그러나 한 가지 단점은 와 같은 scaffolding 메커니즘을 사용하여 데이터베이스 스키마에서 CQRS 코드를 자동으로 생성할 수 없다는 것입니다. (그러나 생성된 코드 위에 사용자 customization할 수 있습니다.)
격리 수준을 높이려면 쓰기 데이터에서 읽기 데이터를 물리적으로 분리할 수 있습니다. 이 경우 읽기 데이터베이스는 쿼리에 최적화된 자체 데이터 스키마를 사용할 수 있습니다. 예를 들어 복잡한 조인이나 복잡한 O/RM 매핑을 피하기 위해 데이터의 를 저장할 수 있습니다. 다른 유형의 데이터 저장소를 사용할 수도 있습니다. 예를 들어 쓰기 데이터베이스는 관계형이고 읽기 데이터베이스는 문서 데이터베이스일 수 있습니다.
별도의 읽기 및 쓰기 데이터베이스를 사용하는 경우 동기화 상태를 유지해야 합니다. 일반적으로 이것은 쓰기 모델이 데이터베이스를 업데이트할 때마다 이벤트를 publish하도록 함으로써 수행됩니다. 이벤트 사용에 대한 자세한 내용은 을 참조하세요. 메시지 브로커와 데이터베이스는 일반적으로 단일 분산 트랜잭션에 참여할 수 없기 때문에 데이터베이스를 업데이트하고 이벤트를 publish할 때 일관성을 보장하는 데 문제가 있을 수 있습니다. 자세한 내용은 에 대한 지침을 참조하십시오.
CQRS의 일부 구현에서는 을 사용합니다. 이 패턴을 사용하면 애플리케이션 상태가 일련의 이벤트로 저장됩니다. 각 이벤트는 데이터에 대한 일련의 변경 사항을 나타냅니다. 현재 상태는 이벤트를 재생하여 구성됩니다. CQRS 컨텍스트에서 Event Sourcing의 한 가지 benefit은 동일한 이벤트를 사용하여 다른 구성 요소, 특히 읽기 모델에 알릴 수 있다는 것입니다. 읽기 모델은 이벤트를 사용하여 쿼리에 더 효율적인 현재 상태의 스냅샷을 만듭니다. 그러나 Event Sourcing은 설계에 복잡성을 더합니다.
) 패턴을 적용하여 상태를 변경하는 메서드와 그렇지 않은 메서드를 명확하게 구분합니다. 이를 위해 개체의 각 메서드는 다음 두 범주 중 하나에 속할 수 있습니다(둘 다는 아님).
한편, 도메인 개체 반환에 대한 책임은 별도의 읽기 모델로 전환됩니다. 읽기 모델은 클라이언트가 편리하다고 생각하는 구조에서 클라이언트가 원하는 데이터만 반환하도록 특별히 설계된
대기열 명령을 위한 명령 버스 (선택 사항): 보다 미묘하고 선택적인 설계 결정은 명령 버스로 다이어그램에 표시된 수정 API에서 생성된 명령을 대기열에 넣는 것입니다. 이렇게 하면 데이터베이스를 업데이트하는 여러 앱의 처리량을 크게 늘릴 수 있을 뿐만 아니라 업데이트를 직렬화하여 병합 충돌을 방지하거나 적어도 감지할 수 있습니다. 버스를 사용하면 변경 사항이 데이터베이스에 기록되는 동안 업데이트를 수행하는 클라이언트가 동기적으로 차단되지 않습니다. 오히려 데이터베이스 변경 요청이 으로 캡처됩니다 ( ) 클라이언트가 다른 작업을 진행할 수 있도록 메시지 큐에 넣습니다. 쓰기 모델은 백그라운드에서 비동기식으로 데이터베이스가 과부하되지 않고 데이터베이스가 처리할 수 있는 지속 가능한 최대 속도로 명령을 처리합니다. 데이터베이스를 일시적으로 사용할 수 없게 되면 명령이 대기하고 데이터베이스가 다시 사용 가능해지면 처리됩니다.
CQRS에는 메시징이 필요하지 않지만 메시징을 사용하여 명령을 처리하고 업데이트 이벤트를 게시하는 것이 일반적입니다. 이 경우 애플리케이션은 메시지 실패 또는 중복 메시지를 처리해야 합니다. 우선 순위가 다른 명령을 처리하려면 열에 대한 지침을 참조하십시오.