GraphQL Client Architecture Recommendation 社外版

この記事は、Merpay Advent Calendar 2022 の15日目の記事です。
こんにちは。メルペイのvvakameです。

最近、社内向けにGraphQL Client Architecture Recommendationというドキュメントを書きました。社内のiOS/Android、そしてバックエンドのエンジニア向けにGraphQLをやるならこの辺りの条件を満たしておかないと恩恵を感じられなくなっちゃうかもよ、と伝えるためのものです。嬉しいことに、今までに100名弱の人たちがこのドキュメントを閲覧してくれたようです。

これをAdvent Calendarで公開するために、ちょっと調整したものがこの社外版です。
すでにGraphQLをやっているけどあまり便利じゃないな…なんでだろ?とか、これから導入したいんだけど何を気をつけるべきかな…と考える時の材料にしてください。
併せて、拙書GraphQLスキーマ設計ガイドも参照していただけると幸いです。

元の文章の背景

グループ全体を見るとSouzohのメルカリShopsや社内ツールなどでGraphQLを導入しているところはありますが、会社全体で見るとまだまだ広く使われるには至っていません。
筆者はGraphQLの大ファンで、全社的に使われるようになって全社大統一APIが爆誕しないかな…と期待しているのですが今の所実現まではまだ遠そうです。

そんな中、一部の部門でGraphQLをバックエンド主導での導入を計画しているという話を聞きつけ(Slackの個人の分報チャンネルとかで情報が入ってきます)、すこしウォッチした結果、クライアント側のアーキテクチャを考慮することでより生産性が上がり、将来に渡って使いがいのある資産となりそうだと思いました。
そんな愛されモテカワGraphQLエンドポイントをみんなで作り上げるために、絶対守ってもらいたい勘所を覚えて生産性100倍!という気持ちで筆者の経験を詰め込みました。最後まで読んでね!

メルカリではクライアントと言った場合、おもにアプリ(Android/iOS)を指す場合が多いです。Webの場合はWebとかWebフロントエンドと表現する場合が多いです。
このドキュメントでもAndroid/iOSを主なターゲットとして記述しています。

このドキュメントでは設計上の指針と、考慮すべき事項について指摘しています。指摘だけして、あとは担当チーム内で議論してくださいね、ということで(組織によって答えも異なるため)結論を出していない箇所も複数あります。
また、想定読者が社内の人間であるため、利用数や開発組織の規模もそれなりに大きなものを想定しています。小規模な組織(たとえば筆者も関わる技術書典の運営など)であれば、ナァナァで済ませてしまってもいい箇所もあります。
読者の方々でそれぞれ自分たちに役立ちそうな箇所をピックアップして便利に使ってみてください。

それではドキュメント本編をお楽しみください。

Introduction

GraphQLは非常に強力なツールで、アプリケーション開発に用いると簡単にお客さま・開発者双方に対してリッチな体験を作り出すことができます。
ただし、GraphQLを単にプロトコルであると考えて取り組むとそのパワフルさを体験することができません。そのくせ、GraphQLを使いこなさなくても開発はできてしまうのです。そして、GraphQLはパワフルではないし、面倒なだけだと思い込み、嫌いになってしまいます。

筆者はGraphQLは特定のアーキテクチャと組み合わせることでそのパワフルさを発揮し、UIの開発を簡単で手軽なものにしてくれることを体験しています。
このドキュメントではどのようなクライアントアーキテクチャであればGraphQLとの親和性が高いかを解説し、そのパワフルさをプロダクトに組み込めるか方針を示します。
また、GraphQLをパワフルに使うにはバックエンドの協力も不可欠です。

筆者が考えるGraphQLのアドバンテージを次に示します。
もし、このアドバンテージが自分たちのプロダクトにも必要だと思ったら、GraphQLの導入を前向きに考えてみてください。

  • 型付けされたデータと、それに対する操作
    • 操作の組み立てや、得られるデータについて静的に検証可能
    • フィールド名や値の型について実行せずとも問題がないことがわかる
  • UIに必要なデータを過不足なく取得可能
    • 必要なデータをUIに合わせた形で取れる
    • 計算が必要なデータをcomputed fieldとしてバックエンドが実装しつつ、UIに不要なときはその負荷を発生させない
  • 宣言的UIとの親和性が高い
    • GraphQLクエリで取得した結果をノーマライズされたキャッシュとしてローカルに構築する
    • キャッシュに変化が生じたら自動的にUIに反映されるため、データ操作後のUI更新は自動的に管理・反映される

