はじめに
こんにちは、サーバーサイドエンジニアの中川(@tkdev0728)です。
本記事を書いているのが2月なのですが、絶賛足先が冷える点に悩んでいます。冷え性改善のおすすめグッズがあればぜひコメントにて教えてくださいw
さて、今回はタイトル通りヤプリで数多くあるAPIの中で一番重要と言っても過言ではないとあるAPIを再構築しました。リリースまで色々ありましたが多くの学びがあったので書いていこうと思います。
今絶賛APIのリプレイスを行なっている人、APIのリプレイスを検討している人の参考になれば幸いです。
リプレイスの背景
私が所属しているチームではよりモダンなUIを実現してアプリ表現を拡張することを目的として日々の業務にあたっています。
いくつかあるやりたいことに対して優先度を決めて順番に対応していたのですが、今回はリリース当初から手付かずだった部分に対して改修を行うことになりました。
その部分の値を返しているAPIはサービス開始直後からあるAPIで、10年以上の歴史があります。少しずつ改修を重ねていたのもあり、10年経って色々と辛い部分が出てきました。
- 歴史的経緯でレスポンス内容は Atom Syndication Format を擬似的にjsonに置き換えたものになっており、キー名から値が想像しづらい
- PHPの独自フレームワークで動いていて処理が関数化、クラス化されておらず改修しづらい
- 最終的なレスポンスをテストするE2Eテストはあるが、関数化されていない関係でロジックごとの単体テストが書きづらい
- アプリごとの設定やレイアウト情報など起動に必要な情報を取得しているので責務が肥大化している
上記の課題を抱えつつ10年以上やってきたのですが、今回の改修を皮切りにどんどん同じ部分の表現拡張を行なっていく方針なのを考えると、どうしても機動力が落ちてしまうところが懸念でした。
そこで、今回の改修をきっかけにAPIを再構築することで今後の機能拡張をより早く、より安全に行えるようにすることを目的として機能拡張と並行してAPIもリプレイスすることになりました。
リプレイス対象のAPIのレスポンス例
{ "feed": { "category": [ { "_term": "#ffffff", "_scheme": "backgroundColor" } ], "id": "sample", "title": "sample", "updated": "2025-02-12T18:00:00+09:00", "entry": [ { "content": { "_type": "image/png", "_src": "hoge.png" }, "id": "sample", "link": [ { "_href": "https://example.com", "_type": "application/json" } ], "summary": "sample", "title": "sample", "updated": "2025-02-12T18:00:00+09:00" } ] } }

