🐈

マイクロサービスとイベント駆動の考え方、間違ってませんか?

に公開

はじめに

俊敏で、変更に強く、スケールしやすい──。「マイクロサービス」という言葉には、そんな理想的なシステムへの期待が込められています。多くの開発現場でその導入が進められていますが、その理想は実現できているでしょうか?

現実には、
「サービスを分割したはずなのに、なぜか一つの変更が広範囲に影響してしまう」
「チーム間の調整コストが減るどころか、むしろ増えてしまった」
といった声も少なくありません。

こうした悩みは、マイクロサービスを単に「物理的にサービスを分けること」だと誤解していることから生まれます。この課題を解決する鍵こそが、「イベント駆動」という設計思想です。

ただし、安心してはいけません。イベント駆動にもまた、陥りやすい罠が存在します。この記事では、マイクロサービスのよくある誤解を解きほぐし、その処方箋となるイベント駆動アーキテクチャ、そしてその実践におけるアンチパターンまでを紐解いていきます。

1. よくある誤解①:UI/バックエンド分離 ≠ マイクロサービス

まず、最も基本的で広範囲にわたる誤解から解きほぐしましょう。
「UI(フロントエンド)とバックエンドをAPIで通信するように分離する構成」は、現代のWebアプリケーション開発における非常に優れたベストプラクティスです。これはしばしば「ヘッドレスアーキテクチャ」と呼ばれます。

しかし、これはマイクロサービスそのものではありません。

アーキテクチャの「スコープ」を理解する

この誤解を解く鍵は、アーキテクチャが関心を持つ**スコープ(範囲)**の違いを理解することです。

オニオンアーキテクチャ:単一アプリケーションの「内部設計図」

オニオンアーキテクチャ(クリーンアーキテクチャ、ヘキサゴナルアーキテクチャ)は、単一のアプリケーションやサービスの「内部」をどのように整理整頓するかという設計思想です。ビジネスの核となるドメインロジックを中央に置き、UI、データベース、外部APIといった具体的な技術への依存を外側に追い出すことで、変更に強く、テストしやすいコードを目指します。

伝統的なモノリシックなWebアプリでは、このアーキテクチャの「UI層(プレゼンテーション層)」は、サーバーサイドでHTMLを生成するビューやコントローラーを指していました。

UI/バックエンド分離:プレゼンテーション層の物理的な独立

UI/バックエンド分離アーキテクチャでは、このプレゼンテーション層が、完全に独立した「UIアプリケーション」として物理的に分離されます。そして、残りの部分が「バックエンドアプリケーション」となります。

ここからが重要なポイントです。

では、バックエンドアプリケーションにおける「プレゼンテーション層」はどこへ行ったのでしょうか?

それは、APIのエンドポイント(コントローラー層)に姿を変えます。
バックエンドにとっての「UI(ユーザーインターフェース)」とは、もはや人間のための画面(Graphical User Interface)ではなく、**プログラム(この場合はUIアプリケーション)のためのインターフェース(Application Programming Interface)**なのです。

バックエンドのAPIコントローラーは、HTTPリクエストを受け取り、それを内側のアプリケーションサービス(ユースケース)が理解できる形に変換し、結果をJSONなどの形式で返すという、プレゼンテーション層としての役割を変わらず担っています。

「それぞれに適用する」の真意

この考え方を推し進めると、「UIアプリケーション」も「バックエンドアプリケーション」も、それぞれが一つの独立したアプリケーションとして、内部にクリーンなアーキテクチャを持つことができます。

  • バックエンドアプリケーション: APIコントローラーをプレゼンテーション層とし、内部にオニオンアーキテクチャを適用して、ドメインロジックを守ります。
  • UIアプリケーション: ReactやVueで作られたSPAも、複雑化すれば内部構造の整理が必要です。UIコンポーネントをプレゼンテーション層、API通信部分をインフラ層、状態管理(State)をドメイン層と見立てて、オニオンアーキテクチャに似たクリーンな構造を適用することができます。
アーキテクチャ 関心事 (Concern) 適用範囲 (Scope) 目的
UI/バックエンド分離 プレゼンテーションとビジネスロジックの物理的な分離 システム全体 チームの分離、複数クライアント対応
オニオンアーキテクチャ ビジネスロジックの依存関係の分離 単一のアプリケーション/サービス内部 保守性、テスト容易性の向上
マイクロサービス バックエンドのビジネス機能ごとの分割 バックエンド全体 独立したデプロイ、スケーラビリティ

APIの裏側が、オニオンアーキテクチャで見事に設計された巨大な一枚岩のアプリケーション(モノリス)であっても、UIとAPIで通信する構成は成り立ちます。

