Architecture Pattern

CQRS (Command Query Responsibility Segregation) 패턴 이란 ?

Ed2024 2024. 11. 5. 12:12

CQRS 패턴은 마이크로소프트의 소프트웨어 아키텍트이자 소프트웨어 설계 전문가인 그레그 영(Greg Young)에 의해 제안되었습니다. CQRS 패턴은 2000년대 후반에, 정확히는 2009년경, 그가 DDD(Domain-Driven Design)와 관련된 커뮤니티와 컨퍼런스에서 CQRS 개념을 처음 설명하면서 널리 알려지기 시작했습니다.

 

또한, CQRS 패턴은 DDD(Domain-Driven Design) 커뮤니티에서도 널리 채택되었으며, 에릭 에반스(Eric Evans) DDD 철학과 밀접한 관련이 있습니다. 그레그 영은 DDD의 철학을 더욱 발전시켜 CQRS와 같은 패턴을 만들어냈으며, 그 이후 마이크로서비스 아키텍처와 이벤트 소싱 등에서도 자주 사용되며 많은 기업에서 이를 채택하고 있습니다.

 

오늘날에는 여러 대형 기술 회사들이 대규모 애플리케이션에 CQRS 패턴을 적용하고 있으며, 특히 데이터 일관성과 성능이 중요한 금융 서비스나 전자상거래 플랫폼, 클라우드 기반 서비스 등에서 유용하게 사용되고 있습니다.

 

CQRS (Command Query Responsibility Segregation) 패턴은 애플리케이션의 명령(상태 변경 요청)과 조회(데이터 조회 요청)를 분리하여 관리하는 설계 패턴입니다.

 

CQRS (Command Query Responsibility Segregation) 패턴에서 "명령" "조회"를 분리한다는 것은 애플리케이션이 데이터를 수정하는 작업과 조회하는 작업을 서로 독립적으로 관리한다는 의미입니다. 이를 통해 데이터 상태 변화와 데이터 읽기 로직을 별도의 모델로 구분할 수 있어, 시스템 성능 및 확장성을 극대화할 수 있습니다. 구체적으로 설명하면 다음과 같습니다:

 

1) 명령(Command, Data 추가 수정 삭제)

명령은 애플리케이션 내에서 상태를 변경하는 모든 작업을 의미합니다. 데이터베이스에 새로운 데이터를 추가하거나, 기존 데이터를 수정하거나, 삭제하는 등의 작업이 이에 속합니다.

 

 예시:

- 쇼핑몰 애플리케이션에서 상품 주문 시 사용자가 구매를 "확정"하면, 이는 시스템에 새로운 주문을 추가하는 명령이 됩니다.

- SNS 애플리케이션에서 사용자가 게시물을 작성하거나 수정할 때, 새로운 게시물이 추가되거나 기존 게시물이 수정되는 작업도 모두 명령에 해당됩니다.

 

이러한 명령 작업은 시스템의 상태를 변경하는 것이므로, 주로 트랜잭션 처리가 필요하며, 데이터 일관성을 보장해야 합니다.

 

 주요 특징:

- 상태 변경 작업만 수행합니다.

- 트랜잭션을 통해 데이터의 일관성을 보장합니다.

- 비즈니스 규칙이 적용되는 핵심 로직을 포함합니다.

 

 2) 조회(Query)

조회는 애플리케이션 내에서 데이터의 상태를 확인하는 모든 작업을 의미합니다. 데이터베이스에서 필요한 정보를 검색하거나 필터링하여 사용자에게 보여주는 것과 관련됩니다.

 

 예시:

- 쇼핑몰 애플리케이션에서 사용자가 상품 목록을 볼 때, 이 작업은 데이터베이스에 조회 요청을 보내 상품 정보를 가져오는 것입니다.

- SNS 애플리케이션에서 사용자가 다른 사용자의 게시물을 읽기 위해 프로필을 방문하는 작업도 조회에 해당합니다.

 