Goal

GraphQLを利用する上で設計上必要な考慮が何かを明示する。

Non-Goal

GraphQLに関する基本的な知識の解説。

GraphQL with Declarative UI

GraphQLを使う上で、宣言的UIとの組み合わせは非常に重要です。宣言的UIと組み合わせたアーキテクチャがもっともGraphQLの旨味を引き出し、パワフルさを得ることができます。
もし宣言的UIと組み合わせないのであれば、GraphQL以外のものを検討してもよいでしょう。

Android/iOSに対してはApollo以外のクライアントの選択の余地はほぼなさそうですので、これを前提とします。

GraphQLクライアントはQueryで得た結果をキャッシュに保持します。このキャッシュはノーマライズされ、バックエンド上のDBの部分的なコピーを作り出します。
Mutationでのoptimistic updateやレスポンスの結果、キャッシュは更新されます。このキャッシュはSingle source(state) of truthとして機能します。

Single source of truthはWebで広く使われる宣言的UIであるReactと、そのソースとなるアプリケーションの状態をカバーするReduxの3つの原則のうちの1つです。
ReduxRxKotlinRxSwiftTCAなどを置き換えるものがGraphQLとそのキャッシュです。キャッシュが更新されたらUIに自動的に反映されるのは基本的な機能であると言ってよいでしょう。

いわゆるMVVMのViewModel相当の部分もGraphQLによって置き換えられます。宣言的UIのComponent1つに対してGraphQLのFragmentを1つ定義します。基本的に1:1の構成になるように作ります。
GraphQLのFragmentはコード生成によってモデルが生成されます。FragmentとComponentは1:1になるように作るので、まさにViewModelとして活用できます。うまく1:1の対応が作れない場合、バックエンドと協調してスキーマを改善していく必要があります。

1つの画面を複数のComponentを組み合わせて構成するように、GraphQLのQueryも複数のFragmentを組み合わせて1つのQueryを構成します。
AndroidやiOSなどで共通の用語がないので、このドキュメントではScreen Componentという用語を作ります。Screen Componentは1つの画面・画面遷移に伴う一番外側のComponent(またはそのラッパ)を指すことにします。
GraphQLでは3種類のOperation(Query/Mutation/Subscription)があります。Operation操作はApollo Clientのインスタンスにアクセスする必要があります。なので、Operation操作はScreen Componentに閉じ込めるのが良いでしょう。
Screen Componentではない各ComponentにはOperationの結果に含まれるFragmentを渡していき、Apollo Clientインスタンスに依存しない、Pureな状態を保つようにします。

Local cache

GraphQLにおいてローカルのキャッシュはSingle state of Truthのソースそのものです。ローカルのキャッシュはノーマライズされ、データの重複がないように正規化されます。詳しくは公式ブログの解説を見てください(重要)。

GraphQLと宣言的UIのアドバンテージを最大限得るためにはローカルキャッシュが最新の状態(≒MicroserviceのDBの状態と同じ、UIに表示したい内容)になるよう保たなければいけません。その状態を達成するためにはクライアントとバックエンドの協力が不可欠です。
たとえばクライアントがQueryで取得したデータをMutationで更新・削除する場合、更新・削除されたデータがMutationのレスポンスに含まれるようにする必要があります(参考 iOS, Android)。

スキーマ(バックエンド)は更新・削除したデータ、またはそのデータが含まれるリストにアクセスできるように設計します。クライアントはMutationのレスポンスにQueryで使っているのと同じFragmentを埋め込み、キャッシュを更新させます。

具体的に、ToDoアプリを作る場合を考えてみます。ToDoの新規追加のMutationでは更新後のToDo一覧にアクセスできるように、Todo.user.todosのようなアクセスができるようにしてやる必要があります。ToDoの更新のMutationでは更新後のToDoそのものにアクセスできる必要があります。

Query直下にあるリストはMutationのレスポンスによるキャッシュの自動更新が難しいため、可能であればあまり使わないほうがよいかもしれません。リストはQueryより個別のデータに紐付いているほうが使いやすい、ということです。たとえばQuery.todos(userID: ID!)という構造より、Query.viewer.todosという構造のほうがよいかもしれません。

