okpy

Pythonエンジニア兼テックリーダーが、多くのプロジェクトとチーム運営から得た実践的な知識を共有するブログです。

スケーラビリティ向上のためのCQRSとイベントソーシング活用法

近年、マイクロサービスアーキテクチャや分散システムが普及するにつれ、データの整合性を保ちつつ、パフォーマンスを向上させる ことが重要になっています。
その解決策の一つとして、CQRS(Command Query Responsibility Segregation)とイベントソーシング が注目されています。

本記事では、CQRSとイベントソーシングの基本概念、オブジェクト指向設計との関係、Javaを用いた実装例を紹介します。


1. CQRS(コマンドクエリ責務分離)とは?

1-1. CQRSの基本概念

CQRS(Command Query Responsibility Segregation:コマンドクエリ責務分離) は、データの書き込み(Command)と読み込み(Query)の処理を分離する設計パターン です。
従来のアーキテクチャでは、データベースの読み書きが同じモデルで行われるため、パフォーマンスやスケーラビリティに課題が生じることがありました。

CQRSでは、以下のように書き込み用(Command)読み込み用(Query) のモデルを分離します。

項目 従来のアプローチ CQRS
データ操作 読み書きが同じデータモデル 読み書きを異なるモデルで処理
パフォーマンス 読み取りと書き込みが競合しやすい 読み取り専用のデータ構造を利用可能
スケーラビリティ 同じDBを使うため負荷が集中 読み取りと書き込みを別々にスケール可能
データの一貫性 即時整合性が必要 最終的な整合性を考慮

2. CQRSの実装

2-1. CQRSのアーキテクチャ

CQRSを適用したアーキテクチャは、次のような構成になります。

+-------------------------------+
| クライアント(API, UI)        |
+-------------------------------+
         |    |
         ▼    ▼
+-------------------------------+
| コマンドハンドラー(Command)  |
| (注文作成、更新)             |
+-------------------------------+
         |  
         ▼  
+-------------------------------+
| 書き込みデータストア(DB)      |
+-------------------------------+
         |  
         ▼  
+-------------------------------+
| イベントハンドラー(Event)     |
+-------------------------------+
         |  
         ▼  
+-------------------------------+
| 読み込みデータストア(Read DB) |
+-------------------------------+
         |  
         ▼  
+-------------------------------+
| クエリハンドラー(Query)       |
| (注文一覧取得)                |
+-------------------------------+

2-2. コマンド(Command)の実装

コマンドは、データの変更(作成・更新・削除) を担当します。

注文作成コマンド

public class CreateOrderCommand {
    private String orderId;
    private String customerName;
    private double amount;

    public CreateOrderCommand(String orderId, String customerName, double amount) {
        this.orderId = orderId;
        this.customerName = customerName;
        this.amount = amount;
    }

    public String getOrderId() { return orderId; }
    public String getCustomerName() { return customerName; }
    public double getAmount() { return amount; }
}

コマンドハンドラー

import org.springframework.stereotype.Service;

@Service
public class OrderCommandHandler {
    private final OrderRepository orderRepository;

    public OrderCommandHandler(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void handle(CreateOrderCommand command) {
        Order order = new Order(command.getOrderId(), command.getCustomerName(), command.getAmount());
        orderRepository.save(order);
    }
}

2-3. クエリ(Query)の実装

クエリは、データの取得(検索) を担当します。

クエリモデル

public class OrderQueryModel {
    private String orderId;
    private String customerName;
    private double amount;

    public OrderQueryModel(String orderId, String customerName, double amount) {
        this.orderId = orderId;
        this.customerName = customerName;
        this.amount = amount;
    }

    public String getOrderId() { return orderId; }
    public String getCustomerName() { return customerName; }
    public double getAmount() { return amount; }
}

クエリハンドラー

import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class OrderQueryHandler {
    private final OrderReadRepository orderReadRepository;

    public OrderQueryHandler(OrderReadRepository orderReadRepository) {
        this.orderReadRepository = orderReadRepository;
    }

    public List<OrderQueryModel> handle() {
        return orderReadRepository.findAll();
    }
}

3. イベントソーシングの活用

3-1. イベントソーシングとは?

イベントソーシング(Event Sourcing) は、データの状態をイベントの履歴 として記録し、イベントの再生によって現在の状態を構築する手法です。

3-2. イベントの定義

public class OrderCreatedEvent {
    private String orderId;
    private String customerName;
    private double amount;

    public OrderCreatedEvent(String orderId, String customerName, double amount) {
        this.orderId = orderId;
        this.customerName = customerName;
        this.amount = amount;
    }

    public String getOrderId() { return orderId; }
    public String getCustomerName() { return customerName; }
    public double getAmount() { return amount; }
}

3-3. イベントを処理するイベントハンドラ

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Service
public class OrderEventHandler {
    private final OrderReadRepository orderReadRepository;

    public OrderEventHandler(OrderReadRepository orderReadRepository) {
        this.orderReadRepository = orderReadRepository;
    }

    @KafkaListener(topics = "order-events", groupId = "order-group")
    public void handleOrderCreated(OrderCreatedEvent event) {
        OrderQueryModel queryModel = new OrderQueryModel(event.getOrderId(), event.getCustomerName(), event.getAmount());
        orderReadRepository.save(queryModel);
    }
}

ポイント: - OrderService はイベントを発行し、OrderEventHandler はイベントを処理して 読み込み用データベース を更新。 - 最終的な整合性(Eventual Consistency) を確保しつつ、高速なデータ取得が可能になる。


4. まとめ

CQRSとイベントソーシングを活用することで、パフォーマンスと整合性のバランスを最適化 できます。
特に、読み取りと書き込みを分離することでスケーラビリティを向上 させ、イベントソーシングによりデータの履歴を管理 できます。

次回の記事では、以下のトピックを取り上げる予定です:

質問やリクエストがあれば、ぜひコメントでお知らせください! 🚀