クライアント/サーバー通信のための言語仕様「GraphQL」

田尻宗準氏:「minne AndroidにおけるGraphQLの取り組み」という内容で、minne事業部 Android開発チームの田尻が発表します。

自己紹介をします。minne事業部の田尻宗準です。社内では「ムネ」と呼ばれています。2020年4月入社で、現在は「minne byGMOペパボ」のAndroidアプリ開発を担当しています。最近、minneでは特徴絞り込み機能をリリースしました。例えばピアスなら「素材、形状、金属アレルギー対応」など、作品の特徴で絞り込み検索ができるようになりました。私に関してお話すると、ビールが好きで、Netflixでドラマを観るのにハマっています。Twitterなど、インターネット上では「_mune0903」のアカウントを使っていることが多いです。よろしくお願いします。

本日の内容です。1つ目が「GraphQLとは何か」。2つ目が「なぜminneでGraphQLを採用したのか」。3つ目が「Apollo GraphQL clientを使ってみる」。4つ目が「GraphQLを採用して良かったこと」。最後が「まとめ」という構成です。

はじめに、「GraphQLとは何か」をざっくり紹介します。GraphQLとは、Facebookのクライアント/サーバー通信のための言語仕様です。GraphQLは、クエリ言語とスキーマ言語からなっています。

SQLを少し知っていることを前提とした話になりますが、クエリ言語については、SQLと問い合わせ言語という点で共通しています。GraphQLのオペレーションであるQueryは、SQLでいうところのSELECTにあたり、Mutationは、INSERT、UPDATE、DELETEにあたります。SubscriptionはGraphQL特有のオペレーションで、リアルタイムにデータの更新情報を受け取ります。Facebookの「いいね!」機能で使われているそうです。スキーマ言語に関しては、GraphQL APIの型システムを定義します。これは、クライアントがアクセスできる、かつ、存在しているデータの完全な集合を意味します。GraphQLはこのスキーマをもとにレスポンスを生成します。

(スライドを指して)ここに、スキーマ、クエリ、レスポンスの一例を紹介します。スキーマにPerson型を定義するとします。Person型はID型のidと、Int型のage、String型のnameを持ちます。クエリでは、「person」のデータを取得するために必要なフィールドを記述します。今回はid、nameを指定してリクエストを送ってみます。そうすると、クエリで指定したフィールドのみレスポンスが返ってくるので、id、nameだけが返ってきます。

GraphQLは、クエリとレスポンスデータの構造がすごく似ていて、レスポンスを想像しながらクエリが書けるので、非常にわかりやすいという特徴もあります。また、クエリドキュメントに複数のクエリを書けます。ただ、実行できるオペレーションは一度に1つだけなので、QueryとMutationを同時に実行できません。複数のクエリを実行した場合も、一度に実行した分だけのデータを取得できます。以上、GraphQLについての簡単な紹介でした。

APIの柔軟性と型による堅牢性の恩恵を受けるためにGraphQLを採用

次に、「なぜminneでGraphQLを採用したのか」について話します。minneでは、APIの柔軟性と、型による堅牢性における恩恵を受けるためGraphQLを採用したという背景があります。

GraphQLではクエリ言語を操作してデータを操作するため、クライアントは本当に必要なリクエストのみを送ることができるという柔軟性が備わっています。また、スキーマによる型付けにより型安全で運用ができ、スキーマによってクライアントがアクセスできるほか、存在しているデータの完全な集合が担保されている堅牢性が特徴です。

(スライドを指して)GitHubのREST実装のv3 APIと、GraphQL実装のv4 APIを比較したいと思います。まず、スクリーンショットのように、RESTではレスポンスの過剰な取得が起こり得ます。GitHubのユーザーエンドポイントは、39のキーからなるオブジェクトをレスポンスとして返します。これは非常に大きいレスポンスで、クライアントが必要としていない、つまりユースケースに合わない余分なデータが多く含まれている可能性があります。

例えば、クライアントがlogin、avatar_url、nameの3つのキーしか必要としていない場合、ここでは過剰な取得が起きていることがわかります。3つのキーが欲しいだけなのに、実際に受け取っているのは39のキーからなるオブジェクトで、ネットワークを介して無駄なデータを受け取っています。

同じデータをGraphQLで取得した場合、どうなるかを見ていきます。(スライドを指して)画像の左側がGraphQLのクエリです。先ほど紹介したように、クエリではクライアントが必要なフィールドのみを指定しています。右側が受け取るJSONレスポンスです。要求したフィールドのみがレスポンスに含まれ、ネットワークを介して余分な36ものフィールドを取得せずに済みます。欲しいデータだけを受け取れるようにクエリを作成して、望んだデータのレスポンスを受け取ることができます。余分なデータを取得しないことで、より高速にレスポンスを受け取れる可能性も出てきます。

