CQRS 뷰 설계

임태환·2025년 2월 22일

study

목록 보기
10/10

CQRS 뷰 모듈에는 하나 이상의 쿼리 작업으로 구성된 API가 존재
하나 이상의 서비스가 발행한 이벤트를 구독해서 최신 상태로 유지된 DB를 조회하는 쿼리 API

뷰 모듈은 뷰 DB와 세 하위 모듈로 구성된다.

세 하위 모듈 종류 및 역할

이벤트 핸들러

  • 각 서비스에서 발행되는 도메인 이벤트를 구독하여 뷰 DB에 반영
  • 도메인 이벤트가 발생할 때마다 그 이벤트의 정보를 기반으로 읽기 모델(뷰)을
    최신 상태로 업데이트하거나 보정

쿼리 API 모듈

  • 외부 클라이언트나 다른 시스템의 조회 요청을 받아 뷰 DB에서 데이터를 조회하고 응답
  • 조회 성능에 최적화, 복잡한 조인이나 집계 없이 빠른 응답을 제공하도록 설계

뷰 재구축 모듈

  • 뷰 DB에 문제가 발생하거나 초기 상태로 재구축이 필요할 때, 이벤트 로그를 다시 읽어 전체 뷰를 재생성
  • 초기 로딩, 장애 복구, 혹은 데이터 동기화 작업을 수행하여 뷰 DB의 일관성을 보장

뷰 모듈 개발 시 중요한 설계 결정

  • DB를 선정하고 스키마를 설계해야 한다.
    • 뷰 모듈의 쿼리 작업을 효율적으로 구현하기 위함
    • DB역시 이벤트 핸들러가 수행하는 업데이트 작업을 효율적으로 지원 가능해야함

  • 데이터 접근 모듈을 설계할 때 멱등한/동시 업데이트 등 다양한 문제 고려
  • 기존 애플리케이션에서 새 뷰를 구현하거나 기존 스키마를 바꿀 경우 뷰를 효율적으로
    (재)빌드할 수 있는 수단을 강구
  • 뷰 클라이언트에서 복제 시차를 어떻게 처리할지 결정

업데이트 작업지원

뷰 데이터 모델에서는 쿼리뿐만 아니라 이벤트 핸들러가 실행할 업데이트 작업 역시 효율적으로 구현되야 함

이벤트 핸들러는 대개 뷰 DB에 있는 레코드를 기본키로 찾아 수정/삭제할 것이다
만약 외래키를 이용하여 레코드를 수정/삭제해야하는 경우

1대1 관계라면 이벤트 핸들러는 DB 레코드를 쉽게 업데이트할 수 있다.
반면 1대다 관계라면 일괄 업데이트/삭제 (벌크 연산) 또는 개별 처리를 해줘야한다.

일부 DB 자료형은 외래키 기반의 업데이트 작업을 효율적으로 지원한다
하지만 NoSQL에서는 비기본키 기반으로 업데이트 하기가 쉽지 않다

애플리케이션이 업데이트할 레코드를 경정하려면 외래키에서 기본키로 매핑 가능한 데이터를
DB에 갖고 있어야한다.
기본키 수정/삭제만 지원되는 DB를 사용한다면 보조 인덱스를 쿼리해서 수정/삭제할 항목의
기본키를 결정해야한다.

데이터 접근 모듈 설계

이벤트 핸들러와 쿼리 API 모듈은 DB에 직접 접근하지 않는다
대신 DAO 및 헬퍼 클래스로 구성된 데이터 접근 모듈을 사용한다.

DAO의 역할 및 주의사항

역할

이벤트 핸들러가 호출한 업데이트 작업과 쿼리 모듈이 호출한 쿼리 작업을 수행

고수준 코드에 쓰이는 자료형, DB API간 매핑, 동시 업데이트 처리 및 업데이트 멱등성 보장

주의사항
DAO는 동시 업데이트로 서로의 데이터를 덮어 쓰지 않도록 작성해야함
만약 레코드를 읽고 업데이트된 레코드를 쓴다면 낙관적 락이나 비관적 락 둘 중 하나를 적용

동시성 처리

동일한 DB 레코드에 대해 DAO가 여러 동시 업데이트를 처리하는 경우
뷰가 한 종류의 애그리거트가 발행한 이벤트를 구독 -> 동시성 이슈 없음

특정 애그리거트 인스턴스가 발행한 이벤트는 순차적으로 처리되어
어느 한 애그리거트 인스턴스에 해당되는 레코드가 동시에 업데이트 될 일은 없음

뷰가 여러 종류의 애그리거트에 발행한 이벤트를 구독할 경우
여러 이벤트 핸들러가 동일한 레코드에 달려들어 업데이트 -> 동시성 발생

