スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

DroidKaigi 2024 で GraphQL クライアント実装について登壇しました

こんにちは、Androidエンジニアの@morux2です。先日 DroidKaigi2024 にて、「GraphQLの魅力を引き出すAndroidクライアント実装」という発表を行いましたので、その報告をさせていただきます。 2024.droidkaigi.jp

発表資料はこちらになります。 speakerdeck.com

早速アーカイブも公開していただいておりますので、ぜひご覧ください。 www.youtube.com

発表内容のサマリ

スタディサプリ小学講座・中学講座で GraphQL を全面採用し、2年ほど運用した中で蓄積された ADR (Architecture Decision Records) を紹介しました。*1 紹介した ADR は以下の4つになります。

  1. 原則としてドメインロジックはサーバーサイドで実装し、GraphQL Schema 定義に含める
  2. Apollo Client の Wrapper クラスを定義し、各画面から 呼ぶ GraphQL Query / Mutation を一任する
  3. GraphQL Errors と HTTP Status Code のマッピング
  4. Fragment Colocation の指針

1. 原則としてドメインロジックはサーバーサイドで実装し、GraphQL Schema 定義に含める

教育ドメインでは、クライアント間での挙動の差が大きな問題になり得ます。そこで、単にデータソースを返す API にせず、適切なドメインモデルに落とし込んだ GraphQL Schema を設計することで、クライアント間での挙動の差を起こりにくくしています。GraphQL のリクエストで完結する画面については、クライアントでは UI レイヤのみを実装していく形になります。スタディサプリ小学講座・中学講座では、UI レイヤの実装に Jetpack Compose と AAC の ViewModel を用いています。

2. Apollo Client の Wrapper クラスを定義し, 各画面から 呼ぶ GraphQL Query / Mutation を一任する

各画面ごとに feature モジュールを定義し、その中に View / ViewModel / ApolloWrapper を配置します。ViewModel から直接 ApolloClient を呼び出すのではなく ApolloWrapper を経由することで、画面の描画に複数の Query / Mutation を叩く必要がある場合でも ViewModel の処理をシンプルに保つことができるメリットがあります。*2 ViewModel では ApolloWrapper から受け取ったレスポンスと、その他のローカルやリモートの DataSource の値を束ねて、画面の状態を計算します。

3. GraphQL Errors と HTTP Status Code のマッピング

GraphQL Over HTTP では、GraphQL の Errors の有無と HTTP Status に関係を持たせず, 常に 200 を返却することが推奨されています。しかしスタディサプリ小学講座・中学講座では、クライアント起因のエラーは400、サーバーサイド起因のエラーの場合は GraphQL の Errors とステータスコード500を返却しています。サービスの特性上、200を返却したことで得られるメリットが少ないためです。クライアント起因のエラーは401(認証エラー)が返却される可能性があり、OkHttpClient の Authenticator でハンドリングをしています。 サーバーサイド起因のエラーは、GraphQL Errors の有無で判断し、Errors が存在する場合は例外を投げています。*3

4. Fragment Colocation

GraphQL Fragment と UI コンポーネントを「一緒に配置する」 ことを、Fragment Colocation と呼びます。Fragment Colocation を実現すると、UI コンポーネントが必要とするデータがわかりやすく、メンテナンス性が向上するメリットがあります。Apollo Kotlin plugin を用いると、Apollo が生成したクラスからGraphQL の定義元のファイルにジャンプできるので、Composable 関数の第一引数に Fragment のクラスを渡すことで、Fragment Colocation を実現することができます。スタディサプリ小学講座・中学講座では、Composable 関数が GraphQL Query / Mutation の フィールドに依存している場合は、Compose の Preview の単位にあわせて Fragment を切ることにしています。UI コンポーネントと関係のない Fragment を共有することは、オーバーフェッチの要因になるため避けるべきです。