조회는 읽기 작업이기 때문에 데이터베이스에 데이터를 저장하지 않으며, 주로 빠르고 효율적인 방식으로 필요한 데이터를 제공하는 데 초점을 맞춥니다.

 

 주요 특징:

- 데이터 읽기 작업만 수행합니다.

- 데이터 일관성 보장보다는 조회 속도와 응답성이 중요합니다.

- 복잡한 비즈니스 로직을 최소화하고, 주로 데이터 필터링 및 정렬 작업을 수행합니다.

 

1. CQRS 패턴에서 Command Query의 분리가 주는 이점

 

1) 성능 최적화 : 명령과 조회의 책임이 분리되어 있으므로, 각각의 작업에 맞는 최적화가 가능합니다. 예를 들어, 조회 모델은 읽기 성능을 극대화하기 위해 캐시나 읽기 전용 데이터베이스를 사용할 수 있습니다.

 

2) 확장성 : Command Query가 독립적으로 설계되었으므로, 서로 다른 방식으로 확장할 수 있습니다. 예를 들어, 조회 요청이 많이 발생할 경우 Query 모델만 추가로 확장하면 됩니다.

 

3) 코드 구조의 명확성 : Command Query를 분리함으로써, 코드에서 데이터 수정과 조회 로직이 혼재되는 것을 방지할 수 있습니다. 이는 코드 유지보수성을 높여주며, 특히 복잡한 비즈니스 로직을 갖춘 애플리케이션에서 유용합니다.

 

4) 데이터 일관성 및 분산 처리 : CQRS는 이벤트 소싱과 결합해 데이터 변경 사항을 이벤트로 기록할 수도 있습니다. (이벤트 소싱(Event Sourcing)은 애플리케이션의 상태 변화를 데이터베이스에 저장할 때 "이벤트(event)" 단위로 기록하는 설계 방식입니다. 이 방식은 데이터를 최종 상태로 저장하는 대신, 상태 변경의 과정을 나타내는 일련의 이벤트를 저장하는 방식입니다.)

 

5) 유연성 : 복잡한 비즈니스 로직을 처리하거나 다중 사용자 환경에서 데이터 일관성을 유지하기 쉬워집니다.

 

 

2.    예시 1 : CQRS가 적용된 전자상거래 애플리케이션

 

전자상거래 시스템에서 CQRS 패턴을 사용한다면 다음과 같은 구조를 갖게 됩니다:

 

1) 명령 모델:

   - 사용자가 제품을 주문할 때 `OrderCommandHandler`가 호출됩니다.

   - 해당 명령은 데이터베이스에 새로운 주문을 추가하며, 트랜잭션을 통해 안정성을 보장합니다.

 

2) 조회 모델:

   - 사용자가 자신의 주문 내역을 조회할 때 `OrderQueryHandler`가 호출됩니다.

   - 이 조회 모델은 캐시 또는 읽기 전용 데이터베이스에서 정보를 가져와, 필요한 데이터만 빠르게 제공합니다.

  

이렇게 각 작업에 따라 최적화된 모델을 사용하므로, 대규모 사용자가 몰리거나 고성능 처리가 필요한 상황에서도 시스템이 안정적이고 빠르게 동작할 수 있습니다. CQRS 패턴은 특히 복잡한 도메인이나 대규모 시스템에서 유용하며, 시스템의 확장성과 성능을 향상시킬 수 있는 장점이 있습니다.

 

3.    CQRS 패턴의 구성 요소

 

1) Command (명령) 모델: 데이터를 변경하는 책임을 맡습니다. 예를 들어, 새로운 데이터를 추가하거나 수정하는 작업이 여기에 속합니다. Command 모델은 상태를 변경하기 위해 비즈니스 로직을 적용하며, 그 결과가 데이터베이스에 반영됩니다. 일반적으로 트랜잭션 관리와 검증 로직이 필요합니다.

 

2) Query (조회) 모델: 데이터를 조회하는 책임을 맡습니다. Query 모델은 데이터를 읽기 전용으로 조회하며, 데이터베이스에 대한 쓰기 작업은 수행하지 않습니다. 필요한 데이터가 빠르게 조회될 수 있도록 최적화된 구조를 가지는 경우가 많습니다.

 

