2022年5月7日土曜日

GraphQLがRESTに取って代わるということはありえますか?

https://jp.quora.com/GraphQL%E3%81%8CREST%E3%81%AB%E5%8F%96%E3%81%A3%E3%81%A6%E4%BB%A3%E3%82%8F%E3%82%8B%E3%81%A8%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%81%AF%E3%81%82%E3%82%8A%E3%81%88%E3%81%BE%E3%81%99%E3%81%8B

Hojo Masaki
Roman Scharkov
  1. どちらにも一長一短があるため、GraphQLがRESTに取って代わることはないと思います。
  2. 将来的にGraph APIがAPI開発の新たなスタンダードとなり、RPCスタイルのAPI(RESTful APIも含む)を大きく上回るようになる可能性は否定できません。しかし、必ずしもそれがGraphQLである必要はありません。APIの世界で革命が起こるためには、大きなパラダイムシフトが市場で受け入れられる必要があるでしょう。

1. RPC vs クエリ言語

簡単に言うと、RESTは CRUD形式のRPCです。リソースURLに対してHTTPメソッド(GETPOSTPUTDELETE)でリクエストを行うことは、RPCでリモートプロシージャを呼び出すことに相当します。

RPCはとてもシンプルです。特定の引数を指定してリモート関数を呼び出すだけで、データが返ってきます。実に簡単ですね!

GraphQLがクエリ言語(Query Language)であるということは一目瞭然でしょう。RPCとの主な違いは、どのようなデータが返されるかをクライアント側で管理できることにあります。サーバー側がクエリ実行エンジン(query execution engine)になるため、その分実装がはるかに困難になります。

どちらのアプローチにも長所と短所があります。

  • RPC API
    • オーバーフェッチアンダーフェッチの問題がある。
    • 時間の経過とともに、エンドポイントのメンテナンスが難しくなりがちである。
    • RESTful系のAPIには適切な型システムがない。
  • Graph API
    • 複雑過ぎるクエリに対して何らかの対策を練る必要がある。
    • キャッシュの実装が難しい(クライアントサイドのキャッシュしかない)。
    • 正しく実装しようとすると途端にハードルが高くなる。

2. オーバーフェッチ問題

オーバーフェッチとは、実際に必要としているよりも多くのデータを取得してしまうことによってネットワークリソースや計算リソースを無駄にしてしまうことです。例えば、表形式の顧客リストがあったとしましょう。そして、その中からある顧客のfirstNamelastNameidという属性のみを表示させたいとします。典型的なRESTfulクエリでは、必要のない場合でも欲しい属性以外のすべての属性が返されることになります。

  1. GET /customers?limit=50 

このような問題を回避しようとすると、以下のように特別なURLパラメータを取り入れる必要があります。さらに、ハンドラコードを記述する際にそのようなパラメータを考慮に入れる必要があります。

  1. GET /customers?limit=50&props=firstName,lastName,id 

このようなことから、エンドポイントハンドラのコードはどうしても複雑になってしまいがちです。

しかし、GraphQLならばこのような問題でもエレガントに解決することができます。

  1. { 
  2. customers(limit: 50) { 
  3. firstName 
  4. lastName 
  5. id 
  6. } 
  7. } 

このように、GraphQLでは実際にリクエストしたプロパティのみを取得できるというわけです。

ただし、そこには落とし穴があります。

通常、GraphQLのリゾルバは、データベースに対して顧客のプロフィールエンティティをリクエストするような形で記述されています。その際、顧客のグラフノードが解決される前に、データベース内ではテーブル内の全カラムが選択されています(要するに、SELECT * というSQLクエリが投げられていることになります)。一方、RESTのエンドポイントハンドラでは、 SELECT firstName, lastName, id というクエリを実際に投げている可能性があります。実際のプロダクション環境のコードでこのような小さな最適化を見かけたことは一度もありませんが、RESTならば理論的にはこのようなことでも容易に実現することができるのです。


3. アンダーフェッチ問題

RPC形式のAPIで大きな問題となっているのが、アンダーフェッチです。必要なデータを取得するために何度もサーバーにリクエストを投げることにより、サーバーとの間に不必要なラウンドトリップが発生し、特にモバイルや低速ネットワーク回線においてレイテンシが増加してしまうことになります。