멱등한 이벤트 핸들러

멱등한 이벤트 핸들러란 동일한 이벤트가 여러 번 처리되더라도 시스템의 상태가 처음 한 번 처리했을 때와 동일하게 유지되도록 설계된 핸들러

분산 시스템이나 이벤트 소싱 환경에서 네트워크 문제나 재처리로 동일 이벤트가 중복 수신될 가능성이 존재하여 중요

이벤트 식별자 활용
– 각 이벤트에 고유한 ID가 부여되어 있다면, 이벤트를 처리할 때 이 ID를 기준으로 이미 처리된 이벤트인지 확인할 수 있습니다.
– 이미 처리된 이벤트라면 해당 이벤트는 무시하도록 하여 중복 처리를 방지합니다.

조건부 데이터 업데이트
– 이벤트를 처리할 때, 현재 상태와 비교하여 변경이 필요한 경우에만 업데이트를 수행합니다.
– 예를 들어, 특정 값이 이미 이벤트에 의해 갱신되어 있다면 추가 업데이트를 수행하지 않는 방식입니다.

트랜잭션 관리
– 이벤트 처리와 관련된 데이터 변경 작업을 하나의 트랜잭션으로 묶어, 전체 작업의 원자성을 보장합니다.
– 트랜잭션 내에서 이벤트 처리 완료 후 처리된 이벤트 ID를 기록하면, 재처리 시점에 이를 활용할 수 있습니다.

외부 시스템 호출 고려
– 만약 이벤트 핸들러가 외부 시스템(API 호출 등)과 상호작용한다면, 동일 이벤트 재처리 시 외부 시스템에 불필요한 영향을 주지 않도록 재시도 로직이나 상태 체크를 추가합니다.

클라이언트 애플리케이션이 최종 일관된 뷰를 사용할 수 있다

CQRS를 사용하면 커맨드 처리 후 즉시 쿼리를 요청할 경우,
메시징 인프라의 지연으로 인해 클라이언트가 업데이트 내용을 바로 확인하지 못할 수 있다.
이 경우 뷰는 최종 일관성(eventual consistency)을 따른다.

이를 해결하기 위한 방법

토큰 반환:
– 커맨드 처리 후, 클라이언트에게 해당 작업으로 발행된 이벤트의 고유 ID가 포함된 토큰을 반환.

토큰 기반 검증:
– 클라이언트가 쿼리 API를 호출할 때 이 토큰을 함께 전송.
– 뷰 모듈은 토큰에 포함된 이벤트 ID를 기준으로, 해당 이벤트가 뷰에 반영되었는지 확인.

비일관성 감지:
– 만약 해당 이벤트가 아직 뷰에 적용되지 않았다면, 쿼리 API는 에러를 반환하여 클라이언트가 비일관성을 감지하도록 함.

CQRS 뷰 추가 및 업데이트

CQRS 뷰는 애플리케이션이 살아있는 동안 계속 추가/수정

새 뷰를 생성하려면 쿼리 쪽 모듈을 개발,데이터 저장소 세팅,서비스를 배포해야함
-> 쿼리 쪽 모듈의 이벤트 핸들러가 모든 이벤트를 처리하고 뷰는 언젠가 최신 상태가 됌

기존 뷰를 수정하는 작업도 이벤트 핸들러를 변경한 후 뷰를 재생성한다
->실제로 잘 통하지 않는 방법

아카이빙된 이벤트를 이용하여 CQRS 뷰 구축

메시지 브로커는 메시지를 무기한 보관할 수 없다

기존 메시지 브로커는 컨슈머가 메시지를 처리한 직후 메시지를 삭제
미리 설정된 시간 동안 메시지를 보관 가능한 최신 브로커 역시 이벤트를 영구 보관하지 않는다

메시지 브로커에서 전부 읽기만 해서는 뷰를 구축할 수 없다
-> AWS S3같은 곳에 아카이빙된 오래된 이벤트도 같이 가져와야한다.
->아파치 스파크처럼 확장 가능한 빅데이터 기술 응용

CQRS 뷰를 단계적으로 구축

전체 이벤트를 처리하는 시간/리소스가 점점 증가하는 것도 뷰 생성의 문제점
-> 결국 뷰는 언젠가 느려지고 비용도 증가

해결방안

2단계 증분 알고리즘 적용

1단계 - 주기적으로 각 애그리거트 인스턴스의 스냅샷을 이전의 스냅샷과 해당 스냅샷이 생성된 이후 즉 발생한 이벤트를 바탕으로 계산

2단계 - 계산된 스냅샷과 그 이후 발생한 이벤트를 이용하여 뷰를 생성

profile
웹 개발자

0개의 댓글