💫

CQRSとイベントソーシング:複雑なデータを捌くアーキテクチャの真価

に公開

CQRSとイベントソーシング:複雑なデータを捌くアーキテクチャの真価

はじめに

当初、私はCQRS(コマンド・クエリ責務分離)の「書き込みと読み取りを分離する」という考え方を学んでいました。その中で、実際のシステムで下記の流れに触れました。

この記事は、CQRSの基本から一歩進み、この一連の仕組みの核心である イベントソーシング(Event Sourcing, ES) と組み合わせたアーキテクチャ、いわゆる 「ES + CQRS」 について、その全体像と強力な理由を解き明かすための学習の記録です。

間違った理解をしていたら指摘していただきたいです🙏

CQRSの基本原則 - なぜ分離するのか?

CQRSとは 「システムのデータ操作を、『書き込み処理』と『読み取り処理』で、はっきりと別の道に分けて開発しましょう」 という設計の考え方です。CQRSの正式名称は「Command Query Responsibility Segregation」で、日本語では「コマンド・クエリ責務分離」と言われます。

多くのシステムで採用されている CRUDでは、書き込み(Create, Update, Delete)と読み取り(Read)を、同じデータモデルで扱うのが一般的です。これは多くの場面でシンプルかつ効率的な素晴らしい設計です。

しかし、アプリケーションが大規模化・複雑化し、「書き込み」と「読み取り」の要求が大きくかけ離れてくると、この単一モデルのアプローチがいくつかの課題に直面することがあります。

モデルとは何か?

CQRSの核となる「読み取り・書き込みモデルの分離」を理解するためには、まず 「モデル」 という言葉の認識を揃えておく必要があります。ここでは、 システムが扱うデータの「構造」と、そのデータに対する「操作」を合わせたもの、と理解してください。

たとえば、ECサイトで「商品」を扱う場合を考えてみましょう。

  • 書き込みモデル (Write Model):商品をデータベースに保存したり、更新したりするためのモデルです。このモデルは、在庫数、価格、販売期間、商品説明、配送情報など、商品を正確に表現し、ビジネスルールを適用するために必要なすべての情報を含みます。また、これらの情報を変更する 「操作」(例:商品を追加する、価格を変更する、在庫を更新する)もこのモデルに含まれます。データの整合性を厳密に保つことが最優先されます。

  • 読み取りモデル (Read Model):ユーザーに商品情報を表示するためのモデルです。例えば「商品一覧」や「商品詳細」画面に表示される情報に特化します。商品名、価格、サムネイル画像、簡単な説明など、表示に必要な最小限のデータで構成されることが多いです。このモデルは、ユーザーからの 「クエリ」(例:商品一覧を取得する、特定の商品情報を検索する)に応答することに特化しており、高速なデータ取得が求められます。

このように、CQRSでは「書き込み」と「読み取り」という異なる目的のために、それぞれに最適な「モデル」を用意し、分離して開発する考え方です。これにより、それぞれの関心事(ビジネスロジックと表示ロジック)を独立して最適化できるようになります。

モデルの複雑化

ECサイトを例に考えてみます。「商品を登録する」という書き込み処理では、在庫数や価格、販売期間など、厳密なビジネスルールをチェックする必要があります。一方、「商品一覧を表示する」という読み取り処理では、商品名と価格、サムネイル画像など、表示に必要な最低限のデータだけが欲しいはずです。この二つの要求を一つのデータモデルで満たそうとすると、モデルがどんどん複雑で巨大になってしまいます。

パフォーマンスの問題

書き込み処理と読み取り処理では、求められる性能が全く異なります。読み取りは高速なレスポンスが求められることが多い一方、書き込みはデータの整合性を保つことが最優先されます。同じ仕組みを使っていると、両方の要求を同時に満たすのが難しくなり、パフォーマンスのボトルネックになることがあります。

CQRSは、こうした特定の課題を解決するために、2つの関心を分離し、それぞれの道に最適化された実装を可能にする設計の選択肢の一つです。

CQRSの実現と課題 - データストアの分離と「同期」の問題

CQRSの考え方を進めると、読み取りと書き込みの「モデル」を分離するだけでなく、実際にそれらのデータを保持するデータストア(データベース)を物理的に分離する構成に至ることがあります。ただし、CQRSは必ずしもデータストアの物理的な分離を要求するものではありません。同じデータストア内で論理的にモデルを分離するだけでもCQRSは実現可能です。

しかし、もしデータストアを分離すると、以下のような構成になるでしょう。

コマンド側データストア (Write Store):

データの整合性を厳密に保つ、書き込みに最適化されたデータベース

クエリ側データストア (Read Store):

特定の画面表示用にデータを非正規化し、読み取り速度を最大化したデータベース

データストアを物理的に分離することで、究極のパフォーマンス最適化や独立したスケーラビリティといった強力なメリットが生まれます。しかし、同時に大きな課題が生まれます。それは「どうやって、この2つのデータストアのデータを同期させるのか?」という問題です。

この問題を解決する答えが、イベントソーシングです。