ローカルキャッシュについて、Android/iOSともにインメモリキャッシュとSQLiteキャッシュの実装が用意されています。
(筆者はSQLiteキャッシュのようなPersistentなキャッシュを使った経験がありません😿)
キャッシュには独自の実装を用意し、タイプごとにインメモリキャッシュを使うかSQLiteキャッシュを使うかを切り替えるのがよいのではないかと考えます。
特にお客さまの操作なしに変更されることがない、お客さま自身のデータはSQLiteキャッシュに、それ以外の、API側で他者の操作によって更新される機会が多いものはインメモリキャッシュにするのがよいのではないでしょうか。
いずれにせよ、キャッシュのデータが参照され画面に反映される頻度の問題であり、最終的にはネットワーク経由で取得されたデータが画面に反映される場合がほとんどでしょう。

それぞれのライブラリにおいて、キャッシュキーの決定方法についても理解しておく必要があります。Android/iOSともに 型名:idフィールド の値がデフォルトでキャッシュキーになります。もし、我々の扱う型のなかでidフィールドが存在しない場合、もしくは主キー相当ではない場合はカスタマイズする必要があります。画面に表示される内容が意図通りではない場合、デバッガを使ってキャッシュストレージ内部の状態を直接確認したくなる場合もそれなりにあるため、自分たちでコントロールできるようにしておく必要があります。
AndroidではGraphQLのdirectiveを使う方法と、プログラム内で指定する方法の2通りがあります。iOSではプログラム内で指定する方法のみです。個人的にはプラットフォーム内でdirectiveを使う方法に統一できるのであれば、そのほうが好みです。

Recommendation for Client

クライアント側の設計についてのおすすめを書きます。

今のところ、Android/iOS用のGraphQLクライアントはApollo製のもののみが実用に足る印象です。もし有力そうなライブラリがあったら調査してみるので筆者までお知らせください。
Apolloのクライアントを利用する場合、公式ドキュメントは上から下まで読むとよいでしょう。

もし、私達が宣言的UIを採用しない場合、GraphQLで得られた結果を自分たちでコントロールした上で各UIに分配しなければならなくなるため、GraphQLを導入するアーキテクチャ上のアドバンテージは少ないでしょう。その場合、プロトコルとしてのGraphQL(≒ローカルキャッシュなしのGraphQL)だけで恩恵を受けられるかを熟慮するべきです。筆者としては既存の方法とくらべてさほど強いアドバンテージがないと考えます。
ComponentのPreviewなどでもGraphQLが生成する型は役立つとは思います。

クライアント側のアーキテクチャを考えるとき、宣言的UI+GraphQLの経験者をアーキテクトに入れるべきです。これにはReactなどのWebフロントエンドでの経験者でも構わないと思います。ただし、Webフロントエンドの開発の場合アプリケーションのライフサイクルを意識することが少ないため、その部分は各プラットフォームの経験者がイニシアチブを取る必要があるでしょう。

GraphQL周りのアセットは、ドキュメントに従い、スキーマについては *.json または *.graphqls を使い、OperationやFragmentは *.graphql を使うようにするのがよいでしょう。

Android/iOSの両方について、Componentの近く、可能であれば同じディレクトリにFragmentのファイルを配置するようにします(Fragment Colocation)。
命名にはなんらかの規則性をもたせます。たとえば CustomerEditComponent であれば CustomerEditComponentFragment とし、ファイル名もそれに即したものにします。ComponentとFragmentが1:1に紐づき、相互に参照しやすいことは非常に重要です。Android/iOSともに今はできないようですが、Componentと同一のファイル中にStringリテラルでFragmentを定義できるのであれば、その機能を利用するのがよいでしょう。

OperationはQuery/Mutation/Subscriptionの3種類がありますが、基本はQueryとMutationのみです。SubscriptionはいわゆるサーバからのPushを表し、WebSocketなどのリアルタイムコネクションが必要になるため、多くの場合スケーラブルなバックエンドを構築する難易度が高いため利用されないことが多いです。

Operationの利用は前述のScreen Component中に集めるようにします。子ComponentがGraphQLクライアントのインスタンスに関わることがないように留意します。