APIの裏側が、受注サービス在庫サービスユーザーサービスといった、独立してデプロイ可能な小さなサービスの集合体で構成されており、さらにその個々のサービスがオニオンアーキテクチャで実装されている場合、それが理想的な「マイクロサービスアーキテクチャ」なのです。

結論: UIとバックエンドの分離、そして個々の内部実装としてのオニオンアーキテクチャは、マイクロサービスとは別のレイヤーの話です。これらを混同しないことが、適切な設計への第一歩です。

2. よくある誤解②:DBの共有とトランザクションの罠

「サービスは分けたけど、DBは同じものを共有している」

これは、独立したサービスを目指す上での典型的なアンチパターンです。サービスが物理的に分かれていても、データベースという強力な結合点が存在するため、実質的な「分散モノリス」に陥ってしまいます。

  • 何が問題か?:
    受注サービスがテーブルのスキーマを変更したら、同じテーブルを見ている在庫サービスが動かなくなる可能性があります。これでは、サービスを独立してデプロイできません。
  • 正しい考え方:
    各サービスは自身のデータを完全に所有し、独自のデータベースを持ちます。

しかし、ここで多くの開発者が新たな壁にぶつかります。
「DBを分けたら、複数のサービスにまたがる処理の整合性(トランザクション)はどう担保すればいいんだ?」

具体例:予約システムのアンチパターン

ホテルの予約システムを考えてみましょう。ユーザーが予約を確定するとき、以下の処理が**「全て成功」するか「全て失敗」する**必要があります。

  1. 在庫テーブルから部屋の空きを確保する
  2. 予約テーブルにレコードを記録する
  3. 決済を行う

DBを共有している場合、これは単一のDBトランザクションで簡単に実現できます。
しかし、DBを分離したマイクロサービス(在庫サービス予約サービス決済サービス)でこれを実現しようとすると、複数のDBにまたがる分散トランザクションが必要になります。これは非常に複雑で、システム全体の可用性を著しく低下させるため、マイクロサービスでは原則として採用されません。

3. 処方箋としてのイベント駆動とSagaパターン

このトランザクション問題を解決し、真の「疎結合」を実現するのがイベント駆動と、その上で動作するSaga(サーガ)パターンです。

Sagaパターンは、一連のローカルトランザクション(各サービス内での閉じたトランザクション)を、イベントを介して連鎖させることで、ビジネスプロセス全体の結果整合性を保つ設計パターンです。

Sagaパターンによる予約システムの再設計

  1. 予約リクエストとイベントの発行:

    • ユーザーが予約をリクエストすると、まず予約サービスが「ReservationRequested(予約リクエスト済み)」というイベントを発行します。予約自体のステータスはまだ「処理中」です。
  2. 在庫サービスの反応:

    • 在庫サービスがこのイベントを購読します。
    • 自身のDB内でローカルトランザクションを開始し、部屋の在庫を確保(ステータスを「仮押さえ」に)します。
    • 成功したら、「RoomHeld(部屋確保済み)」というイベントを発行します。
  3. 決済サービスの反応:

    • 決済サービスが「RoomHeld」イベントを購読します。
    • 決済処理を実行します。
    • 成功したら、「PaymentProcessed(決済完了)」というイベントを発行します。
  4. プロセスの完了:

    • 予約サービスが「PaymentProcessed」イベントを購読し、自身の予約ステータスを「確定」に更新します。
    • これで一連のビジネスプロセスは完了です。

もし途中で失敗したら? - 補償トランザクション

Sagaパターンの真価は、失敗時のリカバリーにあります。もし決済が失敗した場合、逆のイベントを発行してプロセスを巻き戻します。

  • 決済サービスが「PaymentFailed(決済失敗)」イベントを発行します。
  • 在庫サービスがこのイベントを購読し、補償トランザクションを実行します。つまり、仮押さえしていた部屋を解放(ステータスを「空き」に戻す)し、「RoomReleased(部屋解放済み)」イベントを発行します。
  • 予約サービスが「PaymentFailed」イベントを購読し、予約ステータスを「失敗」に更新します。

このように、Sagaパターンはビジネスプロセス全体を「最終的に」一貫性のある状態に導きます(結果整合性)。これは厳密なACIDトランザクションとは異なりますが、分散システムにおいて可用性と独立性を保つための、現実的で強力な解決策なのです。

4. イベント駆動の罠:「コマンド」と「イベント」の混同

イベント駆動の力を正しく引き出すためには、そのメッセージが何を意図しているのかを明確に区別する必要があります。

罠:「コマンド」と「イベント」の混同

