マイクロサービスとイベント駆動の考え方、間違ってませんか?
はじめに
俊敏で、変更に強く、スケールしやすい──。「マイクロサービス」という言葉には、そんな理想的なシステムへの期待が込められています。多くの開発現場でその導入が進められていますが、その理想は実現できているでしょうか?
現実には、
「サービスを分割したはずなのに、なぜか一つの変更が広範囲に影響してしまう」
「チーム間の調整コストが減るどころか、むしろ増えてしまった」
といった声も少なくありません。
こうした悩みは、マイクロサービスを単に「物理的にサービスを分けること」だと誤解していることから生まれます。この課題を解決する鍵こそが、「イベント駆動」という設計思想です。
ただし、安心してはいけません。イベント駆動にもまた、陥りやすい罠が存在します。この記事では、マイクロサービスのよくある誤解を解きほぐし、その処方箋となるイベント駆動アーキテクチャ、そしてその実践におけるアンチパターンまでを紐解いていきます。
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を分けたら、複数のサービスにまたがる処理の整合性(トランザクション)はどう担保すればいいんだ?」
具体例:予約システムのアンチパターン
ホテルの予約システムを考えてみましょう。ユーザーが予約を確定するとき、以下の処理が**「全て成功」するか「全て失敗」する**必要があります。
- 在庫テーブルから部屋の空きを確保する
- 予約テーブルにレコードを記録する
- 決済を行う
DBを共有している場合、これは単一のDBトランザクションで簡単に実現できます。
しかし、DBを分離したマイクロサービス(在庫サービス
、予約サービス
、決済サービス
)でこれを実現しようとすると、複数のDBにまたがる分散トランザクションが必要になります。これは非常に複雑で、システム全体の可用性を著しく低下させるため、マイクロサービスでは原則として採用されません。
3. 処方箋としてのイベント駆動とSagaパターン
このトランザクション問題を解決し、真の「疎結合」を実現するのがイベント駆動と、その上で動作するSaga(サーガ)パターンです。
Sagaパターンは、一連のローカルトランザクション(各サービス内での閉じたトランザクション)を、イベントを介して連鎖させることで、ビジネスプロセス全体の結果整合性を保つ設計パターンです。
Sagaパターンによる予約システムの再設計
-
予約リクエストとイベントの発行:
- ユーザーが予約をリクエストすると、まず
予約サービス
が「ReservationRequested
(予約リクエスト済み)」というイベントを発行します。予約自体のステータスはまだ「処理中」です。
- ユーザーが予約をリクエストすると、まず
-
在庫サービスの反応:
-
在庫サービス
がこのイベントを購読します。 - 自身のDB内でローカルトランザクションを開始し、部屋の在庫を確保(ステータスを「仮押さえ」に)します。
- 成功したら、「
RoomHeld
(部屋確保済み)」というイベントを発行します。
-
-
決済サービスの反応:
-
決済サービス
が「RoomHeld
」イベントを購読します。 - 決済処理を実行します。
- 成功したら、「
PaymentProcessed
(決済完了)」というイベントを発行します。
-
-
プロセスの完了:
-
予約サービス
が「PaymentProcessed
」イベントを購読し、自身の予約ステータスを「確定」に更新します。 - これで一連のビジネスプロセスは完了です。
-
もし途中で失敗したら? - 補償トランザクション
Sagaパターンの真価は、失敗時のリカバリーにあります。もし決済が失敗した場合、逆のイベントを発行してプロセスを巻き戻します。
-
決済サービス
が「PaymentFailed
(決済失敗)」イベントを発行します。 -
在庫サービス
がこのイベントを購読し、補償トランザクションを実行します。つまり、仮押さえしていた部屋を解放(ステータスを「空き」に戻す)し、「RoomReleased
(部屋解放済み)」イベントを発行します。 -
予約サービス
が「PaymentFailed
」イベントを購読し、予約ステータスを「失敗」に更新します。
このように、Sagaパターンはビジネスプロセス全体を「最終的に」一貫性のある状態に導きます(結果整合性)。これは厳密なACIDトランザクションとは異なりますが、分散システムにおいて可用性と独立性を保つための、現実的で強力な解決策なのです。
4. イベント駆動の罠:「コマンド」と「イベント」の混同
イベント駆動の力を正しく引き出すためには、そのメッセージが何を意図しているのかを明確に区別する必要があります。
罠:「コマンド」と「イベント」の混同
サービス間の連携をAmazon MSK (Kafka), Kinesis Data Streams, EventBridge, SNSといったメッセージング基盤で行う際に、トピックやストリームの設計を間違えるケースが非常に多いです。
※以降はMSKを例にトピック(イベントが発行される場所)やコンシューマーグループという言葉を用います。
悪い例: order-stock
という名前のトピックを作る
この名前は、「order
サービスからstock
サービスへ送るための専用レーン」という意味を持ちます。これは一見分かりやすそうですが、実は「コマンド」の発想です。「受注があったから、在庫を処理しろ」という命令を送っているのです。
-
何が問題か?:
-
密結合:
受注サービス
は、在庫サービス
の存在と役割を知ってしまっています。 -
拡張性の欠如: もし将来、「受注したらメールも送りたい」となった場合、
受注サービス
のコードを修正して、新しいorder-mail
トピックにもメッセージを送る必要が出てきます。新しい機能を追加するたびに、発行元のサービスまで修正が必要になるのです。
-
密結合:
正しい考え方:「事実」を宣言する
イベント駆動の核心は、「過去に起きた事実」を宣言することです。誰かに命令するのではなく、公共の広場(トピック)で「こんなことがありました!」とアナウンスするイメージです。
良い例: orders.events
という名前のトピックを作る
このトピックは「受注ドメインで起きた出来事」を扱う場所です。受注サービス
は、ここに「受注が完了しました(OrderPlaced
)」という事実を書き込むだけです。
そのアナウンスを誰が聞いているか、聞いた人が何をするかについて、受注サービス
は一切関知しません。
5. イベント駆動を支える仕組み
この「事実の宣言」モデルを支えるのが、メッセージング基盤と、そこで使われる重要な概念です。
コンセプト | 例 (Kafka/Kinesis) | 例 (AWSサーバーレス) | 役割 |
---|---|---|---|
広場 | トピック / ストリーム | SNSトピック / EventBridgeイベントバス | イベントが発行される場所 |
聴衆のグループ | コンシューマーグループ / KCLアプリ | SQSキュー | 同じ目的を持つ聴衆の集まり |
シナリオ:受注イベントを在庫とメールサービスが処理する
-
イベントの発行:
受注サービス
は、orders.events
トピックに「OrderPlaced
」というイベントを発行します。 -
イベントの購読:
-
在庫サービス: 自分の責務(在庫管理)を果たすため、
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