3) 이벤트 소싱 (옵션): CQRS는 이벤트 소싱과 자주 함께 사용됩니다. 이벤트 소싱은 시스템에서 발생한 모든 변경 사항을 "이벤트"로 기록하여 이벤트 스토어에 저장하는 방식입니다. 이를 통해 특정 시점의 시스템 상태를 재현할 수 있으며, 시스템의 안정성과 일관성을 강화할 수 있습니다.

 

4) 데이터 저장소의 분리: CQRS Command 모델과 Query 모델이 서로 다른 데이터 저장소를 사용할 수 있도록 설계될 때 효과적입니다. 예를 들어, Command 모델은 관계형 데이터베이스(RDBMS)를 사용하고, Query 모델은 읽기 성능이 좋은 NoSQL 데이터베이스나 캐싱 솔루션을 사용할 수 있습니다. 이를 통해 성능을 더욱 향상시킬 수 있습니다. (국내 금융 공기업에서 계정계는 Oracle, EDWSybase를 적용한 사례와 비슷한 개념)

 

4. CQRS 패턴의 단점

 

1) 복잡성 증가: 설계가 복잡해지고, 각기 다른 데이터 저장소나 모델을 유지하는 것이 어렵습니다.

 

2) 일관성 문제: Command Query 모델이 다른 저장소를 사용할 경우, 즉각적인 일관성이 보장되지 않을 수 있으며, 이로 인해 잠시 동안 데이터 불일치가 발생할 수 있습니다.

 

5. CQRS 패턴 적용 예시 2 (코드 포함 상세 예시)

 

다음은 CQRS 패턴이 적용된 전자 상거래 시스템의 상세 예시입니다.

 

1) 시나리오

전자 상거래 시스템에서 사용자가 제품을 주문하는 상황을 가정합니다. 주문을 추가하거나 변경하는 작업은 Command 모델에서 처리하고, 사용자에게 주문 내역을 조회하는 작업은 Query 모델에서 처리합니다.

 

2) 구조

a) Command 모델:

   - 사용자가 주문을 추가하거나 변경할 때, Command 모델이 요청을 받아 이를 처리합니다.

   - 예를 들어 `AddOrderCommand` 또는 `UpdateOrderCommand`라는 명령 객체를 생성하여 주문 추가 및 변경을 수행합니다.

   - 주문이 처리된 후 데이터베이스에 주문 정보가 저장됩니다.

 

b) Query 모델:

   - 사용자가 자신의 주문 내역을 조회할 때 Query 모델이 빠르게 정보를 제공합니다.

   - `OrderReadModel`을 최적화하여 사용자에게 필요한 정보만 빠르게 조회할 수 있도록 구성합니다.

   - 예를 들어, Redis ElasticSearch를 사용하여 조회 속도를 높이고, 사용자 경험을 향상시킬 수 있습니다.

 

3) 파이썬 예시 코드 (간단한 CQRS 적용 예시)

 

# Command Model class 예시

class OrderCommandService:

    def add_order(self, order_data):

         주문 데이터 추가 로직

        new_order = Order(id=generate_id(), order_data)

         데이터베이스에 저장

        db.save(new_order)

         이벤트 발행 (옵션)

        EventPublisher.publish(OrderAddedEvent(new_order))

   

    def update_order(self, order_id, updated_data):

         주문 데이터 업데이트 로직

        order = db.find(order_id)

        order.update(updated_data)

        db.save(order)

         이벤트 발행 (옵션)

        EventPublisher.publish(OrderUpdatedEvent(order))

 

# Query Model class 예시

class OrderQueryService:

    def get_order_by_id(self, order_id):

         최적화된 조회 방식 (: Redis)

        return read_db.get(order_id)

   

    def get_orders_by_user(self, user_id):

         최적화된 조회 방식 (: ElasticSearch)

        return read_db.query(user_id=user_id)

 

# Usage 예시

# Command

command_service = OrderCommandService()       인스턴스 생성

command_service.add_order({"user_id": 1, "product_id": 123, "quantity": 2})

 

