この記事はアプリチームのAndroid、Backendを主に担当している松田がお送りします。
概要
現在、アプリのバックエンドはREST APIで構築されていますが、これを新規開発はGraphQLに移行しました。移行した背景と技術的な選択、実装時の考慮点を紹介します。
移行理由
GraphQLへの移行を考えたのはオーバーフェッチの問題です。
オーバーフェッチをしないために、ユーザー情報の型がタイムライン用、ユーザー詳細用、店舗の口コミ詳細用と様々あり、管理が大変になっていました。
このような理由から、スキーマを定義して1つだけ情報を返す実装すれば良いGraphQLに移行する事にしました。
技術選択
実装方法
まずは、現在使用しているKotlinのバックエンドの上に乗せるか、社内で標準となっているGoで新しく作り直すかを決めました。以下の理由から既存のKotlinバックエンドの上に実装する事にしました。
- 既存のバックエンドと新しいバックエンドを両方保守するコストが高い
- 一気に全て移行できるわけでもなく、全て移行するには何年かかるか目処も立たない為
- Goを使用すると、アプリバックエンドをWebチームが触りやすくなるメリットがあるが、現状から触ることはあまり考えられない事
- セッション、その他のデータソースへの接続等の実装を再度実装しなくて済む事
GraphQLライブラリ
まずはgraphql.orgから使えそうなライブラリを探してみました。
既存のフレームワーク(Jersey/JAX-RS)上に構築する為、Spring用のものなどは使えません。
https://graphql.org/code/#java-kotlin
Kotlinを使用しているので以下のライブラリを検討しましたが、スキーマからコードを生成するスキーマファーストではなく、コードからスキーマファイルを生成するコードファーストなライブラリでした。
https://github.com/ExpediaGroup/graphql-kotlin/
コードファーストにする事に関して、以下の懸念がありました。
また、スキーマを最初に全員でレビューして使用側、実装側の合意をしてから開発に着手するという方法にスキーマファーストが合っていました。これは、マイクロサービスのprotoでも行っている為、それに合わせるという面もあります。
その為、スキーマファーストなKotlinを生成できるライブラリを探しました。その結果、以下のライブラリ郡を使用する事にしました。
- スキーマの生成
これくらいしか見つからなかったので比較対象はありません。
https://github.com/kobylynskyi/graphql-java-codegen - GraphQLの実装
多くのライブラリがこれをラップしているだけなので、そのまま使います。
https://github.com/graphql-java/graphql-java - 未登録のリゾルバを検出する用途
https://github.com/graphql-java-kickstart/graphql-java-tools
実装時に困った事、対応した事
サーバーのコード生成
サーバーのコード生成ライブラリに関して、足りない機能がありました。戻り値について、 CompletionStage<DataFetcherResult<T>>
のような二重になった型パラメータを出力する事ができませんでした。
https://github.com/kobylynskyi/graphql-java-codegen/issues/1167
fun hoge(id: Id, env: DataFetchingEnvironment): CompletionStage<DataFetcherResult<Hoge>>
その為、PRを提出して対応しました。
https://github.com/kobylynskyi/graphql-java-codegen/pull/1200
セキュリティ
GraphQLは何も対策を行わなければ、クライアントから任意のクエリが発行できてしまいます。大きいネストしたリクエストを送って、負荷を書ける事もできてしまいます。
コストを設定し、大きいリクエストを行えないようにすることも可能ですが、正規のリクエストもコストに気を配ったりしなければならず面倒です。その為、事前に発行できるクエリを登録しておき、クエリのhashだけを送ってリクエストするPersisted Queries
を使用する事にしました。
アプリケーションのリリースビルド時にクエリをCIでAWS S3に送信し、それをサーバー側で参照するようにしました。
DataLoader
n+1問題が発生しないように、ほぼ全てのデータをDataLoader経由で取得するようにしました。
GraphQLとの統合により、クエリの階層ごとにリクエストがまとめて行われるため、効率よくデータが取得できます。
https://github.com/graphql-java/java-dataloader
Datadog
Rettyではサーバーの監視にDatadogを使用していますが、Datadogを入れている環境だけエラーが起きる問題がありました。
Java9からの機能である、CompletableFuture.completedStage
を使用するとDatadogAgent側でエラーが起き、そのフィールドだけリクエストが失敗するというものでした。
こちらはIssueだけ提出して、暫定としてCompletableFuture.supplyAsync
だけを使用するようにしました。
https://github.com/DataDog/dd-trace-java/issues/6284
デバッグ
エラーが起きた際に追跡しやすいように、graphql-javaのInstrumentationとリフレクションを利用して、idという名前のフィールドをDatadogやログに出力するようにしました。
iOSのコード生成
Android/iOS共にGraphQLのクライアントにはメジャーなApolloを採用しました。
iOSのコード生成で、ドキュメントでは生成されたコードを編集してくださいと書いてあるところ、コメントには編集しないでくださいと書いてあるコードが生成されました。使い方に頭を悩ませましたが、直前のリファクタで入った不具合でした。
https://github.com/apollographql/apollo-ios/issues/3323
iOSのOSSライブラリの開発とPRの提出、tuistの使用は初めてだった為、四苦八苦しました。
https://github.com/apollographql/apollo-ios-dev/pull/243
導入結果
まずは以下のAndroidの店舗ページから全面導入を行いました。導入してみてどのような事が変わったのか紹介します。
開発の容易性
サーバーでは1つのオブジェクト(人、店舗、投稿等)に対して1つだけ定義を行えば良いため、シンプルに開発する事が可能になりました。
また、フィールドごとに並列に実行される為、並列処理を気にする事無く実装する事ができます。
実行速度の向上
フィールドごとに並列に実行される為、今まで並列処理にしていなかった部分も並列に処理されるようになり、実行速度も向上しました。
店舗の表示に使用している旧APIは1秒程かかっていた上に、追加で0.1秒程かかる3つのAPIを呼び出していました。GraphQLに移行後は全て合わせて400ms程で取得できるようになり、社内からも体感できるほど早くなったと声がありました。
特にGraphQL+DataLoaderを使用して、特に工夫せず実装して実行速度が改善したというのが良い点です。
おわりに
これらは通常の施策開発の合間に進めていた為、1年程かかりましたが、無事に導入する事ができました。
iOSアプリに関しては、まずは店舗詳細の一部だけに導入しました。今後はiOSメインの開発者と少しずつ移行を行っていきます。
開発思想に合ったライブラリの調査、修正、他サービスとの統合時の不具合等で作業範囲が多岐にわたり大変でしたが、開発体験もユーザー体験も向上し、GraphQLの導入をして良かったという評価をしています。