GraphQLのエンドポイントやクライアントは1アプリで1つに限定したほうが、よりよいアーキテクチャになります。なぜならば、ローカルキャッシュを複数持ってしまうとSingle source of truthとして不完全になってしまうためです。
もしエンドポイントを複数持たなければならなくなる場合は、極力同じデータが複数のスキーマ上に持たれないように努力し、同一種類のデータが含まれる場合は相互の操作においてローカルキャッシュの削除を適切に行うようにするほうがよいかもしれません。特に、単一のScreen Componentが複数のエンドポイントのデータを同時に扱うことは避けられるとよいでしょう。
複数のエンドポイントを持つ苦しみは圧倒的にクライアント側が辛い思いをします。対してバックエンド側はほぼノーペインで開発が進められるため、辛さの非対称性についてたいへん大きな差があると認識する必要があります。クライアント側とお客さまが苦しむのか、バックエンド側が苦しむのか、どちらか(あるいは両方?)になります。もし、複数のエンドポイントでスタートした場合でも、将来的には1つに統合されることを強く勧めます。
[筆者注釈: メルカリの開発スタイルはボトムアップ型が多く、CTOやVPoEによるアーキテクチャの指示といったトップダウン型の動きはほとんどありません。都合、全体が同じアーキテクチャを同時に志向しはじめるというのは難しいという背景があります。]

Android/iOSともに、fetchとwatchの2種類のOperation実行方法が用意されています。ドキュメントを見るとfetchを主に使うように見えますが、これは間違いです。
fetchは誤解を恐れずにざっくり表現すると、キャッシュから1回、ネットワークから1回、レスポンスが返ってきます。その後は更新されません。
対して、watchはキャッシュから1回、ネットワークから1回、その後別の場所でキャッシュが更新されたらその都度、レスポンスが返ってきます。
いずれの場合でも、レスポンスが1回しか返ってこない場合というのは稀ですので、それを念頭においてコードを書く必要があります。
宣言的UIと親和性が高いのはwatchです。fetchを使うのはpagingの時か、コメントに書けるくらい特別な事情がある場合だけと考えてよいでしょう。

fetch policyについて、キャッシュを無視するのは得策ではありません。基本的にはキャッシュ&ネットワークか、キャッシュelseネットワークを利用します。fetch policyを明示的に指定する場合、なぜそれを使うのか、常にコメントを書くようにするのがよいでしょう。
GraphQLのキャッシュに対する知識が浅い場合、特にリスト周りで意図しない挙動に見舞われることがあります。この時の安易な解決策としてネットワークオンリーを使ってしまいがちですが、これは割れ窓になりがちなのでぐっと我慢し根気よく解決したほうがよいでしょう。
データの編集画面などを作成するときはネットワークオンリーとするか、いったんキャッシュからのデータを編集不可で表示しつつ、ネットワークからのデータが到着してから編集可能にしたほうがよいかもしれません。pagingの実行も、実行元の結果がネットワークからのものかどうかを確認したほうがよいかもしれません。

Componentを構成するとき、1つのUI要素を表示するために複数のGraphQLのフィールドを組み合わせる必要がある場合、その処理をバックエンド側で肩代わりするcomputed fieldを導入することを検討してください。
たとえばメルカリの出品物に何個のコメントがついているかのバッジをUIに表示したいとします。しかしGraphQLスキーマにコメント数のフィールドが存在しない場合、クライアント側でコメントを全件取得し、その数をカウントするコードを書く必要があります。これは不便です。
クライアント側は不要なフィールドの取得をして、面倒なページングの処理も必要になります。バックエンド側もいくつかの不便を強いられます。個別のキャッシュが難しくなること、UIに対して過剰な本来必要な分以上のデータが取得されてしまうこと、これによりbackend主導の改善がより困難になることです。
バックエンド側はGraphQLのフィールドが要求されていることはわかっても、それがUI上でどのように使われているかは分析できません。どういったcomputed fieldが必要であるかは、基本的にはクライアント側のリクエストベースで検討されるべきです。