# Query

query_service = OrderQueryService()                인스턴스 생성

orders = query_service.get_orders_by_user(1)

 

6.    CQRS 패턴 적용 사례

 

CQRS(Command Query Responsibility Segregation) 패턴은 마이크로서비스 아키텍처에서 명령과 조회의 책임을 분리하여 시스템의 확장성과 성능을 향상시키는 데 활용됩니다. 이 패턴을 적용한 사례로는 '배달의 민족'이 있습니다. 배달의 민족은 CQRS를 도입하여 시스템 신뢰성을 높이고, 이벤트 중심 설계를 구현하였습니다.

 

7.    CQRS 패턴 아키텍처 예시

CQRS 패턴 기본 구조

 

8.    관련 기술 1 : 이벤트 소싱

 

이벤트 소싱(Event Sourcing)은 애플리케이션의 상태 변화를 데이터베이스에 저장할 때 "이벤트(event)" 단위로 기록하는 설계 방식입니다. 이 방식은 데이터를 최종 상태로 저장하는 대신, 상태 변경의 과정을 나타내는 일련의 이벤트를 저장하여 해당 객체의 현재 상태를 필요할 때 재구성할 수 있도록 합니다.

 

1)    이벤트 소싱의 주요 개념

 

a) 이벤트(Event) : 이벤트는 시스템 내에서 발생한 특정 상태 변경을 나타내는 불변(Immutable) 데이터입니다. 예를 들어, 쇼핑몰 애플리케이션에서 "상품이 장바구니에 추가됨", "주문이 취소됨" 같은 상태 변경이 각각의 이벤트가 됩니다. 이벤트에는 누가, 언제, 무엇을 했는지에 대한 정보가 포함되어 있습니다.

 

b) 이벤트 스토어(Event Store) : 이벤트는 일반 데이터베이스 대신, 이벤트 스토어에 저장됩니다. 이벤트 스토어는 발생한 모든 이벤트를 시간순으로 기록하며, 각 객체의 상태를 언제든지 재구성할 수 있게 합니다.

 

c) 현재 상태 재구성 : 객체의 현재 상태를 알고 싶을 때, 이벤트 소싱에서는 최초 상태에서부터 현재까지 발생한 모든 이벤트를 순차적으로 적용하여 현재 상태를 복원합니다. 이를 통해 항상 최신 상태를 만들 수 있습니다.

 

2)    이벤트 소싱의 주요 장점

 

a) 데이터의 추적 가능성 및 복구 : 모든 변경 사항이 이벤트로 기록되므로, 시스템 내에서 데이터가 어떻게 변화했는지 과거의 상태를 쉽게 추적할 수 있습니다. 예를 들어, 사용자의 특정 주문 내역이 수정된 이유를 알기 위해 이벤트 기록을 분석할 수 있습니다.

 

b) 데이터 무결성과 일관성 : 이벤트는 발생 순서대로 기록되므로, 이벤트를 재생해 상태를 재구성할 때 데이터 일관성을 보장할 수 있습니다. 이는 분산 시스템에서 특히 유용하며, 이벤트의 순서대로 상태를 복구함으로써 데이터의 무결성을 유지합니다.

 

c) 시간을 되돌리는 기능 : 이벤트 소싱을 사용하면 특정 시점으로 "시간을 되돌릴 수" 있습니다. 예를 들어, 과거 특정 시점의 데이터를 복원하거나 이벤트의 집합을 반복해서 시뮬레이션할 수 있습니다.

 

d) 이벤트 기반 시스템과의 자연스러운 통합 : 이벤트 소싱은 CQRS와 자연스럽게 통합될 수 있습니다. 이벤트 소싱에서 기록된 이벤트를 조회 모델에서 즉각적으로 반영하거나, 필요 시 비동기적으로 반영할 수 있습니다.

 

3)    이벤트 소싱의 단점

 

a) 복잡성 증가: 모든 상태 변화를 이벤트로 기록하므로 이벤트 설계가 중요하며, 이벤트 재구성에 따른 시스템 복잡성이 증가합니다.

