마이크로서비스의 핵심 설계 철학, CQRS(명령 조회 책임 분리 패턴) - 데이터 저장소에 대한 Read와 Update 작업을 분리하는 설계 패턴 - 구현 시 성능, 확장성, 보안을 최대화 가능 - Update 명령으로 인해 도메인 수준에서 병합 충돌 발생 방지 가능
배경 - 일반적 DB 트랜잭션은 DB로 부터 데이터를 읽어 화면에 출력 (읽는 시점과 출력 시점이 상이함) - 따라서, CRUD중 R과 CUD의 공존은 무의미하며, R의 출력물은 정도의 차이가 있을뿐 실제 데이터와 상이하니, 캐쉬를 사용하여 더욱 빠르게 EndUser가 R할 수 있도록 구현하고, CUD는 메시지 큐를 통해 실제 데이터를 변경시키며, 그 변경이 일어나는 시점에 이벤트를 발생시켜서 캐쉬를 업데이트하는 방식으로 진행
데이터의 R과 CUD가 일치하지 않을 수 있는 기존 구조
기존 DB트랜잭션의 문제 - 데이터의 R과 CUD 표현이 불일치 - 데이터 경합 현상 발생 (데이터 집합에서 작업을 병렬로 수행 하는 경우 빈번) - 데이터 저장소 및 데이터 액세스 계층에 대한 로드로 인해 성능에 부정적 영향 - 정보를 검색하는 필요한 쿼리의 복잡성 증가 - 각 엔티티는 R과 CUD 작업을 수행하므로 잘못된 컨텍스트에서 데이터를 노출할 수 있어 보안 및 사용권한 관리가 복잡 가능
동작원리 - 명령(Command)의 책임을 CUD에, 쿼리(Query)의 책임을 Read에 부여 - 명령은 데이터 중심이 아닌 작업을 기반으로 수행 - 명령은 동기적으로 처리하지 않고 비동기 처리를 위해 큐에 배치 - 쿼리는 DB 수정하지 않고, 도메인 정보를 캡슐화 하지 않는 DTO를 반환
모델의 격리 가능한 예시
구현 해법 - O/RM 툴과 같은 스캐폴딩 매커니즘을 사용하여 DB스키마에서 CQRS 코드를 자동으로 생성 불가능 - 높은 격리수준을 위해 쓰기와 읽기 데이터를 물리적으로 분리 구분 (데이터 최적화된 스키마 구성 가능) - 데이터의 물리적 분리 구분은 복잡한 조인이나 O/RM 매핑을 방지하기 위해 데이터의 구체화된 뷰를 저장 가능 - 다른 유형의 데이터 저장소 사용도 가능 (CUD는 RDBMS, R은 NoSQL) - 구분된 R, CUD DB 구현 시 데이터 정합성 유지 필요 (일반적으로 CUD 모델에서 DB Update시 이벤트를 게시 하도록 수행 필요)
CUD모델과 R모델의 격리 후 이벤트 소싱 통한 정합성 유지 예시
성능 극대화 - R저장소의 복제본을 복수개로 응용 애플리케이션 인스턴스에 가까운 위치에 분산 시나리오 구성시 쿼리 성능 극대화 가능 - CUD보다 R저장소의 부하가 높은것이 일반적이므로, 적절한 확장정책 사용시 높은 효과 가능
장점 - 독립적인 크기 조정 가능 : R과 CUD의 워크로드를 독립적으로 확장하고, 락킹현상 최소화 가능 - 최적화된 데이터 스키마 구성 가능 : R과 CUD의 특성에 맞는 스키마 구성 가능 - 보안성 강화 : 올바른 도메인 인터티만 데이터에서 CUD 수행할 수 있는지 손쉽게 확인 가능) - 근본적 문제의 분리 가능 : R은 단순화하여 유연성 확보하고, CUD는 복잡한 비즈니스 로직 구현시 가능) - 단순 쿼리 가능 : 복잡한 조인 방지 가능
구현시 문제 - 복잡성 : 기본개념은 간단하나, 이벤트 소싱 패턴을 포함할 경우 복잡성 증가 가능 - 메시징 : 메시지 오류, 중복 처리, 우선순위 등 처리 필요 - 최종 일관성 : 타임아웃에 따라 R저장소의 데이터 일관성 보장이 완전하지 않을 수 있음
권장 예시 - 많은 사용자가 동일한 데이터에 동시에 액세스 하는 공동 작업 도메인 - 여러 단계를 거치거나 복잡한 도메인 모델을 사용하는 복잡한 프로세스를 통해 사용자를 안내하는 작업 기반 사용자 인터페이스 - R모델과 CUD모델의 극명한 트랜잭션 차이가 있을 경우 - 개발시 A팀은 R모델에 집중하고, B팀은 CUD모델에 집중 가능 - 시스템의 진화가 주기적으로 발생하여 여러버전의 모델이 필요하고, 비즈니스 정책이 정기적으로 변하는 시나리오 - 이벤트 소싱과 조합해 타 시스템과 통합하는 경우 (하위 시스템의 하나의 일시적 장애가 타 시스템의 가용성에 무영향)
비권장 예시 - 간단한 비즈니스 시나리오 - 간단한 CRUD 스타일의 사용자 인터페이스 및 데이터 액세스 작업
CQRS + 이벤트 소싱 패턴 - 이벤트의 저장소는 CUD모델이며 데이터의 원본 - 보통 CQRS기반 시스템의 R저장소는 비정규화된 뷰의 형태로 데이터의 구체화된 뷰를 제공 - 특정 시점의 실제 데이터 대신 이벤트의 스트림을 CUD저장소로 사용하면 단일 집계에서 업데이트 충돌을 방지하고, 성능과 확장성을 최대화 가능 - 즉, R저장소를 채우는 데 사용하는 데이터의 구체화된 뷰를 비동기적으로 생성하는 데 이벤트를 사용 가능 - 이벤트 저장소는 데이터의 원본이므로, 시스템이 진화하거나 R모델을 변경해야 할 때 구체화된 뷰를 삭제하고 모든 지난 이벤트를 재생해 현재 상태의 새로운 표현을 생성 가능 - 사실상 구체화된 뷰는 데이터의 지속형 읽기 전용 캐시 가능
CQRS + 이벤트 소싱 패턴 사용 시 고려사항 - CUD와 R 저장소가 분리되는 시스템의 경우 이벤트 소싱 패턴만이 일관성 확보 가능 (생성되는 이벤트와 업데이트 되는 데이터 저장소 사이에는 약간의 지연 발생) - 패턴의 복잡성이 높아 구현의 난이도가 높으나, 뷰를 다시 작성하거나 신규생성은 수월 - 특정 엔터티 또는 엔터티 모음을 위한 이벤트를 재생하고 처리하여 데이터의 R모델 또는 구체화된 뷰를 생성하려면 상당한 처리 시간과 리소스 필요 (이벤트의 전수검사 필요하나 예약된 간격으로 스냅샷을 구현해 해결 가능)
관련 패턴 - Data Consistency Primer(데이터 일관성 입문) - Data Segregation Pattern (수평, 수직 및 기능별 데이터 분할) - Event Sourcing Pattern (이벤트 소싱) - Materialized View Pattern (구체화된 뷰 패턴)
의견 - 클라우드의 장점인 Scalability가 핵심요건일 경우에 반드시 필요할지도. - 엄격한 데이터정합성이 필요한 정산, 매출 등의 비즈니스 로직 보다는 조회성 대시보드 구현시 높은 효과 - 복잡하더라도 이벤트 소싱 패턴과 함께 사용 - 유지보수를 위한 개발자 능력이 다소 필요하지만, 패턴 구현 후 조회를 위한 뷰 구현 시 탁월