エラーハンドリングについて。これは難しい課題です。
QueryについてはUI側で指定可能な入力値を制御することで、そもそもGraphQLレベルでのエラーが返ってこないようにするのが楽です。APIがメンテナンス中であったり、AccessTokenがexpireした時などの、200以外が返ってくる場合(GraphQL的にはネットワークエラー)の制御は必要かもしれません。その場合も、Android/iOSともにinterceptorの仕組みがあるのでそのレイヤーで透過的に解決できる場合があるかもしれません。
MutationについてはShopifyのガイドApolloの一部のドキュメントではMutationの結果にエラー内容を含めるように書かれていますが、筆者としてはあまり一般的でないと思います。
入力値に対して、どの値がどんなエラーの原因になったかをクライアント側がハンドリングしやすいように返すのは困難です。これはGraphQLのみならずREST APIにも当てはまる課題です。API側で検知したエラーをどうクライアント側に戻し、ハンドリングし、UIに反映するかはクライアントとバックエンド双方協議の上、標準を考えておくのがよいでしょう。

ローカルキャッシュを読み書きするコードを書く必要が生じることは稀ですが、必要になることがあります。プロダクションコードで使わなくてもデバッグやテストの時にお世話になることもあります。プラットフォームによってはGradleで generateFragmentImplementations の値のセットなどが必要になる場合があるようなので、事前に用意・検証しておくとよいでしょう。

Mutationを使う場合、Optimistic updateが利用できるか検討しましょう。とは言っても使える場面はさほど多くはありません。Mutationを実行した時に返り値をクライアント側で計算できる場合、かつMutation後に画面遷移が発生しない場合などが利用可能な場面として当てはまるでしょう。たとえば、リスト中のアイテムをお気に入りに入れる、などの細かい操作に向いています。

APQ(Automatic Persisted Queries)の利用はクライアント・バックエンドで相談し利用するかしないか決めるとよいでしょう。
事故を避けるため、HTTPメソッドはGETではなくPOSTのままの利用でよいと思いますが、これはバックエンド側の決定に従えば問題ないでしょう。
メルカリのような利用数の大きなサービスを構成するにあたり、バックエンド側が想定していない複雑度のクエリが本番にリリースされることを避けるため、Persisted Queriesを絡めたなんらかのレビューシステムを構築してもよいかもしれません。GraphQLの表現力はコントロールされています。たとえばSQLのような実行計画を念入りに精査しなければいけないようなものではありません。しかし、それでもN+1クエリに代表されるような高負荷なリクエストのパターンは存在します(一般的にネストが深くなればなるほど、リストが絡めば絡むほど、負荷が二次関数的に大きくなる)。そして、GraphQLクエリがDBに対してどれほどの負担を与えるか、クライアント側から予測することは難しいです。
このような事故を防ぐために、バックエンド側からの協力や仕組みの構築依頼があった場合、優しく対応してあげてください。

AndroidとiOSではクライアントに絡んだテスト用のモックなどの実装があるようなので、この辺りの検討も事前に行っておくほうがよいでしょう。

main threadやバックグラウンド処理について、Android/iOSともにApolloクライアントが適切なスレッドにdispatchしてくれるようです。が、筆者はこの辺りをあまり理解していないので個別に実装を調べてみてください。

Android/iOSの実装にはWebでいうLocal Resolver, Client Resolver, Local-only fieldのような機能が存在しないようです。このため、GraphQLで管理しきれない状態、特にComponentをまたがるような状態のハンドリングについて頭を悩ませることになりそうです。
そのような状態が発生しうるか、それはどの程度なのか、その場合どうやって状態を管理し更新するかを設計段階で考えておけるとよいでしょう。
ReactでGraphQLを複数プロジェクトで使った経験では、なんらかのアイテムをコピーして新規アイテムを作るような操作(画面遷移前のComponentの入力値を引き継ぐようなもの)を実装したいときにこの問題が発生しがちです。それ以外のパターンでは1つのComponentに閉じない入力というのは少ないため、あまり問題になることはないと考えています。

@deprecated がついている要素をOperation中で参照していることを検知する仕組みを構築するべきです。ただ、いきなりErrorにしてしまうと運用上問題になる場合があるはずなので、Warningにとどめるか、コメントなどでsuppressできるようにしてもよいかもしれません。廃止される要素についてはバックエンド側とネゴっておき、いつまでに利用をやめられるか、利用廃止した後のバージョンを全お客さまに強制できるかを明確にし管理したほうがよいでしょう。

GraphQL first step

GraphQLの段階的導入について書きます。これまでに述べた話は1からアプリケーションを組むとしたら何がベストか、という話でした。

