TT Lab
はじめる
学ぶ 学習パス コース

統合とデプロイ

フィールド名が変わったのに例外は出なかった

TT Labで続きを見る

一言でいうと

他人のシステムとつなぐとき、こちらが守るべきものは、相手の全体のスキーマではなく、こちらが実際に読むフィールドの一覧であり、その一覧をドキュメントではなく、毎回動くテストとして固定しておいて初めて、バージョンアップが黙ってこちらの数字を間違わせることがなくなります。

なぜ必要なのか

統合で最も高くつく事故は、接続が切れる事故ではありません。接続が切れればアラートが鳴って、人が駆けつけます。本当に高くつくのは、何のエラーもなく、間違った数字が出る事故です。

パートナーが注文APIを1.3から1.4に上げたとしましょう。変わったのは3つです。regionがmarketに名前を変え、amountが整数12300から文字列"12300.00"に変わり、statusにon_holdという値が1つ増えました。こちらの収集プログラムはどうなるでしょうか。

3つとも、アラートが鳴りません。数週間後に、顧客が「数字が少しおかしいのですが」と言うとき、すでに間違ったレポートは何枚も出たあとです。

どう動くのか

この問題を扱う方法は、3つです。

1つ目は、何が壊すのかを知ること。コンシューマー(応答を読む側)の視点で変更を分けると、ルールは単純です。フィールドが追加されることは安全で、消える、名前が変わる、型が変わることは安全ではありません。列挙値が増えることも安全ではありません。こちらの分岐にない値が来ると、どの分岐にも行かないからです。方向が逆であるリクエスト側は、ルールも逆です。リクエストに任意のフィールドが増えることは安全ですが、必須のフィールドが増えることは、こちらのリクエストが拒否されるので、壊します。

ここで基準になるのが、「知らないフィールドをどうするか」です。JSON Schema 2020-12のコア仕様は、additionalPropertiesでその扱いを決め、requiredは、必ずあるべきキーの一覧です。コンシューマーの契約を書くときは、たいてい知らないフィールドを許容します。そうすれば、パートナーがフィールドを追加するたびに、こちらが壊れることがありません。

2つ目は、契約をこちら側で書くこと。パートナーのOpenAPI仕様をそのままこちらの契約にしたくなる誘惑がありますが、そうすると、こちらが読まないフィールドの変更にも、こちらのテストが赤信号を出します。ノイズが増えると、人はテストを切ります。そのため、契約にはこちらが実際に読むフィールドだけを書きます。この方式を、一般にコンシューマー駆動契約(consumer-driven contract)と呼びます。

3つ目は、契約をドキュメントではなくテストとして置くこと。パートナーの応答を受け取って契約と突き合わせ、ずれていたら0以外のコードで終了するスクリプト1つで済みます。それをパイプラインに入れれば、パートナーが通知なしに動いても、こちらが先に気づけます。バージョンアップの通知メールを見逃したというのは、事故報告書に最もよく書かれる文です。

파트너 응답 ──▶ 계약 대조기 ──▶ 위반 0 ? 통과
                    │
                    └─ 위반 n ? 파이프라인 실패 + 무엇이 어긋났는지 필드 단위로 출력

このコードブロックの韓国語は、パートナーの応答→契約照合器→違反0なら通過、違反nならパイプラインを失敗させて、何がずれたかをフィールド単位で出力する、という流れです。

バージョン表記そのものも約束です。セマンティックバージョニング(Semantic Versioning)は、互換を壊す変更にはメジャー番号を上げるように定めています。ただし、それは発行者の約束なので、パートナーが守らないこともあります。1.3から1.4に上がったのにこちらが壊れたなら、それはこちらが誤って読んだのではなく、相手が約束を破ったのです。ただ、その事実を証明するには、契約テストの出力が必要です。

現場での姿

1つ目、バージョンアップは一度には来ません。旧バージョンと新バージョンが数か月ずつ一緒に動きます。そのため、読む側は両方を受け入れる必要があります。名前が変わった箇所は別名表で、型が変わった箇所は正規化関数で吸収し、内部では1種類の形だけを使います。別名表をコードのあちこちに散らしておくと、3か月後に旧バージョンを切るときに、どこを消せばよいのか誰にもわかりません。

2つ目、「必須なのに、ときどき空」が最も多いです。仕様には必須と書かれているのに、実際の応答の3%は空文字列です。そのため、契約テストは、仕様を読むのではなく、実際の応答を、サンプルではなく全件で調べる必要があります。

3つ目、金額と時刻が常に問題です。整数の最小単位から十進文字列へ、またはその逆へ変わることが多いです。浮動小数で受け取った瞬間に丸めが生じるので、正規化は整数か文字列で行い、実数を経由しません。

4つ目、契約テストをどこで動かすかが、実際の論点です。パートナーの本番環境に対して、毎分動かすことはできません。たいていは、相手が提供するサンドボックスに対して、1日に数回、そしてデプロイの直前に1回動かします。サンドボックスと本番が、異なるバージョンを動かしている場合があるので、契約テストの出力に、どのアドレスに対して測ったかを必ず残します。

次のラボですること

パートナーの注文APIの1.3と1.4を一緒に提供するサーバーを起動して、2つのバージョンの応答を手元に揃えます。フィールドの差を機械が読める形で取り出し、変更の種類ごとに、何がこちらを壊すのかを判定する小さなツールを作ります。そのあと、こちらが読むフィールドだけを書いたコンシューマー契約と、その契約を照合する照合器を作り、旧バージョンと新バージョンの両方を受け入れる読み取り層を付けます。最後に、パートナーが通知なしにまた動いた状況を作り、契約テストがそれをフィールド単位で検出するかを確認します。