概要
Webアプリケーションを開発しているとしばしば、APIサーバ側がクライアントのことを考えたレスポンスを返すことを意識した結果、いろいろと辛い状態になることがある。このような状態をいかに解消していくかを考察していく。
本稿で想定するWebアプリケーション
サーバサイドはWeb API (大抵はHTTP, JSON)であり、iOSアプリ、Androidアプリ、ブラウザアプリ(Single Page Application=SPA)があるような、一般的なWebアプリケーションを考える。
開発の流れは、まず仕様が提案され、それをもとに、iOS/Android/Browserでの画面仕様が作られる。それを見ながら、アプリのエンジニアとサーバサイドのエンジニアが打ち合わせ、APIのエンドポイント・リクエスト・レスポンスの型を定め、その後お互いに開発を開始する。
Smart UI アンチパターン
Back-end APIを作る際に、画面仕様に密接に紐づくAPIを作ってしまうことがある。
例えば、この画面仕様を元にAPIの形を決めるときに、APIを画面に必要なデータと1対1 (あるいはそれに近い形) に作ってしまうことがある。
このような状態を、本稿ではSmart UIパターンと呼ぶことにする。
Smart UIの例1
概要
画面仕様を確認すると、投稿の一覧画面と詳細画面がある。
ここで、一覧画面はid, titleのキーが必要であり、 詳細画面ではid, title, textが必要であった。
したがって、APIを以下のように定義した。
エンドポイント | レスポンスの型 |
---|---|
/posts |
[{ id: Number, title: String }] |
/posts/:id |
[{ id: Number, title: String, text: String }] |
ポイントは、一覧画面ではtext
のカラムを外していること。
なぜそうするか
3つのキーのうち text のデータ量が明らかに大きいことが想定されるので、不要であれば一覧でのレスポンスからは削りたいから。
起こりうる問題点
/posts
から textを削ってしまったことで、APIとしての汎用性が失われている。例えば、
- 一覧画面で一発でデータを取得して、詳細画面に遷移する際は通信を発生させない、というほうがUX的に有利になったため、後で変えたくなった。
- 別の画面からは、1画面でtext含めて全て出すことになった。
のようなことが往々にして起こり、結局一覧画面にも text
を追加するように変更せざるを得なくなる。
※この場合、APIの変更はキーの追加だけであるから、破壊的ではない変更で修正することはできるが。
Smart UIの例2
次に、もう一つありがちなパターンについて考察する。
概要
「マイページ」の画面では、自分個人の情報、自分の最近の投稿、注目の投稿、・・・など様々な情報を細切れに表示する。このように、自分に関する情報を全て盛り込んだAPIを用意する。
なぜそうするか
各種情報のAPIを別々にクライアントに叩かせるのは、クライアントが大変だし、パフォーマンスが下がりそうだから。
起こりうる問題点
-
メンテナンスが大変。フロントエンドの都合でAPIを変更しなければならなくなる。
-
このような万能な感じのAPIがあちこちで使いまわされてしまい、パフォーマンスの問題が起きたりする。
-
クライアント側が逆に大変になることがある。(要出典)
例における問題点を考察する
そもそも、「どの画面に何のデータが必要か」というのは、Front-endの問題であり、その問題についてBack-end APIが考えすぎている (Smart UI) ことで問題が発生している。
そもそも、なぜSmart UIなAPIを用意しているかといえば、
- パフォーマンスの問題を引き起こさない。
- クライアントに楽なようにする。
の2点だった。
ひとまずBack-end APIから、画面の観念を捨てて、次のような状態に戻して考える。
-
例1 ... 一覧画面
posts
にも textのカラムをつける[{ id: Number, title: String, text: String }]
-
例2 ... 「マイページ」のAPIを用意しない
- クライアントに別々のAPIを叩かせる
すると、次の問題が起きる。
- パフォーマンス。
- クライアントがAPI呼び分けるのが大変。
まずはパフォーマンスの問題を分解する。
前提としては、パフォーマンスチューニングの基本は「推測するな、計測せよ」である。むやみなチューニングは開発効率を低下させることになるので、問題を分解し、実際には計測を行い、必要なチューニングを施すべきである。
今回のパフォーマンス問題を分解すると、次の3つになる。
- サーバ側で、余計なカラムをDBから取得してしまう (例1, 例2)
- クライアント-サーバ間の通信量 (例1, 例2)
- 画面に必要でないカラムも取得してしまう。
- クライアント-サーバ間のリクエスト回数 (例2)
これに 4. クライアントがAPI呼び分けるのが大変 も加えた4つの問題について、解決策を考えていく。
解決策1: Front-end Server
Front-end Serverの層を用意する。このサーバは、クライアントに直接提供するAPIを用意する。そして、1つのリクエストでBack-end Serverと複数回、あるいは複数のBack-end Serverと通信し、そのデータを統合して画面に使いやすい形で返す。
Front-end Serverは、アーキテクチャ上の位置はフロントエンドでありUIに近いが、物理的な位置はサーバである。
この層を設けることにより、次の問題が解消される。
2. クライアント-サーバ間の通信量
Front-end Serverで絞れるので、解決できる。
ちなみに、例えばGraphQLを利用すると、クライアント側からの指定で、必要なBack-end APIの必要なカラムだけを取得することが可能になる。
3. クライアント-サーバ間のリクエスト回数
Front-end Serverへの通信1回でよくなる。
その分サーバ間の通信は増えるが、これは物理的(ネットワーク的)に近ければ大きな問題にならない。
4. クライアントがAPI呼び分けるのが大変
サーバに位置するので「クライアントが大変」はなくなる。
ただ、アーキテクチャ上はフロントエンドなので、フロントエンドエンジニアの仕事としては変わらないかもしれない。
考え方としては、Back-endで行っていたものを、本来やるべきであったFront-endが担当するようになった、とも言える。
この時、Back-end APIが提供している内容がきちんと分かりやすく見えるようになっていないと、フロントエンジニアが大変になってしまう。その点は別途重要な問題として残るが、これは後の「展望」の項目で述べる。
また、元の状態では、例えばAndroidとiOSで同じページ内容のときにも、AndroidとiOSのクライアントそれぞれで同じような呼びわけロジックを書かなければならなかったかもしれない。この問題は、Front-end ServerのAPIを1つ用意しAndroidとiOS両方がそれを使うことで、解決できる。
解決策2: Back-end APIが、必要なカラムを受け取れるようにする
Back-end APIのパラメータで、どのカラムが欲しいかを指定できるようにする。
これにより、サーバ側で、余計なカラムもDBから取得してしまうことを解決する。
例えば例1の /posts
のAPIであれば、 { keys: ['id', 'title'] }
のようなパラメータを受け取れるようにしておけば、SQL上も SELECT * FROM posts;
ではなく SELECT id, title FROM posts;
と、絞って取得することができるようになる。
上記の例なら単純だが、実際には他のテーブルを見に行ったりなどがあり、必ずしも簡単に実装できるわけではない。したがって、パフォーマンス上問題が起きた時に対応する、という方針が良いように思う。
Front-end Server => Back-end Serverのリクエストでは、常に _keys のように固定で送っておき、パフォーマンス上必要になった時にBack-endが対応する、というのが良いかも。
※Front-end Serverでは、Back-end Serverのレスポンスのキーをフィルタリングしているから、必要なキーの情報は持っているので、そのまま使えるから。(それがFront-end Server上に固定で実装されている場合と、GraphQLを利用する時のようにクライアントから動的に渡される時がある)
展望: Back-end Serverは、よりRESTfulを意識し、Back-endとしての役割に集中する
Smart UIアンチパターンを排除することで、Back-endはよりBack-endとしての役割に集中できる。
このときに、きちんとEntityを明示し、JSON Schemaなどを利用して縛っていくのが重要であるように思う。これにより、レスポンスのブレがなくなり、レスポンスの型を明示するのが可能になる。したがって、上で述べた、
Back-end APIが提供している内容がきちんと分かりやすく見えるようになっていないと、フロントエンジニアが大変になってしまう。
という問題も解決することができる。
展望: Domain-Driven Design, Microservices
ここまでの内容は、Smart UIを排除し、Domain-Driven Designの考え方を取り入れており、それをMicroservicesとして表現している、ということができる。
この場合、Back-end Serverを適切な単位でMicroservices化していくことが良い方針の一つと言えるだろう。