CQRS
1. CQRS 패턴
CQRS 패턴이란 Command and Query Responsibility Segregation의 약자이다. 이를 해석하면 명령과 쿼리의 역할을 구분한다는 의미이다. 즉, Command (Create, Insert, Update, Delete)와 쿼리(Select - Read)의 책임을 분리하는 의미이다.
CQRS는 데이터 저장소에 대한 읽기 및 업데이트 작업을 구분하는 패턴인 명령 및 쿼리 책임 분리를 의미한다. 애플리케이션에서 CQRS를 구현하면 성능, 확장성 및 보안을 최대화할 수 있다.
CQRS로 마이그레이션하면 유연성이 생기므로 시스템이 점점 진화하고 업데이트 명령이 도메인 수준에서 병합 충돌을 일으키지 않도록 할 수 있다.
2. CQRS가 필요한 이유
1) 컨텍스트 및 문제점
기존 아키텍처에서 데이터베이스를 쿼리하고 업데이트하는데 동일한 데이터 모델을 사용한다. 그러면 간단하고 기본적인 CRUD 작업에 적합하다. 그러나 더 복잡한 애플리케이션에서는 이 방법을 사용하기 어려울 수 있다.
예를 들어 애플리케이션은 읽기 쪽에서 다른 쿼리를 수행할 수 있다. 그러면 모양이 다른 DTO(데이터 전송 개체)를 반환한다. 개체 매핑이 복잡해 질 수 있다. 모델은 쓰기 쪽에서 복잡한 유효성 검사 및 비즈니스 논리를 구현할 수 있다. 따라서 너무 많은 작업을 수행하는 과도하게 복잡한 모델이 될 수 있다.
읽기 및 쓰기 워크로드는 성능 및 규모 요구 사항이 매우 다른 비대칭적인 경우가 많다.
작업의 일부로 필요하지 않더라도 올바르게 업데이트해야 하는 추가 열 또는 속성과 같이 데이터의 읽기 및 쓰기 표현이 일치하지 않는 경우가 많다.
동일한 데이터 집합에서 작업을 병렬로 수행할 때 데이터 경합이 발생할 수 있다.
기존의 접근 방식은 데이터 저장소 및 데이터 액세스 계층의 로드와 정보를 검색하는데 필요한 쿼리의 복잡성으로 인해 성능에 부정적인 영향을 미칠 수 있다.
보안 및 사용 권한 관리는 복잡해질 수 있다. 각 엔터티는 읽기 및 쓰기 작업이 모두 적용되므로 잘못된 컨텍스트에서 데이터를 노출할 수 있다.
2) 해결 방법
CQRS는 데이터를 업데이트하는 명령과 데이터를 읽는 쿼리 를 사용하여 읽기 및 쓰기를 다른 모델로 구분 한다.
명령은 데이터 중심이 아닌 작업 기반이어야 한다. (“ReservationStatus를 예약됨으로 설정”이 아닌 “호텔 객실 예약”). 명령은 동기적으로 처리되지 않고 비동기 처리를 위해 큐에 배치될 수 있습니다. 쿼리는 데이터베이스를 수정하지 않습니다. 쿼리는 도메인 정보를 캡슐화하지 않는 DTO를 반환합니다. 그런 다음, 절대 요구 사항은 아니지만 다음 다이어그램과 같이 모델을 격리할 수 있습니다.
별도의 쿼리 및 업데이트 모델을 사용하면 디자인과 구현이 간소화된다. 그러나 한 가지 단점은 O/RM 도구와 같은 스캐폴딩 메커니즘을 사용하여 데이터베이스 스키마에서 CQRS 코드를 자동으로 생성할 수 없다는 것이다.
더 높은 격리 수준을 위해 쓰기 데이터에서 읽기 데이터를 물리적으로 구분할 수 있다. 이 경우에 읽기 데이터베이스는 쿼리에 대해 최적화된 고유한 데이터 스키마를 사용할 수 있다.
예를 들어 복잡한 조인이나 복잡한 O/RM 매핑을 방지하기 위해 데이터의 구체화된 뷰를 저장할 수 있습니다. 다른 유형의 데이터 저장소도 사용할 수 있다. 예를 들어 쓰기 데이터베이스가 관계형일 수 있는 반면 읽기 데이터베이스는 문서 데이터베이스이다.
별도의 읽기 및 쓰기 데이터베이스를 사용하는 경우 동기화된 상태로 유지되어야 한다. 일반적으로 이 작업은 쓰기 모델이 데이터베이스를 업데이트할 때마다 이벤트를 게시하게 하여 수행된다.
이벤트 사용에 대한 자세한 내용은 이벤트 기반 아키텍처 스타일을 참조한다. 데이터베이스를 업데이트하고 이벤트를 게시하는 작업은 단일 트랜잭션에서 이루어져야 한다.
읽기 저장소는 쓰기 저장소의 읽기 전용 복제본이거나 읽기 및 쓰기 저장소가 전혀 다른 구조일 수 있다. 여러 읽기 전용 복제본을 사용하면 특히 읽기 전용 복제본이 애플리케이션 인스턴스 가까이에 있는 분산 시나리오에서 쿼리 성능을 높일 수 있다.
읽기 및 쓰기 저장소를 분리하면 부하를 감안해 각 저장소를 적절하게 확장할 수도 있다. 예를 들어 보통 읽기 저장소는 쓰기 저장소보다 부하가 훨씬 더 높다.
CQRS의 일부 구현에서는 이벤트 소싱 패턴을 사용한다. 이러한 패턴에서 애플리케이션 상태는 이벤트의 시퀀스로 저장된다. 각 이벤트는 데이터에 대한 변경 집합을 나타낸다. 현재 상태는 이벤트를 재생함으로써 구축된다. CQRS 컨텍스트에서 이벤트 소싱의 한 가지 이점은 동일한 이벤트를 사용하여 다른 구성 요소, 특히 읽기 모델에 알릴 수 있다는 것이다. 읽기 모델은 현재 상태의 스냅샷을 만드는데 이벤트를 사용한다. 이것이 쿼리에 보다 효과적이다. 그러나 이벤트 소싱은 디자인에 복잡성을 추가한다.
CQRS의 이점은 다음과 같다.
독립적인 크기 조정: CQRS를 통해 읽기 및 쓰기 워크로드를 독립적으로 확장하고 더 적은 수의 잠금 경합이 발생할 수 있다.
최적화된 데이터 스키마: 읽기 쪽에서는 쿼리에 최적화된 스키마를 사용하는 반면 쓰기 쪽에서는 업데이트에 최적화된 스키마를 사용할 수 있다.
보안: 올바른 도메인 엔터티만 데이터에서 쓰기를 수행할 수 있는지 쉽게 확인할 수 있다.
관심사의 분리: 읽기 및 쓰기 쪽을 구분하면 유지 가능하고 유연한 모델을 생성할 수 있다. 대부분의 복잡한 비즈니스 논리는 쓰기 모델로 이동한다. 읽기 모델은 상대적으로 간단할 수 있다.
단순한 쿼리 읽기 데이터베이스에서 구체화된 뷰를 저장하여 쿼리할 때 애플리케이션은 복잡한 조인을 방지할 수 있다.
3. 구현 문제 및 고려 사항
이 패턴을 구현하는 몇 가지 과제는 다음과 같다.
복잡성: CQRS의 기본 개념은 간단하다. 하지만 이벤트 소싱 패턴을 포함하는 경우에 특히 더 복잡한 애플리케이션 디자인을 만들 수 있다.
메시징 CQRS에 메시징이 필요하지 않지만 명령을 처리하고 업데이트 이벤트를 게시하는데 공통적으로 메시징을 사용한다. 이 경우에 애플리케이션은 메시지 오류 또는 중복 메시지를 처리해야 한다. 우선 순위가 다른 명령을 처리하기 위한 우선 순위 큐 에 대한 지침을 참조한다.
최종 일관성: 읽기 및 쓰기 데이터베이스를 구분하는 경우 읽기 데이터는 기한이 경과되었을 수 있다. 쓰기 모델 저장소에 대한 변경 내용을 반영하도록 읽기 모델 저장소를 업데이트해야 하며, 사용자가 부실 읽기 데이터를 기반으로 요청을 실행한 경우를 감지하기 어려울 수 있다.
4. CQRS 패턴을 사용하는 경우
다음 시나리오에 대해 CQRS를 고려한다.
많은 사용자가 동일한 데이터에 병렬로 액세스하는 공동 작업 도메인이다. CQRS를 사용하면 도메인 수준에서 병합 충돌을 최소화하기에 충분한 세분성으로 명령을 정의할 수 있으며, 발생하는 충돌은 명령에 의해 병합될 수 있다.
여러 단계를 거치거나 복잡한 도메인 모델을 사용하는 복잡한 프로세스를 통해 사용자를 안내하는 작업 기반 사용자 인터페이스: 쓰기 모델에는 비즈니스 논리, 입력 유효성 검사 및 비즈니스 유효성 검사가 포함된 전체 명령 처리 스택이 있다. 쓰기 모델은 데이터 변경(DDD 용어의 집계)에 대한 단일 단위로 연결된 개체 집합을 처리하고 이러한 개체가 항상 일관된 상태인지 확인할 수 있다. 읽기 모델에는 비즈니스 논리 또는 유효성 검사 스택이 없으며 보기 모델에서 사용할 DTO만 반환한다. 결과적으로 읽기 모델과 쓰기 모델의 일관성이 유지된다.
특히 읽기 수가 쓰기 수보다 훨씬 큰 경우 데이터 읽기의 성능을 데이터 쓰기 성능과 별도로 미세 조정해야 하는 시나리오이다. 이 시나리오에서는 읽기 모델을 스케일 아웃할 수 있지만 몇 개의 인스턴스에서만 쓰기 모델을 실행할 수 있다. 소수의 쓰기 모델 인스턴스는 병합 충돌 발생을 최소화하는 데도 기여한다.
개발자 중 한 팀은 쓰기 모델에 포함되는 복잡한 도메인 모델에 집중하고 또 한 팀은 읽기 모델과 사용자 인터페이스에 집중할 수 있는 시나리오
시스템이 시간이 지나면서 진화할 것으로 예상되어 여러 버전의 모델을 포함할 수 있거나 비즈니스 규칙이 정기적으로 변하는 시나리오
특히 이벤트 소싱과 조합해 다른 시스템과 통합하는 경우: 이때 하위 시스템 하나의 일시적인 장애가 다른 시스템의 가용성에 영향을 주지 않아야 한다.
다음과 같은 경우 이 패턴이 권장되지 않는다.
도메인 또는 비즈니스 규칙은 간단하다.
간단한 CRUD 스타일 사용자 인터페이스 및 데이터 액세스 작업으로 충분하다.
가장 가치 있는 시스템의 제한된 구역에 CQRS 적용을 고려해야 한다.
5. 이벤트 소싱 및 CQRS 패턴
CQRS 패턴은 이벤트 소싱 패턴과 함께 사용되는 경우가 많다. CQRS 기반 시스템은 별도의 읽기 및 쓰기 데이터 모델을 사용하며 각각 관련 작업에 맞춤화되고 종종 물리적으로 분리된 저장소에 배치된다. 이벤트 소싱 패턴과 함께 사용할 경우 이벤트 저장소는 쓰기 모델이며 공식 정보 원본이다. 보통 CQRS 기반 시스템의 읽기 모델은 고도로 비정규화된 뷰의 형태로 데이터의 구체화된 뷰를 제공한다. 이러한 뷰는 애플리케이션의 인터페이스 및 디스플레이 요구 사항에 맞춤화되어 디스플레이 및 쿼리 성능을 모두 최대화하는데 기여한다.
특정 시점의 실제 데이터 대신 이벤트의 스트림을 쓰기 저장소로 사용하면 단일 집계에서 업데이트 충돌을 방지하고 성능과 확장성을 최대화할 수 있다. 읽기 저장소를 채우는데 사용하는 데이터의 구체화된 뷰를 비동기적으로 생성하는데 이벤트를 사용할 수 있다.
이벤트 저장소는 정보의 공식적인 출처이기 때문에, 시스템이 진화하거나 읽기 모델을 변경해야 할 때 구체화된 뷰를 삭제하고 모든 지난 이벤트를 재생해 현재 상태의 새로운 표현을 생성할 수 있다. 사실상 구체화된 뷰는 데이터의 지속형 읽기 전용 캐시이다.
CQRS를 이벤트 소싱 패턴과 함께 사용할 때는 다음을 고려해야 한다.
쓰기 및 읽기 저장소가 분리되는 시스템처럼 이 패턴을 기반으로 하는 시스템만이 결과적으로 일관성을 유지할 수 있다. 생성되는 이벤트와 업데이트되는 데이터 저장소 사이에는 약간의 지연이 나타나게 된다.
이벤트를 시작하고 처리하며 쿼리나 읽기 모델에 필요한 적절한 뷰 또는 개체를 어셈블하거나 업데이트하는 코드를 생성해야 하기 때문에, 이벤트 소싱 패턴은 더 복잡하다. CQRS 패턴을 이벤트 소싱 패턴과 함께 사용하는 경우 패턴이 복잡하기 때문에 성공적으로 구현하는 것이 더 어려워질 수 있으므로 시스템 디자인에 대한 다른 접근 방식이 필요하다. 그러나 이벤트 소싱은 도메인을 더 쉽게 모델링할 수 있고 데이터를 변경한 의도가 보존되기 때문에 뷰를 다시 작성하거나 새로 만들기가 더 쉽다.
특정 엔터티 또는 엔터티 모음을 위한 이벤트를 재생하고 처리하여 데이터의 읽기 모델 또는 프로젝션에 사용할 구체화된 뷰를 생성하려면 상당한 처리 시간과 리소스 사용이 필요할 수 있다. 장기간 값의 합계 또는 분석이 필요한 경우에 특히 그런데 그 이유는 관련된 모든 이벤트를 검사해야 하기 때문이다. 발생한 특정 작업 수의 총 개수 또는 엔터티의 현재 상태와 같이 예약된 간격으로 데이터의 스냅샷을 구현하여 이 문제를 해결한다.