b) 성능 문제: 과거부터 모든 이벤트를 재생해 현재 상태를 복원해야 하므로, 이벤트 수가 많은 경우 성능이 저하될 수 있습니다. 이를 해결하기 위해 일정 간격으로 "스냅샷(snapshot)"을 저장하는 방식을 사용합니다.

 

4)    이벤트 소싱 적용 예시

 

 시나리오

전자상거래 시스템에서 사용자가 제품을 주문, 수정, 취소하는 과정에서 상태가 이벤트로 기록된다고 가정합니다.

 

-.1. 사용자가 "노트북"을 주문한 경우, `OrderCreatedEvent`라는 이벤트가 생성되어 이벤트 스토어에 기록됩니다.

-.2. 이후, 사용자가 주문 수량을 변경하면, `OrderUpdatedEvent`가 기록됩니다.

-.3. 사용자가 주문을 취소하면, `OrderCancelledEvent`가 기록됩니다.

 

이벤트 스토어에 다음과 같은 이벤트가 순서대로 기록됩니다:

 

- `OrderCreatedEvent (Order ID: 1, Product: 노트북, Quantity: 1)`

- `OrderUpdatedEvent (Order ID: 1, Quantity: 2)`

- `OrderCancelledEvent (Order ID: 1)`

 

이벤트를 통해 주문 ID 1의 상태를 복원하려면, `OrderCreatedEvent`로 초기 주문을 생성하고, `OrderUpdatedEvent`로 수량을 업데이트한 다음, `OrderCancelledEvent`로 주문 상태를 취소된 상태로 만듭니다.

 

5) 스냅샷(Snapshot) 사용 예시

이벤트가 매우 많아지면 성능 문제가 발생할 수 있습니다. 이를 방지하기 위해 현재 상태를 주기적으로 저장하는 스냅샷을 사용할 수 있습니다.

예를 들어, 위 주문 내역에서 100번째 주문 시점에 대한 스냅샷을 생성하면, 101번째 이벤트부터 재생하여 현재 상태를 복원할 수 있어 성능이 향상됩니다.

 

6) 이벤트 소싱과 CQRS의 결합

이벤트 소싱은 CQRS와 자주 결합됩니다. 명령(Command) 모델은 이벤트를 생성하여 이벤트 스토어에 기록하고, 조회(Query) 모델은 이벤트를 구독(Subscribe : 구독이란 이벤트가 발생할 때 이를 실시간으로 받는 것을 의미함) 하여 데이터베이스에 읽기 전용으로 저장합니다.

예를 들어:

ü  주문이 생성되었다면 OrderCreatedEvent가 발생합니다.

ü  주문 상태가 변경되었다면 OrderStatusUpdatedEvent가 발생합니다.

조회 모델은 이러한 이벤트를 실시간으로 받아서, 각 이벤트에 맞는 데이터 업데이트를 수행합니다.

이를 통해 읽기와 쓰기의 부담을 각각 다른 시스템에 분산시키며 확장성을 높일 수 있습니다.

 

결과적으로 이벤트 소싱을 통해 CQRS 시스템은 복원력 있고 데이터 변화를 추적하기 쉬운 시스템이 됩니다.

 

9.    관련 기술 2 : 이벤트 소싱과 DB Log의 차이점

 

이벤트 소싱(Event Sourcing)과 데이터베이스 로그(DB Log)는 둘 다 데이터 변경 사항을 기록한다는 점에서 유사하지만, 그 목적과 사용 방식에서 중요한 차이가 있습니다. 각 개념의 차이를 살펴보겠습니다.

 

1)    목적 및 용도

- 이벤트 소싱 : 시스템의 상태 변화를 비즈니스 이벤트 단위로 기록하는 것을 목표로 합니다. 이벤트 소싱은 비즈니스 로직의 흐름을 추적할 수 있도록 설계되었으며, 특정 시점의 상태를 재구성하거나 변경 이력을 추적하는 데 적합합니다.