解決策としてのイベントソーシング(ES)

イベントソーシングは、データの 「最新の状態」を保存するのではなく、「状態を変化させたイベントの履歴」 をすべて保存する、という考え方です。

従来の方法(状態を保存) イベントソーシング(イベント履歴を保存)
在庫数 = 9 という結果を上書き保存する。 「商品入荷(+10)」 「商品が売れた(-1)」 という事実を時系列で追記していく。

現在の在庫数は、イベントを最初から再生(足し引き)すればいつでも計算できます。そして、この「イベント」こそが、CQRSのコマンド側とクエリ側をつなぐ完璧な架け橋となるのです。

【図解】CQRS + イベントソーシングの全体像

下記フローがこのアーキテクチャでどう実現されるかを見ていきます。

ここでは「商品の価格を$100から$90に変更する」という例で説明します。

【コマンド側のフロー】

  • コマンドの実行: クライアントが「商品価格を$90に変更せよ」という コマンド をAPIに送ります。
  • ビジネスロジックの実行: コマンドハンドラが、対象の商品オブジェクト(集約)を呼び出します。集約は、ドメイン駆動設計における整合性境界を表す概念で、ビジネスロジックと状態の変更をカプセル化します。
  • イベントの生成: 商品オブジェクトは、自身の価格を直接変更するのではなく、「商品価格が$90に変更された(PriceUpdated)」という過去形の イベント を生成します。
  • イベントの永続化: 生成されたイベントが、イベントストアと呼ばれる書き込み専用DBに追記されます。

【同期のフロー】

  • イベントの発火: イベントがイベントストアに保存されると、それが「イベントが発火」したことになります。このイベントはメッセージブローカー[1]を通じてシステム内に通知されます。
  • イベントの購読: プロジェクターと呼ばれるコンポーネントが、このPriceUpdatedイベントを購読しています。プロジェクターはイベントを読み取り、それを元にリードモデルを構築・更新する役割を担います。
  • リードモデルの更新: プロジェクターはイベントの内容(商品ID、新しい価格など)を受け取り、クエリ用のDB(リードDB)の該当商品の価格を$90に更新します。

【クエリ側のフロー】

ユーザーが商品詳細ページを開くと、クエリAPIは、すでに画面表示に最適化されたリードDBから、加工済みの商品データを高速に取得して返すだけです。

イベントソーシングCQRSの特徴

メリット

  • 完全な監査ログ: 「誰が、いつ、何を」したかが、すべて変更不可能なイベントとして記録される。これは監査や会計の要件が厳しいシステムでは絶大な力を発揮する。
  • デバッグと分析の容易化(時間旅行): 何か問題が起きたとき、イベントを再生することで、任意の過去の時点のシステム状態を正確に再現できる。
  • 驚異的な柔軟性: 将来、「管理画面用に新しい分析データが欲しい」となった場合でも、過去のイベントをすべて再生して、全く新しいリードモデル(クエリ用DB)を後からでも自由に構築できる。既存のシステムに一切影響を与えない。

デメリット

  • 学習曲線と複雑性: ご覧の通り、構成要素が多く、従来のCRUDとは全く異なる考え方のため、チーム全体の高い学習コストが求めらる
  • 結果整合性: 書き込みがリードDBに反映されるまでにタイムラグがある。ユーザー操作の直後に最新状態が表示される必要がないか、慎重な検討が必要です。
  • イベントのスキーマ管理: 一度保存したイベントは不変。将来ビジネスルールが変更され、イベントの構造を変えたい場合に、過去のイベントとの互換性を保つためのバージョン管理戦略が不可欠になる。

まとめ

CQRSとイベントソーシングを組み合わせたアーキテクチャは、複雑で多様なデータを扱う現代のサービスにとって、大きい恩恵をもたらします。 この設計の本質的な強みは、特にデータ利用の多様化とサービス間の独立性確保という点で極めて有効です。

私たちがこの設計を選んだのは、CQRSとイベントソーシングを組み合わせたアーキテクチャが持つ上記の大きな恩恵を受けたいからです。データが多様に利用される状況において、各サービスの独立性をしっかり保ちながら、それぞれに合ったデータの使い方を可能にする点が、まさに求めていたものでした。

「書き込み」と「読み取り」の責務を明確に分離し、イベントストリームを通じてデータを非同期で連携させるこのアプローチは、各サービスが必要な情報だけを効率的に利用できる柔軟性を提供します。データの更新にリアルタイム性が厳しく求められないシステムであれば、結果整合性を受け入れることで、この非同期モデルはさらに大きな力を発揮します。

しかし、この強力なパターンも万能ではありません。学習コストの高さや、データの更新が少し遅れる「結果整合性」という特性への理解も欠かせません。導入にはじっくり検討が必要です。それでも、私たちが達成したい「複雑なデータを最適化し、サービスを独立させる」という目標を考えると、CQRSとイベントソーシングはまさにベストな選択だと確信し、採用に踏み切りました。

脚注
  1. 異なるシステムやアプリケーション間でメッセージの送受信を仲介するソフトウェア ↩︎

Discussion