GraphQLが解決する問題とその先のユースケース
サーバーサイドからみたGraphQL Serverlss Meetup#19
2021/03/31 に行われた Serverlss Meetup#19 で上記のタイトルで登壇してきました。サーバーサイドの話をしようと思ったけどGraphQLの解決している話をしようと思ったらクライアントの事もかなりはいってしまったので記事のタイトルは変えました。
以下内容です。記事の最後に資料を書くにあたって参考になった資料のリンクを置いてます。
GraphQL and me
この1年書いたQiita記事
- GraphQLの特徴を分解する ~API インターフェース・Universal BFF・API Gateway~
- GraphQLはサーバーサイド実装のベストプラクティスとなるか
- GraphQLの全体像とWebApp開発のこれから
今回話す事
- そもそもGraphQLはなんで作られたのか、何を解決しようとしているのか
- その後生まれて来ているユースケース
- GraphQL導入の検討
GraphQLとは
- Facebook発のWebAPI仕様, 2015年にOSS化
- GraphQL自体はspec(仕様)
- GraphQL over HTTP が主流 (HTTP/1.1)
- 通信設計手法がgRPCに似てる
- アプリケーション層のプロトコル(not トランスポート層)
- GraphQL Clientと GraphQL Server間のプロトコル
- Client側も含めてデザインされているWebAPIの仕様
- RESTとの違い
GraphQL(の前身)が作られた時の問題意識
2012年頃のFacebook, GraphQL誕生の話(以前、日本語字幕投稿しました🙌)
背景
- モバイルクライアントの急増(iPhone 5~)
- ネットワークトラフィックの増大
- アプリのWebViewからネイティブ化
問題意識
- 前提としては、変化し続けるアプリケーションを支えるAPIはどのようにあるべきか
- あちこちにあるREST APIを繋ぎ合わせるのが大変
- 作りたいUIを実現するために何度もRESTを叩く必要がある
- 様々なユースケース, 多種クライアントに耐えうるAPI設計にしなければいけない
- モバイルクライアントのリソース最小化(CPU, メモリ, ネットワーク, バッテリー)したい
- 頻繁に起こるクラサバ間での状態の操作と同期を楽にしたい
- RESTでは実現がむずかしい
API インターフェース
WebAPI(RESTish)の問題意識
クライアント側
- 実装コスト
- 増大するエンドポイントに対応するのが大変
- エンドポイント毎の仕様に対応するのが大変
- クライアント任せな状態管理
- ハードウェアの使用リソース
- 増大するリクエスト数(CPU, メモリ, ネットワークを使用する)
- 利用されない受信データを受け取る事による無駄な通信コスト
サーバーサイド側
サーバーサイドの問題意識
- ユースケース毎のAPIを煩わしさ
- クライアント都合でサーバーサイド実装を変える煩わしさ
- APIドキュメントの煩わしさ
- コミュニケーションコスト増
傾向として
サーバーサイド都合なAPIになりがち
WebAPIの設計
“APIをコンシューマにとって理解しやすく使いやすいものにするには、コンシューマの視点に立って設計しなければならない。”
わかる!しかし!
サーバーサイド側の実装都合
現実問題として
- ドメインロジックを実装するときにAPIのユースケースを同時に考えるのは大変
- ドメインロジックのコンテキストは可能な限り狭いほうが良い
- 負荷によってはロジックを分離したい
- 将来に渡って利用可能なユースケースを実現するのは難しい
傾向として
APIは細分化され増えていく → クライアントにとっては使いづらくなる…
Presentation-Domain Separation しましょう
Presentation-Domain Seperation
APIのBackend For Frontend(BFF)としてのGraphQL
- GraphQLはBackend For Frontend(BFF)パターンを受け継ぐ
- クライアントにとってはBFF
- クライアント都合のAPI
- サーバーにとってはAPI Aggregator
- サーバーはバックエンド都合のAPI
GraphQLが解決した問題
クライアント
- 単一のエンドポイント、単一の仕様
- 利用可能な全ての状態がわかる
サーバーサイド
- ドメインAPIから微細なユースケースの対応を減らせる
- クライアント都合の変更が減る, API仕様を伝えるコミュニケーションが減る
- Schemaがドキュメントになるためドキュメント作業がなくなる
- サーバーサイド都合で自由にAPIを作成・統合・撤廃出来る
- 1フィールド毎, 1タイプ毎に開発言語や実行環境、データストアを適材適所で選択出来るためクラウド/Serverless/Microservicesのアーキテクチャのポテンシャルを引き出しやすい
- データソースを切り替えるのが簡単で状況に応じて変更しやすい
サーバサイド実装の切り替えのユースケース
- 特定の1つのフィールドが計算コストが高くキャッシュも出来ない -> そこだけGo, Rustで置き換えする
- RDBのJSON型にデータを入れていたがオブジェクト型DBに変更する
- 長時間処理が掛かる操作をRESTからメッセージキューを使ったイベントソーシング, 非同期リクエスト-レスポンスパターンで実装し直す
- あまり利用されないが膨大な量のデータがDBに入っている
GraphQLを前段に置くことでクライアントに一切影響なく、サーバーサイドの構成を変更することが出来る。また逆説的に後から変更が自由に容易に行える事で、実装時に恒久的な選択にプレッシャーを感じることなく現在のコンテキストに合う技術を自信を持って選択出来る。サーバーサイドの人も技術的負債を作りたくないというプレッシャーはあるので、変更しやすくしておくことで適切なタイミングで返済していける土壌を作っておく事が心理的安全性につながります。
情報アーキテクチャ的な観点
- 利用可能なデータが発見しやすい・利用しやすい
- クライアントに提供しているデータが一元化される
- ユーザーにとって見せるか見せるべきではないかの監査を1箇所で行う事で監査可しやすい
- RESTishなAPIでは全てのエンドポイントで同一レベルの監査が必要だった
GraphQLが持ち込むパラダイムシフト
一言でいうなら、
APIから特定のユースケース・コンテキストを排除することでクライアント・サーバーサイド双方の柔軟性を向上させた
APIのが特定のユースケース・コンテキストを持たない事で
- クライアントは利用出来る情報を最大限利用して柔軟なUIを構築し、ユーザーに最大の価値を届ける事が出来る
- サーバーサイドはバックエンド都合でドメインロジックを構築出来る
逆に言うとGraphQLを使うとRESTはコンテキストフルなAPI(提供する前にレスポンス表現を固定する必要がある)で、それがクライアントのUI表現をいかに固定化させてきたか、バックエンドのアーキテクチャを固定化させてきたかに気づくことになると思います。
クライアントリソースの軽減
クライアントの問題意識
特にモバイルクライアント
問題意識
- ネットワークやバッテリーの使用を可能な限り抑えたい
- 劣悪な環境にあるクライアントでもサービスを利用してもらいたい
- ユーザー規模が多くなると通信コストだけで莫大なコストが掛かる
解決手段
- アプリケーションから発生するリクエストを可能な限り減らす
- 利用されない無駄なデータを減らす
GraphQLのアプローチ
- 1リクエストで取得出来るデータを増やす
- レスポンスデータの有効率を上げる
方法論
- クライアントに必要なデータを宣言させる(宣言的データフェッチ)
- 利用可能なデータは全て先に知らせる(Schema Introspection)
GraphQLのAPIデザイン
- 1リクエストで多くの情報を取得出来るように設計されている
- 1リクエストに複数のクエリを含めることが出来る(Batch Query)
- 1リクエストで段階的にレスポンスを受け取るRFCが進行中(@defer, @stream)
- もちろんクライアント都合で複数リクエストにしても良い
- GraphQL Clientを使うとGraphQLの価値を最大限引き出せるが、素朴なHTTPクライアントからHTTPのAPIとしてRESTと同じような感覚で使うことも出来る
GraphQLが解決した問題
- リクエスト数を減らしながら利用したいデータを効率よく取得出来るようになった
- サービス全体で無駄なデータ通信が減らす事が出来た
クライアント-サーバーの状態同期
クラサバの状態同期の問題意識
状態とは時間経過に伴い変化するデータ
前提
- RESTishなAPIはクライアントとの状態同期に関しては特に指定がない
- クライアント-サーバー間の状態同期クライアント任せ
- 特にネイティブクライアント, SPAアプリはクラサバ状態同期の実装が大変
- フロントエンドでFlux系の状態管理ツールが出てきたりで頑張ってきた
サービスが大きくなると
状態管理のコードが肥大化し管理不能になるとフロントエンドの開発効率が急激に落ちる
Flux系(Redux)の状態管理手法
- 状態を持つ信頼できる唯一リソース(Single Source of Truth)
- Immutable State
- 操作は必ずActionから
- (これまでの用途は主にサーバーから取得したデータのキャッシュ管理だった?)
GraphQL Clientによる状態管理
- 状態管理の考え方はFlux系と同じ
- クライアント-サーバー間の状態同期に特化 (Server State)
- 操作はQuery, Mutationで行う
- GraphQL Clientが状態をキャッシュとして保持している
GraphQLはクライアントも含めてデザインされている
GraphQL Clientの実装
Managing local state
Interacting with local data in Apollo Client
- GraphQLクライアントが状態を管理している
- 主要なGraphQLクライアント, Apollo Client, Relay, urql, は正規化されたキャッシュを自動で行う機構を持つ(Normalized Cache)
- デフォルトでは
id
と__typename
を利用してKeyとする - 異なる経路から取得されたデータでも同一のkeyであればキャッシュが更新される
- データの更新時(Mutation)の場合も、同一のkeyを持つオブジェクトを含むレスポンスとして受け取る事でクライアント側だけで同期が可能
- 変更操作を伴う場合、MutationもGraphQLで実装する必要がある
GraphQLが解決した問題
状態管理の多くの作業をGraphQLクライアントが自動化してくれるため開発者がクラサバ間の状態管理をRedux等で設計・実装する必要がなくなった(サーバーステートはGraphQL Clientが行うためReduxを使わなくなるが、クライアント側で発生する状態管理にはこれからもReduxを使うケースはある)
利用の中で生まれて来たユースケース
マルチクライアント
アイディア
- 異種クライアントを追加しやすい
- ユーザー毎のUI(新レイアウト, 玄人向け, 高齢者向け)を作りやすい
- 同じサービスでも異なったUIを複数展開しやすい
- Notion, Asanaのような柔軟なUIはGraphQLなら作れそう
- GraphQL APIを提供するところまでがサービス提供、UIは自分で作ってね、みたいなサービスが出てきても面白い
APIがユースケースを持たないからこそ様々なUIのユースケースを考えられる
API層のAnti-Corruption Layerとして導入する
大規模アーキテクチャ変更のお供に
ビッグバンリニューアル💥
進化的アーキテクチャ
- インクリメンタルな変更
- 適度な結合
- 適応度関数
どこから始めるか
- 最も簡単なことを最初に
- 最も価値のあるものを最初に
達成すべきこと
機能要件を最速で実現しながらアーキテクチャも段階的に変更する
手順
- レガシーな単体のバックエンド、サーバーがHTMLを返す
- GraphQLへ配信するためのAPIがなければ実装する(腐敗防止層)
- API層としてGraphQLを導入する
- GraphQLを利用するフロントエンドを作成する
- この際、可能であればコンポーネント単位、ページ単位でインクリメンタルにリリースする
- 完全にフロントが切り替わったタイミングで旧フロントを廃止する
- 必要に応じて(!), 順次新バックエンドに切り替える
- こちらも段階的に切り替える
- 切り替えるメリットがあるものから行う
- 切り替えるメリットがないのであればそのままにする
※ 4, 5は並行作業可能
GraphQLをAPI層として入れる事で
- フロントとバックの結合を分離出来る
- それぞれ異なったタイミングでのリリースを可能にする
- まずは機能要件から優先、バックエンドの整備や切り替えは後から
- 特に中規模になってきたアプリケーションの改修時に重宝する
CQRS
- そもそもGraphQLはQuery, MutationとCQRSでデザインされている
- エンタープライズなデータ仮想化基盤と相性が良い
- Denodo(データ仮想化技術)がGraphQL APIを対応してきている
-
非同期リクエスト-レスポンスパターンの実装例
- Shopify BulkOperation(従来のstatusを返すURLを返すパターン)
- 3 Factor App(Subscriptionでサーバープッシュで返すパターン)
Realtime API with Subscription
- Subscriptionで状態をサーバープッシュで受け取る
- Clubhouseのようなアプリを作りやすい
- ユーザーのログイン状態, 入室しているRoom, 最終ログイン時刻, チャット, リアクション
- ユーザー自身の操作よりもサーバー側での状態変更が多い場合に有用
- Firestoreを使うようなユースケース
巨人の肩に乗る
- 活発に議論されている場所は課題への解決策が出てくる可能性が高い
- 後発の技術はすでにある技術の課題を解決している可能性が高い
現在活発に議論されている場所
- フロントエンド: Web基準, React.js/Vue.js, SPA, etc…
- GraphQL
- クラウド, Serverless
GraphQLの導入するタイミング
GraphQLが向いてるケース
- 変化・進化し続けるアプリケーション。変化しない、作り切りのアプリの場合はRESTで十分かも
- 複雑性の高いアプリケーション
フロントエンド視点
- クラサバ間での状態同期が必要かどうか
- 必要であれば最初期からでも導入するべき
- 特にクライアントからの状態更新(Mutation)が多い
- 逆にサーバーから一方的にデータが送られるSSR, SSG, ReadヘビーなサイトはRESTとSWRで十分かも
- モバイルクライアント(低速、劣悪環境)で利用されるかどうか
- 複数種類のクライアントがある, または作る予定がある
- エンドポイントが多すぎて開発体験が悪い
サーバーサイド視点
- APIデザイン都合
- クライアントのユースケースによるAPI変更が多い
- Microservicesを採用している
- (API ドキュメントを書きたくない)
- 大規模なアーキテクチャ変更が必要
プロダクトの規模
- 最初期から中期、エンタープライズ規模までどの規模でも利用可能
- 最初期はフロントエンドの状態管理都合が多い
- 中期はアーキテクチャ変更のお供に
- エンタープライズ規模ではCQRS, Microservicesのエンドポイントとして
- 段階的な導入が望ましい
- TwitterやAirbnb, GitHubでも全体の数%から適用中
GraphQLに向かない事
- WebAPIであること, HTTPの制約を受ける
- 巨大なデータ転送には向いてない
- オブジェクト型の構造データが向いている
- 非構造型データ、テーブル型のデータは向いてない
おわりに
冒頭であげた今年1年書いてきた記事をさらにまとめた仕上がりで1年のGraphQLの調査の成果を発表しました。また機会があれば別の視点からGraphQLをお話したいと思います。
P.S. Zennでの初投稿の実績を解除しました🙌
[PR] Software Design 2021年8月号にGraphQLの特集を寄稿しました
Software Design 2021年8月号にて、GraphQLの特徴や解決している問題、そしてGraphQLの基本動作原理(クエリ言語、型システム、実行エンジン)について29ページに渡って特集を寄稿しました。 GraphQLを理解するための手がかりになればと思って書いたので興味ある方はぜひ読んでみてください!
References
- APIの設計 - Amazon
- 進化的アーキテクチャ - Amazon
- https://github.com/graphql/graphql-spec
- https://github.com/graphql/graphql-js
- https://github.com/graphql/graphql-over-http
- https://github.com/graphql/graphql-spec/tree/main/rfcs
- GraphQL: The Documentary - Youtube
- Designing GraphQL Mutations - Apollo Blog
- How Facebook organizes their GraphQL code
- New features in GraphQL: Batch, defer, stream, live, and subscribe
- Best Practices for GraphQL Clients
- Demystifying Cache Normalization
- https://redux.js.org/
- https://swr.vercel.app/
Discussion