- DB 로그 : 데이터베이스 로그는 주로 데이터의 일관성과 무결성 보장을 목적으로 합니다. 데이터베이스에서 발생하는 모든 트랜잭션(삽입, 수정, 삭제)을 순서대로 기록하여 장애 상황에서 복구하거나 트랜잭션을 롤백하는 용도로 사용됩니다.

 

 2) 기록 내용

- 이벤트 소싱 : 비즈니스 이벤트가 기록됩니다. 예를 들어, "사용자가 주문을 생성함", "수량을 변경함", "주문을 취소함" 등의 비즈니스 도메인에서 발생한 이벤트가 저장됩니다. 각 이벤트에는 발생 시점, 관련된 도메인 객체의 ID, 변경된 상태 등이 포함됩니다.

- DB 로그 : 데이터베이스 로그는 로우 수준의 변경 사항을 기록합니다. 예를 들어, 특정 테이블의 특정 행이 수정되었다면, 이전 값과 새로운 값이 기록될 수 있습니다. 로그에는 데이터베이스 트랜잭션의 세부 정보가 포함되며, 비즈니스 이벤트보다는 데이터 변경에 초점을 맞춥니다.

 

 3) 사용 방식 및 재생

- 이벤트 소싱 : 특정 시점의 상태를 재구성하기 위해 이벤트 소스를 순차적으로 재생합니다. 시스템의 상태를 처음부터 모든 이벤트를 적용하여 복구할 수 있으며, 과거의 이벤트를 분석하여 시스템의 동작을 추적하거나 감사(Audit) 목적으로 활용할 수 있습니다.

- DB 로그 : 데이터베이스의 로그는 주로 장애 발생 시 데이터 복구나 트랜잭션 롤백을 위해 사용됩니다. 예를 들어, 복구할 때 로그에 기록된 마지막 일관성 있는 시점까지 되돌리거나 롤백합니다. DB 로그는 이벤트 소싱처럼 비즈니스 로직을 재생하기 위한 용도가 아니며, 트랜잭션 단위의 무결성 유지에 중점을 둡니다.

 

 

 4) 저장 위치와 형식

- 이벤트 소싱 : 이벤트는 보통 이벤트 스토어라는 별도의 저장소에 기록됩니다. 이벤트 스토어는 비즈니스 이벤트를 시간순으로 저장하며, 이를 통해 조회 및 재구성을 빠르게 할 수 있습니다. 이벤트는 비즈니스 도메인과 연관된 형식으로 기록됩니다.

- DB 로그 : 데이터베이스 로그는 DBMS 내부에서 관리되며, 주로 바이너리 형식으로 저장되어 쉽게 접근하거나 읽을 수 없습니다. 로그는 데이터베이스의 내부 트랜잭션 처리 메커니즘에 의존하며, 외부에서 분석하거나 읽기 위한 형식이 아닙니다.

 

 5) 데이터 일관성과 무결성

- 이벤트 소싱 : 이벤트 소싱은 "최종 일관성"을 보장하는 방식입니다. 이벤트는 특정 순서대로 적용되어 시스템의 상태를 재구성하지만, 모든 이벤트가 실시간으로 일관되게 반영되지 않을 수 있습니다. 일관성 요구 사항에 따라 다른 모델이나 CQRS와 결합하여 상태를 조회할 수 있습니다.

 

- DB 로그 : DB 로그는 데이터베이스의 즉각적인 일관성을 보장합니다. 트랜잭션 로그는 DB ACID(원자성, 일관성, 고립성, 지속성) 속성을 유지하는데 필수적이며, DBMS가 데이터의 무결성과 일관성을 강력하게 보장하는 데 필요한 요소입니다.

 

6) 이벤트소싱과 DB로그 비교 요약 테이블

이벤트 소싱과 DB로그 비교

 

이벤트 소싱과 DB 로그는 비슷한 기능을 하지만, 이벤트 소싱은 시스템의 비즈니스 로직에 맞춘 변화를 기록하고 추적할 때, DB 로그는 주로 데이터베이스 복구와 무결성 유지를 위해 존재한다는 점에서 차이가 있습니다.