次は、RESTにおける過小な取得についてです。先ほど取得したlogin、avatar_url、nameに加えて、follower数、following数、gistの名前も欲しい場合を考えてみます。この場合、user、followers、following、gistの4つのエンドポイントが必要となり、それぞれにリクエストを送るため、合計4回のリクエストが必要です。さらに多くのリクエストを送信する場合は、それぞれのHTTPリクエストがクライアントのリソースを占有する上、余計なデータが多く含まれる可能性もあります。

その結果、レスポンスが遅くなるうえユーザー体験が悪化して、性能の低い端末を使用しているユーザーや、低速なネットワーク環境下のユーザーは、コンテンツの閲覧すら難しくなる可能性があります。

本当に必要なデータは、login、avatar_urlなどの6個だけなのに、それに対して非常に多くのレスポンスが返ってきてしまう問題も考えられます。先ほど紹介したように、GraphQLではまとめてクエリを定義することで、一度のリクエストで必要なデータすべてを要求できます。また、必要なデータのみ受け取るためすっきりします。

最後は、エンドポイントの管理についてです。RESTでは、クライアントに変更が加わると新たなエンドポイントを生やすケースがあります。また、リクエスト回数を最適化しようとすると、エンドポイントの数が膨れ上がることもあると思います。

GraphQLは送られるクエリに基づいてデータを統合するため、単一のエンドポイントで済みます。Railsなら、コントローラとアクションが1つで済みます。

こうした課題をもとに、minneではAPIの柔軟性、型による堅牢性における恩恵を受けるためGraphQLを採用しています。minne Androidは新規機能で優先的にGraphQLを使っています。実はまだ移行自体が始まったばかりなので、がんばって移行している最中です。以上が、minneでGraphQLを採用した理由です。

Apollo GraphQL clientの使用例

次に、Apollo GraphQL clientを用いてクエリを実行するまでの流れを簡単に紹介します。Apollo GraphQL clientは、GraphQLでHTTPクライアントを実装するために使用されます。minneではOkHttp3とApollo GraphQL clientを使って通信を行っています。また、Apollo GraphQL clientはスキーマとクライアントのリクエスト情報をもとに、自動でクライアントが利用する型を持つモデルを生成します。Apollo以外にも、「Relay」というGraphQL clientがありますが、Relayは「React」と「React Native」のみ対応しているので、minneではApollo GraphQL clientを採用しています。

実際に、AndroidでApollo GraphQL clientを使えるようにしていきます。(スライドを指して)まず、Apolloのpluginをいつもどおり「gradle」に追加します。このpluginには、プロジェクトをビルドした時にモデルを自動生成するためのコンパイラが含まれています。

次に、依存関係もgradleに追加していきます。現在は、2.5.9が最新の安定版です。(※取材当時)Apollo GraphQL clientはCoroutinesをサポートしているので、(スライドを指して)以下の拡張機能を利用できます。そのため、「coroutines-support」も一緒に入れるとよさそうです。

私も自動生成されるモデルクラスは問答無用でJavaだと思っていましたが、このようにgradle内のオプションを追加するとKotlinでモデルクラスを生成してくれます。minneはJavaで生成させていたので、今後はKotlinにしたいと思っています。

次は、GraphQLのスキーマを追加します。Apollo CLIを使えば簡単に取得できます。ここでは、GitHubを題材として紹介します。「apollo client:download-schema」コマンドを使って、「--endpont」、tokenなどを「--header」に乗せてリクエストすれば、(スライドを指して)スクリーンショットのようにGitHubのschema.jsonを取得できます。Build Variantsが複数あるminneでは、schema.jsonを取得するためにエンドポイントなどをその都度変えるのが大変なので、別途シェルスクリプトなどを書いて便利にしています。schema.jsonは、APIモジュール内のGraphQL配下などの、しかるべき場所に置いてください。

次は、「GraphiQL」を使ってクエリを書きます。GraphiQLとは、クエリの実行結果などをブラウザ上で確認できる便利ツールです。GraphiQLは間違った引数や存在しないフィールドなど、正しくないクエリを書くと親切にエラーを吐いてくれます。そのため、GraphiQL上で正しいクエリかどうかの最終確認を行います。

次に、実際にプロジェクトにクエリを追加します。ここで注意すべきは、schema.jsonと同じディレクトリ配下にViewer.graphqlなどのGraphQLファイルを作成することです。そして、先ほどGraphiQLで書いたクエリをコピー&ペーストして終わりです。(スライドを指して)これは実際のminneのディレクトリですが、このようなディレクトリ構成になっていくと思います。