GraphQLのクリエイターの一人であるニック・シュロック氏は、最近公開されたドキュメンタリー動画の中で自動販売機の例えを交えながらRPC APIのアンダーフェッチ問題について説明しています。この例えはお世辞にもそれほど正確なものだとは言えませんが、RPCの問題を説明することには十分成功していると言えるでしょう。例えば、自動販売機ではボタンを押せば欲しいものが手に入ります。しかし、欲しいものが複数ある場合には、一度に1回ずつ複数回ボタンを押す必要があるでしょう。

ボタンを押して待つということを何度も繰り返さずに済むようにするために、一度に複数のものを提供できるような特別なボタンを作成することを思いつくかもしれません。API開発の観点からすると、このようなボタンは専用のAPIエンドポイントを作成することに相当します。しかし残念なことに、このようなエンドポイントは時間の経過とともにメンテナンスが不可能になってしまうような代物なのです。それぞれ独自のニーズを持った複数のクライアントが存在する場合やデータ要件の変更をもたらすようなフロントエンドのイテレーションが頻繁にある場合には、特にそのことが当てはまると言えます。

この回答を読んでいる人の中には、「でも、HTTTP/2には多重化の仕組みがあるよね?それを使えば複数のリクエストでも一度に投げることができるんじゃないの?」と思った人もいるかもしれません。たしかにその通りです。ただし、それが当てはまるのは特にヘッドオブラインブロッキング( Head-of-Line Blocking)が問題にならない場合にのみ限定されます。時には、次に呼び出すべき関数の選択が、前回の呼び出しでどのようなデータが返ってきたかに依存することもあるでしょう。

例えば、アルバムを表示させるアプリで考えてみましょう。そのアプリの中には、アルバムだけでなく、アルバムの中の写真やコメントをすべて表示できるビューがあるとします。コードで表すと、以下のような感じになるでしょうか。

  1. // APIクライアントの疑似コード 
  2. var albums 
  3. var pictures 
  4. var comments 
  5.  
  6. albums = GET /user/1/albums 
  7. for a in albums { 
  8. pictures = GET /user/1/albums/$a/pictures 
  9. for p in pictures { 
  10. comments = GET /user/1/albums/$a/pictures/$p/comments 
  11. } 
  12. } 

ここで、それぞれ10枚の写真を保持する4枚のアルバムがあったとしましょう。その場合、上記のコードでは計45回のHTTPリクエストを投げる必要があるでしょう。

  1. 1回 GET /user/1/albums 
  2. 4回 GET /user/1/albums/$a/pictures 
  3. 40回 GET /user/1/albums/$a/pictures/$p/comments 

HTTP/2の多重化(multiplexing)でも、ラウンドトリップの回数は最大で3往復になります(多重化なしの場合には最大で45往復となります。ただし、実際にはこれよりも少なくなると考えられます。HTTP/2ほど効率的ではありませんが、HTTP/1.1でもTCP/IP接続をオーブンにしたままにすることによって多重化を行おうとしているからです)。通信衛星を介してインターネットに接続している場合、ビューが読み込まれるまでに1秒近く(3回x最大350ms)も待つことになるわけです!

しかし、GraphQLならばこのような問題でも1回のラウンドトリップで解決することができます。その結果、読み込み時間を大幅に短縮させることが可能となります。

  1. query($uid: ID!) { 
  2. users(id: $uid) { 
  3. albums { 
  4. name 
  5. pictures { 
  6. src 
  7. name 
  8. comments { 
  9. contents 
  10. created 
  11. } 
  12. } 
  13. } 
  14. } 
  15. } 

4. キャッシャビリティ

RESTでは、非常にパワフルなHTTPキャッシュのメカニズムがデフォルトで使用されています。URLはキャッシュ可能なリソースを表わしており、 cache-controlヘッダで期限を指定することができるようになっています。

しかし、そこには1つの問題があります。この問題があるために、HTTPキャッシュではオーバーフェッチの最適化をうまく行うことができないのです。