サービス間の連携をAmazon MSK (Kafka), Kinesis Data Streams, EventBridge, SNSといったメッセージング基盤で行う際に、トピックやストリームの設計を間違えるケースが非常に多いです。
※以降はMSKを例にトピック(イベントが発行される場所)やコンシューマーグループという言葉を用います。

悪い例: order-stock という名前のトピックを作る
この名前は、「orderサービスからstockサービスへ送るための専用レーン」という意味を持ちます。これは一見分かりやすそうですが、実は「コマンド」の発想です。「受注があったから、在庫を処理しろ」という命令を送っているのです。

  • 何が問題か?:
    1. 密結合: 受注サービスは、在庫サービスの存在と役割を知ってしまっています。
    2. 拡張性の欠如: もし将来、「受注したらメールも送りたい」となった場合、受注サービスのコードを修正して、新しいorder-mailトピックにもメッセージを送る必要が出てきます。新しい機能を追加するたびに、発行元のサービスまで修正が必要になるのです。

正しい考え方:「事実」を宣言する

イベント駆動の核心は、「過去に起きた事実」を宣言することです。誰かに命令するのではなく、公共の広場(トピック)で「こんなことがありました!」とアナウンスするイメージです。

良い例: orders.events という名前のトピックを作る

このトピックは「受注ドメインで起きた出来事」を扱う場所です。受注サービスは、ここに「受注が完了しました(OrderPlaced)」という事実を書き込むだけです。

そのアナウンスを誰が聞いているか、聞いた人が何をするかについて、受注サービスは一切関知しません。

5. イベント駆動を支える仕組み

この「事実の宣言」モデルを支えるのが、メッセージング基盤と、そこで使われる重要な概念です。

コンセプト 例 (Kafka/Kinesis) 例 (AWSサーバーレス) 役割
広場 トピック / ストリーム SNSトピック / EventBridgeイベントバス イベントが発行される場所
聴衆のグループ コンシューマーグループ / KCLアプリ SQSキュー 同じ目的を持つ聴衆の集まり

シナリオ:受注イベントを在庫とメールサービスが処理する

  1. イベントの発行:
    受注サービスは、orders.eventsトピックに「OrderPlaced」というイベントを発行します。

  2. イベントの購読:

    • 在庫サービス: 自分の責務(在庫管理)を果たすため、stock-service-groupという名前でorders.eventsトピックを聞いています。
    • メールサービス: 自分の責務(メール送信)を果たすため、mail-service-groupという名前でorders.eventsトピックを聞いています。

なぜ「コンシューマーグループ」を分けるのか?

これが最も重要なポイントです。メッセージング基盤は、コンシューマーグループごとに「どこまで話を聞いたか」を独立して記憶しています。

  • 在庫サービスがイベントを聞いたからといって、メールサービスが聞けなくなることはありません。
  • 放送局のラジオ番組を、AさんとBさんがそれぞれ別のラジオで聞いているのと同じです。Aさんが聞いたからといって、Bさんが聞けなくなることはありません。

もし、コンシューマーグループを同じにしてしまうと、それは「処理の分担」モードになります。在庫処理が重いので、複数のサーバーで手分けして処理する、といったスケールアウトの際に使います。

目的 コンシューマーグループ 結果
違うことをしたい(在庫処理とメール送信) 分ける 各グループにメッセージが複製される
同じことを速くしたい(在庫処理の並列化) 同じにする グループ内でメッセージが分散される

6. 「1トピック = 1コンシューマー」というルールの罠

ここまでくると、なぜ「1つのトピックは、1つのコンシューマー(グループ)しか持ってはいけない」というルールがアンチパターンであるかが分かります。

このルールを設けることは、せっかくの「放送局モデル(1対多)」を、わざわざ「専用電話回線モデル(1対1)」に戻す行為です。システムの柔軟性と拡張性を自ら捨てていることに他なりません。

まとめ

あなたのマイクロサービスが真に疎結合でスケーラブルであるか、以下の質問でチェックしてみてください。

  • 質問1: あなたのアーキテクチャは、バックエンドが分割されているから「マイクロサービス」ですか?それとも、UIとバックエンドが分離しているからですか?(それらは別の話です!)
  • 質問2: 新しい機能(例: 受注イベントを元に分析するサービス)を追加する際、既存の受注サービスのコードを修正する必要がありますか?
    • Yes: あなたのシステムは密結合です。トピックの設計が「コマンド型」になっている可能性があります。
    • No: 素晴らしい!あなたのシステムは疎結合で、拡張性が高いと言えます。

イベント駆動は、単に非同期でメッセージをやり取りする技術ではありません。発行者と消費者の責任を完全に分離し、システムの進化を容易にするための設計思想です。

あなたの設計が、「命令」ではなく「事実の宣言」に基づいているか、もう一度見直してみてはいかがでしょうか。

Discussion