まず、GraphQLを導入する場合、Apollo製クライアントを使わない場合でも、Apolloクライアントの型生成機能は必ず導入するべきです。
バックエンドへのアクセスにGraphQL専用クライアントではない、既存のHTTPクライアントを使う場合でもです。まずは型安全であるというベネフィットを活用できることを目指します。

次に目指すのがComponentでのGraphQLが生成した型の利用です。GraphQLの生成した型をMVVMのVMとして使い、手書きのViewModelからの脱却を目指します。
また、これと同時にobservableな環境下でComponentが使われることを意識します。GraphQLではViewModelが何度も自動更新されることに対応できていた時の恩恵が大きいため、これを目指します。

次にGraphQLクライアントを導入しつつローカルキャッシュは利用しないネットワークオンリーで動作させ、最終的にはローカルキャッシュ有りのGraphQLクライアントを使うことを目指すのがよいでしょう。

今現状のAndroid/iOSのアーキテクチャをざっくりした伝聞でしか知らないため、もっと細かい進め方があるかと思います。
[筆者注釈: メルカリアプリのアーキテクチャのこと。最近リアーキテクチャしてスリムになったけどそれでも規模がでかい。一気に導入するのは無理があるよね…!]

Recommendation for Backend

バックエンドがGraphQL導入に対して行えることを考えていきます。

GraphQLは単なるプロトコルではなく、アーキテクチャとして活用するべきものです。そのために、クライアント側がどのようにGraphQLを利用し、どうすれば簡単かつ快適にUIを構築できるかを学ぶ必要があります。
基本的にはこのドキュメントで述べているローカルキャッシュの構築や更新に対して不便がないか、という点が満たされていれば80点は取れると考えてよいでしょう。
逆に、クライアント側のアーキテクチャに対して親和性の低いスキーマを作成すると、クライアントエンジニアをGraphQLファンにさせることは難しいですし、将来的な別のプロトコルへの揺り戻しは避けられないでしょう。
クライアントエンジニアをGraphQLファンにさせることは必須の目標です。

そのためにいくつかの原則を守ってやる必要があります。

Demystifying Cache Normalization – Apollo GraphQL Blogを読む。クライアントエンジニアはローカルキャッシュのことを強く意識しつつ開発しますので、バックエンドエンジニアも概要を理解する必要があります。

Mutationは更新される可能性があるすべてのフィールドにアクセスできる型を返さないといけない。
GraphQL schema basics

computed fieldを定義することをためらわないこと。どういうcomputed fieldを作るべきかは、バックエンドエンジニアが考えることは難しいでしょう。だから、クライアントエンジニアから相談があった場合、快く応じましょう。それが複雑な計算が要求されるとしても、必要ならばやるべきです。
なぜならば、バックエンド側が実装を拒んだ場合、単にクライアント側で同等の処理が実装されるだけだからです。しかも、それがどんなに重たい処理であろうとも、たった1つのUIを表示するために大量のリソースを消費するものだとしても、それを最適化する余地はあなたの手から離れてしまいます。computed fieldとして実装してあげれば、その処理が重かったらmemcachedに突っ込むなりなんなり、あなたが自由にチューニングすることができます。

ログを解析するときにどうやって楽をするべきかを考えておきます。GraphQLを利用する場合、HTTP的なリクエストログでは単一のエンドポイントへのアクセスになるためデバッグが難しくなります。
これに対応するため、HTTPのリクエストログの他にGraphQLとしてのリクエストログを出力したり、 /api/graphql?operationName=FooQuery&client=xxx&version=yyy のようにクエリーパラメータに集計用の情報を仕込んでもらうのも手です。

Operationの複雑度(complexityと呼ばれることが多い)を計測できるようにすること。
そのための仕組みは色々ありますが、自分が使うフレームワークなどで利用可能なものを使います。
GitHubのドキュメントが一番概念を理解しやすいと思うので参照するとよいでしょう。

クライアントはOperationごとにユニークな名前をつけてリクエストを送ってきます。
そこで、Operation名、Operation本文、プラットフォーム名(Android/iOS etc)、アプリケーションバージョン、計算したcomplexityの値などをログとして保管しておくと後で役に立ちます。
クライアント側が自分でcomplexityを計算することは難しいので、本番環境やdev環境でcomplexityが高すぎるものを自動的にクライアント側に共有するための仕組みも用意できるとよいかもしれません。
complexityはOperationを実行するに計算されるので、指標として扱いやすいです。
具体的に、リストを100件要求するようなクエリで、実際には10件の結果を返したとします。この時バックエンドはリクエストを容易に処理できました。しかし、将来的には90件の結果を返すことになり、その時バックエンドは処理をしきれなくなるかもしれません(リストの下に他のリストがネストしていたら、と考えてみてください)。処理にかかった実際の時間を計測することも大事ですが、将来的に問題になりうるクエリをcomplexityという尺度で予測できるようになるのは有意義です。