まずはこちらのリクエストをご覧下さい。

  1. GET /customer?props=id,email,firstName,lastName,birthDate 

そして、次にこちら。

  1. GET /customer?props=id,email,firstName,lastName,birthDate,bio 

そう、上の2つのリソースはHTTPクライアントとサーバーの観点からは全く異なるリソースになってしまうのです。その理由は明白です。URLがリソース識別子になっているためです。

GraphQLはクライアント側のキャッシュに依存しており、クライアントの実装にキャッシュの責任を負わせています。スマートなGraphQLクライアントであれば、query.users内にあるidなどのリゾルバ引数によってエンティティをキャッシュすることになるでしょう。例えば、以下のようなクエリを以前に投げたことがあるとします。

  1. query($uid: ID) { 
  2. users(id: $uid) { 
  3. email 
  4. firstName 
  5. lastName 
  6. birthDate 
  7. } 
  8. } 

その後、さらにbioプロパティを追加したリクエストが送られたとします。その場合、bioプロパティ以外のプロパティはすべてキャッシュとしてクライアント側に残っているため、GraphQLクライアントは自動的にbioプロパティだけを送ります。

  1. query($uid: ID) { 
  2. users(id: $uid) { 
  3. bio 
  4. } 
  5. } 

GraphQLが適切なクライアント実装で使用されている場合には、どちらの方が実際にキャッシュとして優れているのかを判断するのは難しいと思います。

  • HTTPは中間キャッシュ(プロキシ)を利用してサーバーの負荷を軽減することができます。
  • 一方、GraphQLはクライアントのデバイス上にスマートにキャッシュすることでユーザーエクスペリエンスを向上させることができます。

5. GraphQL APIでは複雑すぎるクエリに対して何らかの対策を練る必要がある

複雑なクエリに対して全く対策が行われていないパブリックなGraphQL APIがあった場合、次のような複雑なクエリを一回投げるだけで簡単にAPIサーバーをダウンさせることができます。

  1. query { 
  2. users { 
  3. name 
  4. friends { 
  5. name 
  6. friends { 
  7. name 
  8. friends { 
  9. name 
  10. friends { 
  11. name 
  12. } 
  13. } 
  14. } 
  15. } 
  16. } 
  17. } 

とはいえ、GraphQL APIを複雑なクエリから保護するための方法はすでにいくつも存在しています。

  • クエリホワイトリスト(別名、「永続化クエリ(Persisted Queries)」)
    • 簡単なわりに非常に効果的だが、やや面倒。
  • クエリの深さに対して制限をかける
    • クエリホワイトリストよりも安全性に欠ける。
  • クエリコスト分析
    • 正しく利用するのが非常に難しい。

現時点では、クエリホワイトリストのアプローチが最も好ましいのではないかと思います。というのも、ただサーバー上に許可を与えたクエリのリストを保存しておき、それ以外のクエリを拒否するようにすればいいだけだからです。クライアント側で別のクエリが必要だと気付いた場合でも、すぐにサーバー上でそのクエリをホワイトリストに登録すれば何の問題もありません。

「でも、それだとRPCと変わらないんじゃないの?」と疑問に思う人もいるかもしれません。答えは極めて明白です。クライアントのデータ要件が変わるようなケースでもサーバー側を書き直す必要がない、という大きなメリットがGraphQLにはあるのです。ただホワイトリストを編集するだけで済むというわけです。

ただし、異なるサードパーティ製のクライアントが複数存在するようなAPIの場合には、ホワイトリストを使用する方法はおそらくベストな選択肢とは言えないかもしれません。「このクエリを下さい(can we have this query, please?)」問題と呼ばれる問題があるためです。あるいは、APIに対して実行可能なクエリが何かということに対して完全なコントロールを持っているからだとも言えるでしょう。いずれにせよ、時と場合によるということでしょう。

6. GraphQLを正しく実装することは至難の業である

最近、GraphQL + Dgraph + Goという3つの技術を現実的と思われる方法で組み合わせた小規模の技術デモを公開したことがあります。しかし、このデモには大きな欠陥がありました。キャッシュやバッチ処理の最適化が行われていないため、必要以上にデータベースに負荷がかかり、n + 1リクエストという有名な問題が発生してしまうのです。