ここまでできたら1回ビルドします。ビルドすると、スキーマとクエリからモデルクラスが自動生成されます。これでアプリからクエリを実行する準備がほとんど整いました。実際にリポジトリからクエリを実行すると、自動生成されたモデルに必要な引数等を渡して、Apollo GraphQL clientを使ってクエリを実行します。

そしてRetrofit同様に、レスポンスをハンドリングするのですが、GraphQLはエラーであっても200を返すので、ステータスコードでのハンドリングはできません。注意が必要です。自動生成されたモデルをレスポンスの型として使うこともできますが、UseCase、ViewModelにApolloの依存関係が漏れることは今後の変更に弱く、設計的によくないので、minneでは独自のEntityにmapして使用しています。あとはUIレイヤーで取得したデータを好きなように使って終わりです。以上がApollo GraphQL clientの簡単な使用例でした。

GraphiQLが便利で開発体験が良い

次は、「GraphQLを採用して良かったこと」を紹介します。まず、GraphQL APIの便利ツールが便利すぎて、開発体験がいいと感じました。GitHub同様にminneでもGraphiQLを使用していますが、シンタックスハイライトや、入力補完、構文エラーの表示などの機能が利用できるほか、クエリの実行結果をブラウザ上でさくっと確認することもできます。

GraphiQLでは、APIのリクエストを作成するために必要な情報がすべて記載されたドキュメントを見ることができます。これはGraphQLが持つイントロスペクションという強力な機能によって、APIスキーマの詳細を取得できるからです。ここで問題なくクエリが実行できることを確認したら、先ほど紹介した「.graphql」ファイルにコピー&ペーストしてAndroidの実装に取り掛かるという流れなので、クエリを間違えるなどのミスは起こりにくいと思います。

サーバーサイド・クライアントサイド関係なく双方向なやり取りが活発になった

いきなりですが、次の「よかったところ」に入る前に、minneの歴史をざっくり紹介します。「minne web」はRuby on Rails製のアプリケーションです。Webフロントは、現在進行形で「Next.js」で刷新しています。Webは2012年に爆誕し、iOSは2012年に爆誕し、Androidは2013年に爆誕しました。

いろいろなサービスに共通していると思いますが、Webの開発がクライアントよりも先行していたこともあり、Web APIの開発のほとんどをサーバーサイドやWebのエンジニアが担当していました。エンジニアがクライアントサイドのWeb APIの開発にまったく参画していなかったわけではありませんが、APIの仕様検討の段階からサーバーサイドのエンジニアと積極的に開発する機会や体制はなく、単方向的なやり取りが多かったと思います。

(スライドを指して)これは、以前のminneにおけるWeb APIの開発スタイルを表したものですが、設計をWeb APIの開発者に依存していて、APIの実装がある程度進まないとクライアントの実装に着手しづらいなど、開発者と利用者のかかわり方が単方向になりがちという課題がありました。

導入するにあたってminneでは、サーバーサイド、iOS、Android、Webフロントの各プラットフォームから1名ずつ代表者を決めて「GraphQL会」を発足しました。minneはAPIのバージョンが複数存在するので、今後のWeb API開発について話し合いをしました。GraphQLスキーマを実装する人はサーバーサイドのエンジニアに限定しないことや、スキーマレビューのルールなど、大まかな開発フローを決めるほか、エラーハンドリング、ページネーション、命名規則など、導入前に固めることはみんなで話し合いました。

サーバーサイド・クライアントサイドのエンジニアが集まって、GraphQLの導入を一緒に促進しました。GraphQLによってサーバーサイド・クライアントサイド関係なく双方向なやり取りが活発になる、よりよい体制を築けたことが一番よかったと思っています。

これがGraphQLを導入したあとのWeb APIの開発スタイルです。いわゆるスキーマ駆動のWeb API開発で、Web APIの開発者・利用者が同時に実装を進められるので、開発効率が向上します。さらに、開発者・利用者が設計から参加するため、以前の体制と比べれば検証時の認識のずれも相対的に起きにくいと言えます。以上が、「minneでGraphQLを採用して良かったこと」の紹介でした。

最後に、まとめです。本日は、「GraphQLとは何か」から「GraphQLを採用して良かったこと」までを紹介しました。発表を通してわからないことやもっと知りたいことがあれば、ハッシュタグ「#android_meetup」やメンションなどでお待ちしています。本日は、貴重な機会をありがとうございました。以上です。