DDD - CQRS
- 명령 모델과 조회 모델
- CQRS 의 장단점
매핑되는 테이블은 DDD - ERD 을 참고하세요.
목차
개발 환경
- 언어: java
- Spring Boot ver: 3.2.5
- Spring ver: 6.1.6
- IDE: intelliJ
- SDK: JDK 17
- 의존성 관리툴: Maven
1. 단일 모델의 단점
단일 비즈니스 도메인 모델로 모든 요구사항을 해결하기 어려울 때가 있다.
여러 모델로 작업하는 또 다른 이유는 다양한 언어를 사용하는 영속성 개념과 관련이 있다.
완벽한 DB 는 없기 때문에 그에 대한 대안으로 폴리글랏 영속성 모델을 사용한다.
폴리글랏 영속성 모델은 관련 요구사항을 구현하기 위해 여러 DB 를 사용하는 것이다.
예) 실시간 데이터 처리는 docDB 를 사용, 견고한 검색 기능을 위해 검색 엔진 사용
CQRS 는 이벤트 소싱과 밀접하게 관련이 있다.
원래 CQRS 는 이벤트 소싱 모델의 질의 한계를 극복하기 위해 정의되었다. 즉, 이벤트 소싱 모델은 한 번에 하나의 애그리거트 인스턴스에 대한 이벤트를 질의할 수 있다.
CQRS 패턴은 프로젝션된 모델을 물리적 DB 에 머터리얼라이즈(Materialized) 해서 유연한 질의에 사용할 수 있게 해준다. (= 빈번한 질의의 결과를 물리 테이블에 저장하여 성능을 높이는 메커니즘)
CQRS 와 이벤트 소싱의 추가 설명
이벤트 소싱 모델에서는 애그리거트 상태를 이벤트 로그를 기반으로 재구성해야 하기 때문에 특정 시점의 상태를 빠르게 조회하는 것이 어려움
즉, 이벤트 로그를 하나씩 재생하면서 상태를 만들어야 하므로 실시간으로 복잡한 질의를 수행하는 것은 비효율적임CQRS 패턴을 적용하면 명령(Command) 과 질의(Query) 작업을 분리하여 각각 최적화된 모델을 사용할 수 있음
이를 통해 프로젝션된 데이터 모델을 별도의 DB 나 캐시 시스템에 저장하여 질의를 더욱 유연하고 효율적으로 수행할 수 있음
즉, CQRS 에서는 이벤트를 기반으로 Materialized View(사전 계산된 질의 결과) 를 생성하고 유지하여 질의 성능을 개선함
CQRS 와 이벤트 소싱을 함께 사용하여 활용하는 예시는 아래와 같다.
- Command 모델
- 애그리거트의 상태를 변경하는 이벤트 저장(Event Store 활용)
- 상태를 수정하는 오퍼레이션을 전담으로 수행하는 단일 모델
- 비즈니스 로직을 구현하고, 규칙을 검사하며, 불변성을 강화하는데 사용됨
- 강력한 일관성을 가진 데이터를 표현하는 유일한 모델
- Query 모델(프로젝션)
- 이벤트를 기반으로 여러 프로젝션 테이블을 생성하여 빠른 조회 제공
- 예) Elasticsearch, Redis
- 사용자에게 데이터를 보여죽나, 다른 시스템에 정보를 제공하기 위해 필요하 만큼 모델을 정의할 수 있음
- 캐시에서 언제든 다시 추출할 수 있는 프로젝션으로 DB, 파일, 인메모리 캐시에 위치할 수 있음
- 읽기 전용이므로 어떠한 오퍼레이션도 읽기 모델의 데이터를 직접 수정할 수 없음
- Event Handler(이벤트 프로세싱)
- 저장된 이벤트를 소비하여 프로젝션된 DB 를 업데이트
주문 내역 조회 기능을 구현해야 한다고 해보자.
Order 에서 주문 정보, Product 에서 상품명, Member 에서 회원 정보 등 여러 애그리거트에서 데이터를 가져와야 한다.
조회 화면 특성상 조회 속도가 빠를 수록 좋은데 여러 애그리거트의 데이터가 필요하면 구현 방법을 고민해봐야 한다.
4.2. ID 를 이용한 애그리거트의 간접 참조 에서 본 것처럼 식별자를 이용하여 애그리거트를 참조하는 방식을 사용하면 즉시 로딩 방식과 같은 JPA 의 쿼리 관련 최적화 기능을 사용할 수 없다.
이는 한 번의 SELECT 쿼리로 조회 화면에 필요한 데이터를 읽어올 수 없어 조회 성능에 문제가 생길 수 있다.
4.1. 필드를 이용한 애그리거트 참조 에서 본 것처럼 애그리거트 간 식별자가 아니라 직접 참조하는 방식으로 연결해도 조회 화면 특성에 따라 즉시 로딩이나 지연 로딩으로 처리해야 하는 문제가 생긴다.
이런 고민이 발생하는 이유는 상태를 변경할 때와 조회할 때 단일 도메인 모델을 사용하기 때문이다.
객체 지향으로 도메인 모델을 구현할 때 주로 사용하는 ORM 기법은 도메인 상태 변경 기능을 구현하는 데는 적합하지만 주문 상세 조회 화면처럼 여러 애그리거트에서 데이터를 가져오는 기능을 구현하기에는 고려할 게 많아서 구현을 복잡하게 만드는 원인이 된다.
이런 구현 복잡도를 낮춰주는 방법이 바로 상태 변경을 위한 모델과 조회를 위한 모델을 분리하는 것이다.
2. CQRS (Command Query Responsibility Segregation): 명령과 조회의 분리
복잡한 도메인에서는 상태를 변경하는 작업과 상태를 조회하는 작업의 요구사항이 완전히 다르다.
CQRS 는 이러한 책임의 불일치를 해결하기 위한 아키텍처 패턴이다.
CQRS 는 상태 변경을 위한 Command 모델과 데이터 조회를 위한 Query 모델을 명확히 분리하는 패턴이다.
- Command: 데이터 변경 (예: 생성, 수정, 삭제)
- Query: 데이터 조회 (예: 목록 조회, 상세 정보 조회)
- 사용자에게 조회용 Projection 뷰가 렌더링됨
- 사용자가 양식을 입력하고 커맨드 제출
- 커맨드 모델이 상태 변경 수행
- 변경 이벤트가 발생하고, Projection 뷰로 반영
- 최신 상태 반영된 Projection 으로 사용자에게 노출 (= 1단계로 돌아감)
위 그림은 커맨드 모델과 쿼리 모델을 위한 스토리지를 분리된 2개의 DB 로 표현했다. 이 설계는 처리량이 많은 대규모 시스템에 적합하지만 반드시 필요한 것은 아니다.
CQRS 는 반드시 대규모 시스템에만 사용하는 패턴은 아니다.
도메인의 복잡도가 증가하거나, 조회 성능과 모델 간 충돌이 발생하는 순간이 바로 CQRS 도입을 고려할 시점이다.
단일 모델은 복잡한 도메인에서 아래와 같은 문제가 발생한다.
- 명령(Command)은 대개 단일 애그리거트 단위로 수행됨
- 반면, 조회(Query)는 여러 애그리거트에 걸친 데이터 집계나 통계가 필요함
이로 인해 아래와 같은 문제가 생긴다.
- 데이터 과복잡
- 하나의 JPA 엔티티로 양쪽 책임을 동시에 지려다보니 불필요하게 복잡한 모델이 됨
- 성능 튜닝의 한계
- 조회 성능 최적화를 위해 도메인 모델을 왜곡된 형태로 구성해야 함
- 유지보수 어려움
- 양쪽 요구사항이 섞이면서 하나의 모델에서 충돌 발생
<CQRS 로 해결되는 문제>
- 조회 성능 최적화 가능
- 조회에 특화된 Projection 모델 생성 가능
- 인덱싱, 캐시 전략, 집계 뷰 등을 자유롭게 설계
- 도메인 모델 단순화
- 도메인 로직은 오직 상태 변경에 집중 → 응집도 향상
- 복잡한 Read 모델 지원
- 통계, 보고서, UI 구성 등 다양한 조회 뷰를 유연하게 지원
- 이벤트 소싱과의 궁합
- 커맨드 모델에서 발생한 이벤트를 쿼리 모델로 프로젝션하여 다양한 Read 뷰 생성 가능
- 커맨드 → 이벤트 → Read 모델 프로젝션 연결
단일 모델 사용 시
통계 쿼리를 빠르게 실행하기 위해
- JPA 에 복잡한 fetch join
@QueryProjection
DTO- 인덱스 최적화 등 과도한 설정
CQRS 적용 시
- 커맨드 모델
- InsuranceApplication, RiskEvaluation, QuoteCommandHandler
- 쿼리 모델
- InsuranceQuoteView, QuoteStatsProjection, Top10RiskAreaQueryHandler
2.1. 명령 모델과 조회 모델에 다른 구현 기술 적용
CQRS 를 사용하면 각 모델에 맞는 구현 기술을 선택할 수 있다.
명령 모델은 객체 지향에 기반하여 도메인 모델을 구현하기에 적당한 JPA 를 사용하여 구현하고, 조회 모델은 DB 테이블에서 SQL 로 데이터를 조회할 때 좋은 마이바이트를 사용해서 구현할 수 있다.
위 그림을 보면 조회 모델에는 응용 서비스가 없다.
단순히 데이터를 조회하는 기능은 응용 로직이 복잡하지 않기 때문에 컨트롤러에서 바로 DAO 를 실행해도 무방하다.
2.2. 명령 모델과 조회 모델에 같은 구현 기술 적용
명령 모델과 조회 모델이 같은 구현 기술을 사용할 수도 있다.
JPQL 을 이용한 동적 인스턴스 생성과 하이버네이트의 @Subselect
를 사용할 때 동적 인스턴스로 사용할 클래스와 @Subselect
를 적용한 클래스가 조회 모델에 해당한다.
2.3. 명령 모델과 조회 모델 설계
아래는 명령 모델과 조회 모델의 설계 예시이다.
상태 변경을 위한 명령 모델을 객체를 기반으로 한 도메인 모델을 이용하여 구현한다.
반면 조회 모델은 주문 요약 목록을 제공할 때 필요한 정보를 담고 있는 데이터 타입을 이용한다.
두 모델 모두 주문과 관련되어 있지만 명령 모델은 상태를 변경하는 도메인 로직을 수행하는데 초점을 맞춰 설계했고, 조회 모델은 화면에 노출할 데이터를 조회하는데 초점을 맞춰 설계한다.
2.4. 명령 모델과 조회 모델에 다른 데이터 저장소 사용
명령 모델과 조회 모델이 서로 다른 데이터 저장소를 사용할 수도 있다.
명령 모델은 트랜잭션을 지원하는 RDBMS 를 사용하고, 조회 모델은 조회 성능이 좋은 메모리 기반 NoSQL 을 사용할 수 있다.
두 데이터 저장소 간의 데이터 동기화는 이벤트를 활용하여 처리 가능하다.
명령 모델에서 상태를 변경하면 이에 해당하는 이벤트가 발생하고, 그 이벤트를 조회 모델에 전달해서 변경 내역을 반영하면 된다.
명령 모델과 조회 모델 데이터 동기화 시점에 따라 구현 방식이 달라질 수 있다.
- 명령 모델에서 데이터가 변경되자마자 조회 모델에 동기화해야 하는 경우
- 동기 이벤트와 글로벌 트랜잭션을 사용하여 실시간으로 동기화
- 하지만 동기 이벤트와 글로벌 트랜잭션 사용 시 전반적으로 성능이 떨어지는 단점이 있음
- 명령 모델에서 데이터가 변경된 후 특정 시간 안에만 동기화하면 되는 경우
- 비동기로 데이터 전송함으로써 데이터 동기화로 인해 명령 모델의 성능이 나빠지지 않도록 할 수 있음
이벤트에 대한 상세한 내용은
DDD - 이벤트(1): 이벤트, 핸들러, 디스패처,
DDD - 이벤트(2): 비동기 이벤트 처리
를 참고하세요.
2.5. 웹과 CQRS
일반적으로 웹 서비스는 상태 변경 요청보다 상태를 조회하는 경우가 훨씬 많다.
예를 들면 주문 요청보다 상품을 조회 요청이 훨씬 많고, 게시글도 한번 등록한 글을 여러 명이 여러 번 조회한다.
조회 성능을 높이기 위해 아래와 같은 처리를 할 수 있다.
- 기본적으로 쿼리를 최적화하여 쿼리 실행 속도 자체를 높임
- 메모리에 조회 데이터를 캐싱하여 응답 속도를 높임
- 조회 전용 저장소를 따로 사용
이런 기법들은 결과적으로 CQRS 를 적용하는 것과 같은 효과를 만든다.
- 조회 전용 모델을 캐싱
- 메모리에 캐싱하는 데이터는 DB 에 보관된 데이터를 그대로 저장하기 보다 화면에 필요한 데이터를 변환한 데이터를 캐싱할 때 성능에 더 유리
- 조회 속도를 높이기 위해 쿼리를 최적화한다는 것은 조회 화면에 필요한 데이터를 빠르게 읽어올 수 있도록 쿼리를 작성한다는 의미
대규모 트래픽이 발생하는 웹 서비스는 알게 모르게 CQRS 를 적용하고 있지만 명시적으로 명령 모델과 조회 모델을 구분하지 않을 뿐이다.
조회 속도를 높이기 위해 별도 처리를 하고 있다면 명령 모델과 조회 모델을 구분하자.
이를 통해 조회 기능 때문에 명령 모델이 복잡해지는 것을 막을 수 있고, 명령 모델과 관계없이 조회 기능에 특화된 구현 기법으로 보다 쉽게 적용할 수 있다.
2.6. Query 모델의 프로젝션
Query 모델이 동작하려면 Command 모델에서 변경을 모든 Query 모델로 프로젝션해야 한다.
Query 모델의 프로젝션을 DB 의 Materialized View 의 개념과 유사하다.
프로젝션을 생성하는 방식은 2가지가 있다.
- 동기식 프로젝션
- 비동기식 프로젝션
2.6.1. 동기식 프로젝션: OLTP
동기식 프로젝션은 OLTP(Online Transaction Processing) 데이터의 변경 사항을 트랜잭션 내에서 즉시 반영한다.
즉, 변경이 발생하면 즉시 프로젝션을 업데이트한다. (OLTP 트랜잭션 내에서 동기적으로 처리)
OLTP(Online Transaction Processing) vs OLAP(Online Analytical Processing)
OLTP 데이터는 온라인 트랜잭션 처리 시스템에서 생성되고 관리되는 데이터를 의미하며, OLTP 시스템은 빠른 트랜잭션 처리와 실시간 데이터 업데이트를 목적으로 설계된 DB 임
OLTP 는 은행 계좌 이체, 온라인 쇼핑 결제, 항공권 예약 등 다수의 짧고 빈번한 트랜잭션을 처리하는데 최적화되어 있음
OLTP | OLAP | |
---|---|---|
목적 | 빠른 트랜잭션 처리 | 데이터 분석 및 리포팅 |
데이터 구조 | row 기반 저장(RDBMS) | column 기반 저장(Data Warehouse) |
트랜잭션 유형 | CURD 연산 중심 | 대량의 데이터 분석, 집계 |
성능 최적화 | 빠른 읽기/쓰기 성능 | 복잡한 분석 질의 최적화 |
예시 | 은행 시스템, 전자상거래, 예약 시스템 | 비즈니스 인텔리전스(BI), 데이터 마이닝 |
동기식 프로젝션은 이벤트가 발생할 때 즉시 프로젝션을 업데이트하는 방식이다.
예) 애그리거트 상태가 변경되면 변경 사항을 즉시 Query 모델(프로젝션)에 반영
주로 트랜잭션 내에서 프로젝션을 즉시 업데이트하는 경우가 많으며, 이는 일관성 유지에는 유리하지만 성능에 영향을 줄 수 있다.
2.6.2. 비동기식 프로젝션: 격차 해소 구독 모델
비동기식 프로젝션은 격차 해소 구독 모델(catch-up subscription model) 을 통해 이벤트 로그에서 데이터를 읽어와 변경 사항을 반영한다.
즉, Command 실행 모델이 모든 커밋된 변경 사항을 메시지 버스에 발행하고, 프로젝션 엔진은 발행된 메시지를 구독하여 이벤트 로그에서 변경사항을 가져와 업데이트하며 이 때 격차 해소 구독 모델이 가능하다.
격차 해소 구독 모델은 이벤트 소싱에서 사용되는 비동기적 이벤트 처리 방식 중 하나로, 이벤트 로그에서 이벤트를 읽어와 프로젝션을 업데이트한다.
이벤트가 순차적으로 쌓이기 때문에 구독자가 처음부터 혹은 특정 시점부터 이전 이벤트들을 처리하면서 점진적으로 최신 상태를 따라가는 방식이다.
즉, OLTP 데이터베이스에서 직접 변경 사항을 가져오는 것이 아니라 이벤트 로그를 통해 데이터를 동기화하는 것이다.(= 이벤트 로그 또는 체크포인트 기반으로 변경 사항을 비동기로 읽어와 Query 모델을 갱신하는 방식)
2.7. CQRS 장단점
<CQRS 도입 시 장점>
- 명령 모델 구현 시 도메인 자체에 집중 가능
- 복잡한 도메인은 주로 상태 변경 로직이 복잡한데 명령 모델과 조회 모델을 구분함으로써 조회 성능을 위한 코드가 명령 모델에 없으므로 도메인 로직을 구현하는데 집중 가능
- 명령 모델에 조회 관련 로직이 없으므로 복잡도가 낮아짐
- 조회 성능을 향상시키는데 유리
- 조회 단위로 캐시 기술 적용 가능
- 조회에 특화된 쿼리 사용 가능
- 조회 전용 저장소를 사용하여 조회 처리량 향상 가능
<CQRS 도입 시 단점>
- 구현해야 할 코드가 많아짐
- ‘단일 모델을 사용할 때 발생하는 복잡함 때문에 발생하는 구현 비용’ 과 ‘조회 전용 모델을 만들 때 발생하는 구현 비용’ 을 따져봐야 함
- 도메인이 복잡하거나 대규모 트래픽이 발생하는 서비스라면 조회 전용 모델을 만드는 것이 향후 유지 보수에 유리함
- 반면, 도메인이 단순하거나 트래픽이 많지 않은 서비스라면 조회 전용 모델을 따로 만들 때 얻을 이점이 있는지 따져봐야 함
- 더 많은 구현 기술이 필요함
- 명령 모델과 조회 모델을 다른 구현 기술을 사용하여 구현하기도 하고, 경우에 따라 다른 저장소를 사용하기도 함
- 데이터 동기화를 위해 메시징 시스템을 도입해야 할 수도 있음
이런 장단점을 고려하여 CQRS 패턴을 도입할 지 여부를 결정해야 한다.
도메인이 복잡하지 않은데 CQRS 를 도입하면 두 모델을 유지하는 비용만 높아지고 이점은 없다.
반면, 트래픽이 높은 서비스인데 단일 모델로 구현하면 유지 보수 비용이 높아질 수 있으므로 CQRS 도입을 고려하는 것이 좋다.
비동기식 프로젝션 방식의 확장성과 성능의 장점에도 불구하고 분산 컴퓨팅 구조에서는 메시지의 순서가 잘못되거나 중복 처리되여 Query 모델에 일관성 없는 데이터가 프로젝션 될 수 있다.
따라서 가능하면 동기식 프로젝션 방식으로 구현하고, 그 위에 선택적으로 비동기식 프로젝션 방식을 추가하는 것을 권장한다.
2.8. CQRS 를 사용해야 하는 경우
CQRS 패턴은 여러 모델, 궁극적으로는 다양한 종류의 DB 에 저장된 동일한 데이터와 작동할 필요가 있는 애플리케이션에 유용하다.
또한 이벤트 소싱 도메인 모델에도 적합하다.
이벤트 소싱 모델에서는 애그리거트의 상태에 기반한 레코드 조회가 불가능하지만, CQRS 는 상태를 질의할 수 있는 DB 에 상태를 프로젝젼하므로 이것이 가능하다.
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 최범균 저자의 도메인 주도 개발 시작하기을 기반으로 스터디하며 정리한 내용들입니다.