@deprecatedのディレクティブは積極的に活用します。なぜ廃止にするのか、代わりに使えるフィールドは何か、実際にいつ消したり、エラーを返したりするようにする予定なのかを書くとよいでしょう。
いきなりフィールドを消したり、型を変えたりするのはトラブルの元なので避けましょう。

可能であれば、どのプラットフォームのどのバージョンがどの型のどのフィールドにアクセスしているか、逆にアクセスがないかを集計できる仕掛けを構築しておくと、メルカリのような利用数が多いアプリケーションでは意思決定の助けになると思います。アクセスがないフィールドは消したり、破壊的変更をしても問題ありませんし、逆に頻度が高くアクセスされているフィールドは最適化の優先順位をあげることができるかもしれません。

バックエンドで採用するべき言語やアーキテクチャについて。
Kubernetesに対する知識が運用上必要になること、既存のMicroservicesと接続する必要があること、社内のミドルウェア(認証認可周り含む)の充実具合、などを考えるとバックエンドのエンジニアが管理・運用し、Goで実装するのがよいと思います。
上記を前提とする場合、クライアントやWebフロントエンドのエンジニアが運用を行うのは将来的なレスポンシビリティに不安があると考えます。
本ドキュメントはクライアントに関するrecommendなので、これ以上深くは言及しないものとします。
[筆者注釈: ここは完全にメルカリ社内の事情なので、それぞれの事情に合わせて考えてみてください。]

GraphQL schema placement

GraphQLのスキーマ管理をどうするべきかを考えます。

GraphQLスキーマの変更をどう管理するかは難しい課題です。バックエンドの開発をどうするかにも依存しますが、ここではスキーマファーストの場合を考えます。コードファースト(実装からSDLなどを生成する)だと議論の余地はあまりないでしょう。

バックエンドの開発がある程度進んだ後にクライアントの開発に着手する場合、スキーマはバックエンドのコードと近い場所にあるのがよいでしょう。
一般にGraphQLのスキーマを先に決定した後にDB周りやコードの実装をすすめるのは難易度が高いからです。DBの構成や実装がある程度できてくればGraphQLスキーマに破壊的変更が入ることは少なくなるでしょう。もちろん、クライアント側のためのスキーマの追加などは適宜行うことができます。
GraphQLスキーマを先に決定する場合は開発の途上で何回か破壊的変更が入り、クライアント側の実装が影響を受けることは覚悟する必要があります。

クライアントとバックエンドの開発を同時並行にすすめる場合、スキーマは両者が修正・提案ができる環境を整備するのがよいでしょう。変更についてはお互いのレビューを経てmergeするのがよいかもしれません。ただし、前述の通りDBの設計などを経てGraphQLスキーマに変更が入ることは覚悟する必要があります。

筆者のおすすめとしてはバックエンドの実装の後にUIの開発に取りかかれるのであれば、バックエンド寄りの管理手法でよいように思います。
しかし、GraphQLのFragmentから生成された型をComponentのViewModelとして使うのであれば、今までの開発手法と異なるタイミングで(おそらくはより早い)バックエンドの実装がdev環境に出てこないとクライアント側の開発がブロックされやすくなることは留意しておくべきだと思います。

この話題は複雑で、たとえばApollo Federationなどをバックエンド側で採用する場合、選択肢は狭まるかもしれません。開発体制やアーキテクチャによって答えは変わりやすいです。

結びの言葉

以上でドキュメントは終わりです。
社内向けのドキュメントであり、方向性を明確に述べるため、ちょっと強めの言葉を選んでいる箇所が多いですが、ご容赦ください。
読者の皆さんがGraphQLをやる/やっていく際には、このドキュメントから外れることや、間違いを見出すこともあるかと思います。その時は社内の仲間と協力してよりよいGraphQLライフを目指していただければと思います。

明日の記事は robert さんです。引き続きお楽しみください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加