今回リプレイス対象のAPIはアプリのヘッダ部やフッタ部のカラーコードや、機能一覧などを返しています。(画像赤枠部参照)
リプレイス方法
今後の機能拡張をよりやりやすくするためにAPIをリプレイスすることは決定しましたが、どういうAPIにするかなど具体的な部分は全く決まっていませんでした。
そこで主に以下のようにして具体的な部分を決めていきました。
設計
既存のAPIを読み解く
何はともあれこれがないと始まりません。
とはいえただ読み解くだけでは辛いので、成果物としてシーケンス図を作成することを目的として読み解いていました。ここで作成したシーケンス図は詰まったときの思考の整理に使えましたし、後述する仕様の共有時にも資料として展開できたので作って良かったです。
APIの責務を決める
アプリごとの設定やレイアウト情報など起動に必要な情報を取得しているので責務が肥大化している
出来上がったシーケンス図を見ていて、いくつかある既存APIの辛い部分の中で自分が一番課題だと思う部分が上記でした。
責務が大きいと影響範囲も大きくなり、影響範囲が大きいと改修難易度が高くなるので今後の機能拡張の妨げになると思ったからです。
そこでまず最初は細かく責務ごとにAPIを分割して1つ1つのAPIをシンプルにすることでわかりやすくできないかと考え設計してアプリ側の実装者に共有しました。すると、アプリ側の実装者から以下のフィードバックをいただきました。
- UXを考慮して起動時に叩くAPIはできるだけ少なくしたい
- 今後の新しい機能改修時に内容によってはエンドポイントを増やさずに追加できる仕組みが欲しい
サーバー側視点ではAPIを分割して1つ1つをシンプルにすればわかりやすくなると思っていましたが、アプリ側の視点ではまた別の思いがあるというのを知れたいい学びでした。
個人的にはアプリやブラウザなどユーザーに情報を表示する部分はできる限りシンプルにしてそれに伴う複雑さはバックエンド側で吸収するのが理想だと思っていたので、アプリチームの思いを汲み取って再設計することにしました。
APIのコール数を増やさないためにはどこかに複雑さは残るのですが、機能拡張に伴って徐々に複雑になるのは避けたく、ある1点に複雑さを押し込められないかと思って最終的に実装するAPIは以下の2つになるように設計しました。
アプリ起動時の設定取得用の汎用的なAPI
レイアウト情報以外のアプリ起動に必要な設定などを取得する汎用的なAPIです。
今後新しく必要になる設定などはこのAPI内で取得する方針とすることでレイアウト以外の機能拡張に伴う複雑さをこのAPIに押し込めることが狙いでした。内容によってはエンドポイントを増やさずに追加できるようにしたいというアプリチームの思いも汲み取れましたし、既存の起動時に叩かれるAPIのうち、まとめられるものはこのAPIにまとめることで最終的なAPIコール数も変わらずに済みました。
機能やレイアウト情報取得API
アプリごとのレイアウト情報を取得するAPIです。今後アプリ表現の拡張を積極的にやっていく方針だったので機能一覧とレイアウト情報を返すことに専念させることが狙いでした。 今回本来やりたかったアプリ表現の拡張もこのAPIの改修だけで実現できるようになりました。
レスポンス内容を決める
APIの責務が決まったので、既存のレスポンスをどっちのAPIに移行するかを検討しました。 シーケンス図を書いた時にある程度処理内容はわかっていたので、新しいAPIの責務に合わせてどっちのAPIに実装するかを決め、レスポンスとしてまとめました。 ここでまとめたレスポンス内容をAPI仕様書として定義し、アプリチームにも共有して合意を取りました。
決めた内容について、周りに共有して合意をとる
APIの責務が決まってAPI仕様書もできたのでこれで実装が進められる状態にはなりましたが、実装者同士で合意しただけなので全体に対する影響を考慮してこの段階で周りに共有しました。
具体的にはまず開発チーム内で認識を揃えてからCTOに共有して技術的な懸念点についてアドバイスをいただきつつ合意を取り、その後サーバーチーム全体に設計案の共有を行いました。
人によってわかりやすいの定義は違いますし、使いやすいの定義も違います。なので全員の最大公約数を満たせればと思って「これだけはないよね」というポイントがつぶせているかを意識して共有しました。
実装
API仕様書ができ、各所とも合意を得たのでようやく実装に入れます。 既存のAPIはPHPの独自フレームワークで動いていますが、新しめのAPIはGoで実装されています。そのためGoへのリプレイスを試みました。 いきなり全てを移行するのは辛いので、以下の順番で少しずつ移行していきました。
まずintegration testを実装する
ヤプリのサーバーサイドのテストはプロジェクトにもよりますが、以下のテストを実装することが多いです。
- メソッドごとのunit test
- リクエストを受けてDBアクセスし、レスポンスを返すまでの一連の流れをテストするintegration test
条件分岐の境界値など細かいロジックや最終的なレスポンスなど、テストしたい内容によっていずれかのテストを実装します。
今回は先の設計でAPI仕様書が出来上がっていて最終的なレスポンスは決まっていたのでまずintegration testを書いてそれを通すように実装していくTDDで進めていきました。
PHPの独自フレームワークで実装されている内容を読み解いてGoにリプレイスしていく途中で絶対に「今何がやりたかったんだっけ?期待値ってなんだっけ?」と道に迷うことが容易に想像できたからです。
TDDといいつつ厳密には責務ごとに細かくPRを出していたのでマージ時にテストは通す必要がありました。そのため空のテストを作成しておいて実装段階になったらテストの実装からロジックの実装という流れで進めていました。
処理のまとまりごとに細かくPRを出す
いきなりクソデカPRを投げてもレビュアーは困るので、処理のまとまりごとにPRを出すことを心がけていました。 感覚値ですし特にここを意識していたわけではないですが、テストやモック関数を除いて大体500行くらいでPR出してた気がします。
キャッシュ戦略を考える
全てのアプリで起動時に叩かれるAPIなのでキャッシュはほぼ必須でした。 既存のAPIはEC2上で動いているのでFastCGIキャッシュを使っていました。新APIはfargate上で動作しているのでredisを使ってキャッシュさせるようにしました。キャッシュ保持時間は悩みましたが、今回キャッシュさせる一番の目的はプッシュ通知からのアクセスなど瞬間的なリクエスト数増加に対応することです。なので一旦キャッシュ保持時間は1時間に設定しました。ただ、リリース後のアクセス数などを確認して、UX向上のために必要に応じて調整する予定です。
結果
APIをリプレイスしつつ本来やりたかったアプリ表現拡張の要件も満たすことができました。 また、unit test と integration test の両方ありますし、APIも決められた責務ごとにわかれているので今後の機能拡張のしやすさに繋げられたと思っています。
よかったこと
実装していて特に良かった点は細かくPRを出してたおかげでレビューでのフィードバックをすぐ次のPRで活かすことができた点です。
FBを次のPRに活かすことでまた別の視点でのフィードバックを得られ、それをまた次のPRに取り入れるを繰り返したことで品質向上にも繋げられましたし、インプットとアウトプットのサイクルをたくさん繰り返せたことで技術力も向上したのではと思っています。
難しかったこと
一方で、難しかった点や改善したかった点もいくつかあります。
まず1つは機能拡張とリプレイスを両立させたことです。
今回はアプリ表現を拡張するという目的があってそのための手段の1つとしてリプレイスを進めてましたが、本来リプレイスのアンチパターンだと思います。
不具合などを検知した時に機能拡張の影響なのかリプレイスの影響なのか切り分けが大変なので可能な限り避けた方がいいかと思います。
次に、新旧の互換性の担保が難しかったです。クライアント影響は極力ないようにするのは大前提ですが、互換性の担保のために本来シンプルに実装できる部分もあえて遠回りして複雑に実装する必要がありました。
実装後半で思いがけない影響が見つかることもあったので、新旧の互換性をどこまで担保するかは最初に握っておくことが重要だと感じました。
最後に
今回はAPIのリプレイスの進め方について書いてみました!
今リプレイスを行っている人、これからリプレイスを行おうとしている人の参考になれば幸いです。
記事に書いた以外にも色々やっていたりするので、興味のある方はぜひカジュアル面談にお越しください! open.talentio.com