前置き
今回は、以下の記事のリトライの内容をより深ぼった内容になります。
おとぎ話サーガや、タイムトラベルサーガでは、アプリケーションコードとして、
一時的にリトライ系の処理を実装する必要があります。
この影響によって、おとぎ話サーガの初期の段階では、
サービス同士が互いにアプリケーション層で、結合した状態にならざるを得ません。
下図のように、ビジネスロジック(クリーンアーキテクチャでいうエンティティ層)やユースケース層では独立。
今回の論点
なぜ最初からインフラリトライ系の処理をインフラコードとして表現しないのか?
疑問に感じませんか?
そこで今回の論点は、
なぜ、インフラ系の処理を最初からインフラ層に実装せずに、
図の緑部分のInterface Adapters層に実装する必要があるのか?
ということにします。
クリーンアーキテクチャにおけるサービス間通信
本題に入る前に、まずサービス量子内の構造について、整理しておく必要があります。
その際に、クリーンアーキテクチャの話が避けては通れません。
クリーンアーキテクチャの最も重要なルールは
「依存関係は常に外側から内側に向かう」
です。内側の円は、外側の円(具体的な実装)について何も知りません。
そして、オーケストレーターが各種マイクロサービスを呼び出す際の、正しいステップは以下のようになります。
1. オーケストレーターのユースケース層
役割
ビジネスロジックの実行を決定する。
アクション
自身の内側にあるユースケースが、「マイクロサービスAの機能が必要だ」と判断します。
しかし、ユースケースは外部通信の方法(それがHTTPなのかgRPCなのか)を知りません。
代わりに、自身が依存するインターフェース(抽象)を呼び出します。
例えば、i_microservice_a_gateway.execute_action(data)
2. オーケストレーターのゲートウェイ層 (Interface Adapters)
役割
外部システムとの通信という具体的な実装を担当する。
アクション
上のステップ1で呼び出されたインターフェースを実装した具象クラスがここに存在します。
このクラスが、HTTPクライアントを使ってマイクロサービスのAPIエンドポイントを呼び出すという、具体的なネットワーク通信処理とリトライ処理を実装します。
3. マイクロサービスのコントローラー層 (Interface Adapters)
役割
外部からのリクエストを受け付ける窓口。
アクション
オーケストレーターのゲートウェイから送られてきたHTTPリクエストを受け取ります。
リクエストの正当性を検証し、データを内部のユースケース層が理解できる形式に変換して、内側のユースケースを呼び出します。
4. マイクロサービスAのユースケース層
役割
自身のビジネスロジックを実行する。
アクション
コントローラーから渡されたデータをもとに、自身の責務であるビジネスロジックを実行します。
このユースケースは、自分が誰から(オーケストレーターから?それともユーザーから直接?)呼び出されたのかを知る必要はありません。
なぜこの分離が重要か?
この厳格な分離により、以下のメリットが生まれます。
強制ではないものの、正直個人的にはマイクロサービスするんなら、
最低限クリーンアーキテクチャの構造は、共通言語になっててよとは思います。
その理由は、以下を読んでもらえればすぐわかると思います。
ビジネスロジックの保護
オーケストレーターのコアなビジネスロジック(ユースケース層)は、ネットワークエラーやリトライといった技術的な複雑さから完全に隔離されます。
これにより、ビジネスロジックはシンプルでテストしやすくなります。
技術選択の自由
呼び出される側のマイクロサービスがREST APIからgRPCに変わっても、オーケストレーター側で修正が必要なのはゲートウェイ層だけです。
オーケストレーターのユースケース層のコードは一切変更する必要がありません。
したがって、結合するのはあくまでもシステムの「境界」を担うアダプター層同士であり、中核であるユースケース層は、それぞれのシステム内で保護されたままとなります。
2種類のリトライ:責務の分離
リトライには大きく分けて2種類あり、それぞれ責任を持つべきレイヤーが異なります。
1. インフラ層が担うべきリトライ infrastructural
対象
一過性のネットワークエラーなど、冪等(べきとう)に再実行しても安全な技術的な問題。
事例
HTTP 503 Service Unavailable、接続タイムアウト。
メカニズム
サービスメッシュ(Istioなど)やAPIゲートウェイが、リクエストの内容を解釈せず、機械的に再試行します。
アナロジー
郵便配達員が手紙を届けに行った際、相手が一時的に留守だったため、10分後にもう一度訪ねるようなもの。配達員は手紙の中身(ビジネスロジック)には関知しません。
ここで中身がもろ見えだと、セキュリティ上の観点で超危険です。
誰にでも、その秘匿情報が見えてしまっているのですから。
2. アプリケーション層が担うべきリトライ 📝
対象
ビジネス上の状態に依存する、より複雑なエラー。
インフラ層は、このリトライをすべきか、すべきでないかを判断できません。
事例
例①
「在庫の棚卸しのため、現在在庫がロックされています。5分後に再試行してください。」
例②
「決済プロバイダーのレート制限に達しました。指数関数的バックオフで再試行してください。」
例③
「依存サービスのデータ整合性が遅延しています。完了通知を待ってから再試行してください。」
メカニズム
アプリケーションコードがエラーの内容を解釈し、「今リトライしても無駄か?」
「どのくらい待ってからリトライすべきか?」といったビジネス判断を下します。
アナロジー
手紙の中身が「1万円を請求します」という内容だったとします。
相手が「今9千円しかないので、1時間後の給料日以降にまた来てください」と返答したとします。
この判断は、手紙の中身を知らない郵便配達員にはできず、請求者(アプリケーション)が次のアクションを決めるしかありません。
なぜ最初からインフラで実装しないのか?
では、なぜインフラ層の関心として最初から、リトライ系の処理として実装し、
おとぎ話サーガは、インフラ層でのみ他サービスと同期通信で蜜結合状態を目指せないのでしょうか?
1. 探索フェーズとしての「おとぎ話サーガ」
「おとぎ話サーガ」は、サービス境界を発見し、サービス間の真の依存関係と失敗パターンを学習するための探索的な段階です。
この時点では、
・どのようなエラーが起きるか
・それがインフラ層で機械的にリトライできるものなのか
・アプリケーション層での判断が必要なものなのか
がまだ明確ではありません。
アプリケーションコード内であれば、開発チームはこれらの複雑なリトライロジックを迅速に実装・実験・修正できます。
なぜなら、この段階で必要なリトライ処理には、
インフラ層が判断できない
『ビジネスロジック固有の条件』 が多く含まれているからです。
なので、まずはアプリケーション層でその複雑な要件を探索・検証する必要があります。
2. 時期尚早な共通化の回避 (YAGNI)
学習の不十分な段階で、最初から全てのパターンを網羅する完璧なリトライ機構をインフラ層に作ろうとすると、
過剰に複雑で、実際には使われない機能を作ってしまうリスクがあります。
(時期尚早な共通化)
そこで、まずは各アプリケーションチームが必要なリトライ処理を個別に実装し、その中で共通して使えるパターン(例:特定のエラーコードに対する指数関数的バックオフなど)が見えてきた段階で、初めてそれをライブラリやインフラ層の機能として抽出し、インフラ層に移動するのが、無駄のない進化の順序です。
つまり、一時的にアプリケーションコードにリトライ処理を実装するのは、未知の領域を探索するための、最も手早く安全な学習方法です。
つづき
この次の記事で、ストラングラーパターンを用いて、段階的にインフラ層に責務を委譲していく手法をフレームワーク化したものとして記述していきます。