Restful APIの場合、OpenAPIのようなツールセットを使用すればプロダクション環境でも簡単に導入を行うことができます。一方、GraphQLの場合には正しく実装しようとするだけでもはるかに多くの労力と経験が必要となります。

7. GraphQLのスキーマ言語にはまだそこまでパワフルな機能が存在していない

  • 残念なことに、GraphQLにはジェネリックプログラミングの機能がありません。そのため、ページネーションのあるリストを表現しようとした場合には、以下のように非常にゴチャゴチャした感じになってしまいます。
  1. type A {} 
  2. type B {} 
  3.  
  4. type ListA { 
  5. size: Int! 
  6. version: Int! 
  7. items( 
  8. after: AID 
  9. limit: Int! 
  10. ): [A!]! 
  11. } 
  12.  
  13. type ListB { 
  14. size: Int! 
  15. version: Int! 
  16. items( 
  17. after: BID 
  18. limit: Int! 
  19. ): [B!]! 
  20. } 
  21.  
  22. type Some { 
  23. as: ListA! 
  24. ab: ListB! 
  25. } 

以下のように記述できれば、もっと分かりやすくなるのですが…

  1. type A {} 
  2. type B {} 
  3.  
  4. type List<T> { 
  5. size: Int! 
  6. version: Int! 
  7. items( 
  8. after: ID<T> 
  9. limit: Int! 
  10. ): [T!]! 
  11. } 
  12.  
  13. type Some { 
  14. as: List<A>! 
  15. ab: List<B>! 
  16. } 
  • パッケージやインポートという概念がないため、スキーマ全体を1つのファイルに書いたり、ハッキーな連結テクニックに頼る以外に方法がない。
  • これ以外にもまだまだあるでしょう

結論

控えめに言っても、GraphQLはまだまだ発展途上の段階にあると思います。そして、それはグラフ時代のほんの始まりに過ぎないのです。そう遠くない未来に、Graph API開発に対する新たなアプローチが出現するのを私たちは目の当たりにすることでしょう。その中には、私が現在取り組んでいるサービスモデリング言語( Service Modelling Language)も含まれているはずです。

この3年以上の期間を通して、バックエンド開発のコストと複雑さを劇的に減らしつつ、開発者の満足度を大幅に高めるような方法が存在するということに私は気づきました。しかし、開発者が手作業でスマートに実装を行わなければならないプロトコル仕様であるGraphQLではそれを実現することはできないでしょう。

そこで、私はある1つの言語のコンセプトを作ることにしました。その言語とは、関数型で静的に強く型付けされており、100%宣言的でありながらもチューリング完全であるという特徴を備えたプログラミング言語であり、非常に簡潔な方法でスケーラブルかつ保守の容易なWebサービスを記述することができるような言語です。以下にその特徴を記載してみましょう。

  • チューリング完全なトランザクション(いわゆる、GraphQLの「Mutation」)
  • チューリング完全なアクセス許可、グラフ解決、ビジネスロジック
  • 基盤となる(組み込みの)分散型グラフデータベース
  • CQRS(クエリには副作用がないことが保証されることになります。何らかの理由でトランザクションが失敗した場合に備えて、トランザクションのみが自動的にロールバックされる副作用を生み出すことを許可されています。)
  • APIテストの分離
  • ダウンタイムゼロのデータベースマイグレーション
  • セマンティックバージョニング2.0に準拠したスキーマおよびAPIのバージョニング
  • などなど。

このような機能がすべて1つの言語にまとめられているので、開発者の方は以下のことを気にする必要がなくなるというわけです。

  • N+1リクエスト問題
  • データベースおよびAPIのスケーリング
  • データベースのインデックス作成
  • クエリコスト分析および複雑なクエリに対する保護機能
  • クライアント、スキーマ、サーバー、データベース間での型の違い
  • データベースのマイグレーション
  • 後方互換性およびバージョニング
  • クライアントサイドのキャッシュ、クエリ集約など

0 コメント:

コメントを投稿