スライドの付録

  • Persisted Query に変わるクエリ信頼の仕組みとして Signed Query を採用する
    • サーバーに負荷がかかる悪意のあるクエリが実行できないように、開発者が信頼したクエリのみを受け付ける仕組みを実装しています
  • ViewModel 廃止検討
    • Viewから直接 ApolloClient を呼び出すことで、ViewModel や ApolloWrapper を用いる場合よりも冗長性を排除できないか検討しています

質疑応答

当日の質疑応答の一部をご紹介させていただきます。

ADR はどのように運用されていますか?

GitHub の issue でテーマごとに管理をし、コメントにレコードを連ねております。詳しくは以下のブログをご覧ください。 blog.studysapuri.jp

GraphQL スキーマの管理はどうしていますか?

API Gateway では GraphQL スキーマに破壊的な変更が入っていないことを GraphQL Inspector を用いて確認しています。Androidリポジトリでは、downloadApolloSchema タスクを用いてスキーマを更新します。GHA を毎日定期実行して更新 PR が自動生成されるようにしています。変更内容は PR の diff を見て確認し、手動でマージしています。

自動生成された PR

./gradlew downloadApolloSchema \
  --endpoint="https://your.domain/graphql/endpoint" \
  --schema="app/src/main/graphql/com/example/schema.graphqls"

参考 : 2. Add the GraphQL schema | Apollo GraphQL Docs

Fragment Colocation は実践した方が良いでしょうか?

実践した方が良いと思います。例えば、デザインシステムで定義された、画面間で共有している UI コンポーネントに対して Fragment Colocation を実践すれば、変更が入った時の修正範囲を UI コンポーネントに閉じることができるので、画面ごとに Query の修正が必要なく、メンテナンス性が高まります。

Fragment Colocation については、以下の資料が参考になります。

Fragment Composition of GraphQL - Speaker Deck

GraphQL の Type を跨ぐような Fragment を定義することはできますか?

できます。GraphQL の Query や Mutation 自体も型として定義されているので、以下のように定義が可能です。

type Post {
    id: ID!
    title: String!
    body: String!
}

type Author {
    id: ID!
    name: String!
}

type Query {
    post(id: ID!): Post
    author(id: ID!): Author
}
fragment SampleFragment on Query {
    post(id: $postId) {
        title
        body
    }
    author(id: $authorId) {
        name
    }
}

(雑談) Apollo Kotlin 4系への対応は進めていますか?

4系は7月末に発表されましたが、現状スタディサプリ小学講座・中学講座では3系を利用しています。4系はまだ対応しておりません。 github.com

4系ではエラーハンドリングに変更が入ります。ApolloResponse に exception のフィールドが追加され、クライアントに起因するエラーが例外(ApolloHttpException等)として投げられる代わりに exception に格納されるようになります。Flow の catch・retryWhen ブロックの処理や、自前で定義している例外の扱いを見直す必要がありそうです。

参考 : Migrating to Apollo Kotlin 4 | Apollo GraphQL Docs

当日の様子

大規模なカンファレンスでの登壇は初めてだったので不安でいっぱいでしたが、スピーカーディナーでスピーカーやスタッフの方とお話しして緊張をほぐすことができました。当日のセッションを楽しみにしているという声もいただいて励みになりました。

豪華なディナーでした✨

当日は、運営の方々の細やかな配慮によって自信を持ってリラックスしながら発表できたかなと思います。控え室ではメイクもしていただき、おすすめのコスメも教えてもらいました💄 質疑応答で多くの方と知見共有をできたことも大変嬉しかったです。

さいごに

今回の発表は Android チームのみでなく、多くの社内のエンジニアの方にレビューをいただき完成させることができました。また、DroidKaigi の運営チームの皆さまの多大なるサポートによって、スピーカーとしてセッションを楽しむことができました。多くの関係者の皆さま、およびセッションをご視聴いただいた方々に改めて感謝申し上げます。ありがとうございました。

*1:https://blog.studysapuri.jp/entry/architecture_decision_records

*2:本来は1度の Query で画面の構成要素を全て取得できることが理想であり、GraphQL Schema に改善の余地がある状況です

*3:GraphQL Errorsの有無のみを確認しており、中身を見たロジックは組んでいません