API変更がユーザーの知らない依存関係を壊すとき
バックエンドサービスの新バージョンをデプロイした。パイプラインはグリーン。ログもきれい。ところが5分後、チームチャットが騒ぎ始める。モバイルアプリが空画面を返す、Webフロントエンドにデータが表示されない、別チームのサービスが全リクエストで500エラーを吐いている。
何が起きたのか?APIレスポンスのフィールド名をnamaからfull_nameに変更しただけだ。無害に見えた。しかしモバイルアプリはそのフィールド名でパースしており、Webフロントエンドは直接表示しており、別チームのサービスはデータベースのカラムにマッピングしていた。彼らは誰も変更を知らされていなかった。なぜなら、あなたが彼らの存在を知らなかったからだ。
これがAPIを維持する現実だ。自チームだけが触れるバックグラウンドワーカーやスケジュールジョブとは違い、APIにはあなたが知らない消費者がいる。昨年リリースされたモバイルアプリ。別部門が設定したパートナー連携。次のアプリストア審査まで更新できないフロントエンド。APIを変更するとき、あなたは自分のコードを変えているだけではない。他のシステムが依存する契約を変えているのだ。
見えない問題
核心は可視性だ。大規模組織では、単一のAPIエンドポイントを何十ものチームが利用しているかもしれない。たとえAPIが一つのアプリケーションだけにサービスを提供していても、そのアプリケーションはすでにユーザーの手に渡っており、すぐには更新できない。全コンシューマーに同時リリースを調整することは不可能だ。中には存在すら知らないものもいる。
ここで後方互換性が理論上の理想ではなく、実用的な必要性となる。後方互換性とは、新しいバージョンのAPIが古いバージョンと同じ形式でリクエストを処理できることを意味する。/usersエンドポイントが以前namaというフィールドを返していたなら、新しいバージョンは移行期間なしに突然full_nameにリネームできない。pageパラメータが整数を受け付けていたなら、新しいバージョンは突然文字列を要求できない。これらの変更は契約を破り、壊れた契約は壊れたコンシューマーを意味する。
破壊的変更とは何か
破壊的変更には多くの形態があり、明白なものもあればそうでないものもある。エンドポイントの削除は明らかに破壊的だ。フィールドのリネームも破壊的。データ型の変更も破壊的。リクエストボディへの必須フィールド追加も破壊的。エラーレスポンスの形式変更も破壊的。
しかし、もっと微妙なものもある。JSONレスポンスのフィールド順序変更は、配列インデックスに依存するコードを壊す。新しい必須ヘッダーの追加は、それを送信しないクライアントを壊す。特定のエラー条件に対するHTTPステータスコードの変更でさえ、正確なステータス値をチェックするロジックを壊す可能性がある。
次の簡単な例を考えてみよう。あなたのAPIは以前、次のようなユーザーオブジェクトを返していた:
{
"id": 42,
"nama": "Ani Wijaya",
"email": "ani@example.com"
}
「整理」のためのリネーム後、次のように返す:
{
"id": 42,
"full_name": "Ani Wijaya",
"email": "ani@example.com"
}
あなたにとってはより良い名前だ。しかしresponse["nama"]をパースするモバイルアプリにとってはundefinedだ。user.namaを表示するフロントエンドにとっては空白だ。namaをデータベースカラムにマッピングするパートナーサービスにとっては、静かなnullだ。フィールドは精神的には存在するが、契約は壊れている。
厄介なのは、これらの変更がサーバー側からは無害に見えることだ。単に整理しているだけ、機能を追加しているだけ、命名の不整合を修正しているだけ。しかしコンシューマーから見れば、彼らが求めてもいない、コード変更なしには適応できない方法で振る舞いが変わったのだ。
本番環境に到達する前に破壊的変更を検出する
最も安全なアプローチは、CIパイプラインで破壊的変更を自動検出することだ。手動レビューや誰かがチェックを覚えていることに頼るべきではない。この目的に特化したツールが存在する。
OpenAPIのようなAPI仕様形式を使っているなら、OpenAPI DiffやSpectralのようなツールが新旧の仕様を比較できる。何が変わったか、それが破壊的変更かどうかを正確に報告する。FastAPI、SpringDoc、ASP.NET Core with Swashbuckleのようにフレームワークが自動的に仕様を生成する場合、すべてのプルリクエストでこの比較を実行できる。
ワークフローは次のようになる:開発者がプルリクエストを開くと、CIパイプラインが新しいAPI仕様をビルドし、メインブランチの仕様と比較し、破壊的変更を報告する。変更がなければPRは通常通り進む。破壊的変更があれば、チームはその変更が本当に必要か、段階的に実施できるかを議論できる。
これにより、「これで何も壊れませんように」から「何が変わったか、契約を破るかどうかを正確に示す」へと会話が変わる。目に見えないリスクを、可視的な決定ポイントに変えるのだ。
破壊的変更を避けられない場合
すべての破壊的変更を回避できるわけではない。ビジネス要件が再設計を強制することもある。アーキテクチャを進化させる必要があることもある。古い設計が単純に間違っていて置き換えが必要なこともある。
破壊的変更をしなければならない場合、APIバージョニングが標準的なアプローチだ。考え方は単純:複数のAPIバージョンを同時に維持する。古いコンシューマーは古いバージョンを使い続け、新しいコンシューマーは新しいバージョンを採用する。全員に移行の時間を与える。
バージョニングの実装方法はいくつかあり、それぞれにトレードオフがある。
URLバージョニングが最も一般的だ。パスにバージョンを入れる:/v1/usersと/v2/users。理解しやすく、ルーティングしやすく、テストしやすい。欠点はURLが長くなり、バージョンごとに別々のコードパスを維持することだ。
ヘッダーバージョニングはURLをクリーンに保つが、コンシューマーがカスタムヘッダーを設定する必要がある(例:Accept: application/vnd.myapi.v1+json)。理論上はよりRESTfulだが、ヘッダーを正しく設定する必要があるコンシューマーにとって複雑さが増す。
クエリパラメータバージョニングは?version=1のように使う。単純だが、クエリ文字列を乱雑にし、適切なキャッシュが難しくなるためあまり一般的ではない。
どの方法を選んでも、バージョニングは無料ではない。維持するバージョンごとに、実行、テスト、デバッグ、そして最終的に非推奨にするコードがある。古いバージョンをどのくらいサポートするかのポリシーが必要だ。6ヶ月が一般的。1年は寛大だ。何を選んでも、明確に、かつ十分な余裕をもって伝える。消費者に移行期間を与え、突然の停止は避ける。
より良い道:進化を前提に設計する
バージョニングはフォールバックであり、戦略ではない。より良いアプローチは、新しいバージョンを必要とせずに進化できるようにAPIを設計することだ。これは、何をどのように公開するかを意図的に決めることを意味する。
消費者が不要なフィールドは返さない。レスポンスに追加するすべてのフィールドは、誰かが依存する可能性のあるフィールドだ。フィールドが有用かどうか確信がなければ、省略する。後で追加することはいつでもできるが、削除は破壊的変更だ。
可能な限り柔軟なデータ型を使う。パラメータが合理的にどちらでもあり得る場合、文字列と数値の両方を受け付ける。レスポンスに余分なフィールドを含めても、消費者がそれを使うことを要求しない。原則は単純:受け入れるときは寛大に、約束するときは保守的に。
新しい機能を追加する必要がある場合、既存のものを変更せず、新しいフィールドや新しいエンドポイントを追加する。レスポンスの新しいフィールドは、古い消費者が単に無視するため壊さない。新しいエンドポイントは、古い消費者が決して呼び出さないため壊さない。既存のものを削除したりリネームしたりしない限り、バージョンアップなしで進化を続けられる。
API変更の実践的チェックリスト
プルリクエストをマージする前に、次のチェックを実行する:
- 既存のフィールド、パラメータ、エンドポイントを削除またはリネームしていないか?
- 既存のフィールドやパラメータのデータ型を変更していないか?
- リクエストボディに必須フィールドを追加していないか?
- エラーレスポンスの形式を変更していないか?
- 既存のシナリオに対するHTTPステータスコードを変更していないか?
- 以前のAPI仕様との自動差分を実行したか?
最初の5つの質問のいずれかに「はい」と答えた場合、それは破壊的変更だ。延期するか、バージョン管理するか、既知の全コンシューマーに明確に伝えるかを決定する。
まとめ
あなたのAPIは契約だ。それに依存するすべてのコンシューマーは、今日の振る舞いに基づいて暗黙の合意をしている。警告なしにその契約を変更することは、技術的なミスではない。チーム間の信頼を損なう協調の失敗だ。
目標は決して破壊的変更を行わないことではない。目標は、自分が破壊的変更を行っていることを認識し、それが価値あるかどうかを意識的に判断し、コンシューマーに前進の道を提供することだ。検出を自動化し、変更を伝え、最初から進化を前提に設計する。コンシューマーが後方互換性の維持に感謝することは決してないが、それが欠けているときには必ず気づく。