Slide 1

Slide 1 text

#Offers_GraphQL実践LT GraphQL 「良さ」・「難しさ」 再探訪 〜スタディサプリにおける実例〜 内山高広 @highwide 2024.02.08 #Offers_GraphQL実践LT

Slide 2

Slide 2 text

#Offers_GraphQL実践LT Agenda | 00 01 02 03 04 About Me & Us GraphQL 「良さ」 再探訪 GraphQL 「難しさ」 再探訪 「良さ」「難しさ」を踏まえたスタディサプリでの実際 得られたインサイト 2

Slide 3

Slide 3 text

#Offers_GraphQL実践LT About Me & Us 00 3

Slide 4

Slide 4 text

#Offers_GraphQL実践LT こんにちは! ● 内山 高広 / @highwide ● スタディサプリでプロダクト基盤のWeb開発を行っています ● GraphQLを使っているチーム→使っているチーム→使っていないチー ム...と、スタディサプリの複数チームに携わってきました ○ Ruby on Rails/node.js/Go/Reactなどを書いてきました ● 長男(4歳)と次男(0歳)の子育てを楽しんでいます 4

Slide 5

Slide 5 text

#Offers_GraphQL実践LT 今日話す「スタディサプリ」について 5 スタディサプリブランドには様々 なサービスがありますが、今日は 学生の方に主に使っていただいて いるサービスについて話します。 https://studysapuri.jp/

Slide 6

Slide 6 text

#Offers_GraphQL実践LT GraphQL 「良さ」 再探訪 GraphQLを導入するメリットなんて、いろんなところで語られてますが改めて... 01 6

Slide 7

Slide 7 text

#Offers_GraphQL実践LT 出発地点: RESTful(あるいはRESTish)なアーキテク チャとの比較から ● ここではURLで表現される「リソース」をHTTPメソッドで操作するステー トレスなAPI設計をイメージします ○ ex: ■ GET /articles => 記事一覧 ■ GET /articles/{id} => 記事詳細 ■ POST /articles => 記事作成 7

Slide 8

Slide 8 text

#Offers_GraphQL実践LT sample: 記事一覧画面 8 Awesome Media Article 1 summary Article 2 summary Article 3 summary GET /articles

Slide 9

Slide 9 text

#Offers_GraphQL実践LT sample: 記事一覧画面 9 Awesome Media Article 1 summary Article 2 summary Article 3 summary GET /articles …みなさんが作ってるシステム、 本当にこんなシンプルなやつですか?

Slide 10

Slide 10 text

#Offers_GraphQL実践LT たとえば、こういう感じだったりしませんか? 10 Awesome Media Article 1 summary / author Articleに似せた記事広告 (システム的には別Entityとして表現されてる) 求人 情報 ユーザー名/前回ログイン日時 記事カテゴリ一覧(ユーザーの好みでパーソナライズされてる) 記事閲覧数 ランキング 注目の Author 天気 予報 写真 いろいろ 動画 いろいろ Article 2 これは、 「GET /articles」 ...で、いいのか?

Slide 11

Slide 11 text

#Offers_GraphQL実践LT 複数リソースで成り立つ画面/APIとの向き合い方あれこれ 11 ● 複数のリソースの中から代表的なリソースに着目する ○ 「複数リソースを取得する必要はあるが、あくまでこれは"記事一覧"だ」 ● 複数のリソースによって構成される1つの(メタ)リソースを見出す ○ 「これはDashboardというリソースだ」 ● リソース指向ではなく、画面指向やコンポーネント指向な命名をする ○ 「内部的なAPIルーティングも /api/toppage にしよう」 ● 単一リソースを返すシンプルなAPIをクライアントが必要に応じて複数呼ぶ ○ 「/articles と /authors と /videos と...を非同期で呼ぶ」 ○ ※ この発表では扱いきれないのですが、React Server Componentとの相性◎

Slide 12

Slide 12 text

#Offers_GraphQL実践LT その他、RESTfulな設計で直面する難しさ 12 ● 同一リソースを扱うがコンテキストによって微妙に異なる表示項目 ○ 導線や利用端末によって記事詳細ページで表示している情報が異なる仕様だが、 すべて「GET /articles/{id}」で取得している ■ 本来は不要なデータもまとめて取得してしまう "over-fetching" が起こっ ている ● リソース指向よりもユースケース指向で表現したくなるような更新系API ○ 「accountのactivationを行いたいが、"activationのPOST"として表現する よりも"accountのactivate"として表現したくなる」

Slide 13

Slide 13 text

#Offers_GraphQL実践LT これらの課題に対しての、GraphQLという解決策 13 クライアント サーバー GraphQL スキーマ Query: 必要なエンティティの必要なフィールドを 1つのリクエストですべてクエリする Mutation: 更新処理に必要なパラメータを渡し、必要 な返り値をクエリする (多くの場合) 単一のAPIエンドポイントで QueryやMutationを待ち受ける ex: /api/graphql

Slide 14

Slide 14 text

#Offers_GraphQL実践LT これらの課題に対しての、GraphQLという解決策 14 クライアント サーバー GraphQL スキーマ Query: 必要なエンティティの必要なフィールドを 1つのリクエストですべてクエリする Mutation: 更新処理に必要なパラメータを渡し、必要 な返り値をクエリする (多くの場合) 単一のAPIエンドポイントで QueryやMutationを待ち受ける ex: /api/graphql over-fetchingの心配もなく、 一画面の表示にに大量のリクエ ストを発行する必要もなくなる リソース指向の APIエンドポイント設計は不要に

Slide 15

Slide 15 text

#Offers_GraphQL実践LT 「何を返す必要があるのか」の知識をクライアントに寄せられる 15 Awesome Media Article 1 summary / author Articleに似せた記事広告 (システム的には別Entityとして表現されてる) 求人 情報 ユーザー名/前回ログイン日時 記事カテゴリ一覧(ユーザーの好みでパーソナライズされてる) 記事閲覧数 ランキング 注目の Author 天気 予報 写真 いろいろ 動画 いろいろ Article 2 ※ 仮に画面で表示したい項目が変 わっても、それが既にスキーマで定義 されたものならば、サーバサイドの開 発は不要 query TopPageQuery { article { title summary } adArticles { title summary } job { (以下略)

Slide 16

Slide 16 text

#Offers_GraphQL実践LT スキーマ駆動開発によるフロー効率の向上 16 クライアント サーバー GraphQL スキーマ 型の提供: スキーマが決まれば、型への変換ができるの で、サーバーの実装を待たず開発着手可能 resolverの実装: スキーマで定義されたエンティティや フィールドを実際に返せるような実装 ※ もちろん、クライアント-サーバー間のコントラクトとなるスキーマさえあればスキーマ駆 動開発はできるが、GraphQLスキーマのちょうど良い表現力や、クライアントの型に変換 するエコシステムの充実度合いは、開発体験をより良いものにしている (と、思う。最近だとTypeSpecのことはちょっと気になっている)

Slide 17

Slide 17 text

#Offers_GraphQL実践LT 02 17 GraphQL 「難しさ」 再探訪 「良さ」を理解することで、それに立脚した「難しさ」の解像度が上がるはず

Slide 18

Slide 18 text

#Offers_GraphQL実践LT 単一のエンドポイントに様々なユースケースのリクエストを 行うことによる、これまでの慣習の見直し 18 ● Observability(観測性) ○ SLI/SLOをHTTPエンドポイントごとに計測しているシステムは少なくないはず ○ すべてのQueryやMutationを受け付ける /api/graphql では、そのSuccess Rateを見ても、特定ユースケースの兆候はわからない ● Authorization(認可) ○ たとえばRailsのようなMVCフレームワークの場合、ルーティングに対応する個々 のControllerで認可を行うことが多く、そのためのライブラリも充実している ○ すべてをgraphql_controllerでハンドリングすることになったとき、どのように 認可を行うべきか

Slide 19

Slide 19 text

#Offers_GraphQL実践LT 1つのリクエストで複数のエラーが起こりうることによるエ ラーハンドリングの難しさ 19 ● 1つのリクエストで複数のエンティティの取得を行えるということは「Aの取得 には成功した」「Bの取得には失敗した」という部分成功/失敗が起こりうる ● この部分成功/失敗を鑑みたうえで、以下のような選択を迫られる ○ (これまで通り)HTTP status codeでエラーを表現する ○ response bodyのerrorsというfieldにエラー情報を詰め込む ○ エラーを示すエンティティをスキーマ上で定義する

Slide 20

Slide 20 text

#Offers_GraphQL実践LT N+1はRESTでも起こるが、なぜとりわけ 「GraphQLでは起こりやすい」と言われるのか 20 type Article { title: String! } スキーマ定義 「スキーマで定義したfiledをどのように 返すか」を実装するresolver Article: { title: () => { // ここでDBから取得したArticle 1レコードが持つ // titleカラムのデータを返す処理 } }

Slide 21

Slide 21 text

#Offers_GraphQL実践LT N+1はRESTでも起こるが、なぜとりわけ 「GraphQLでは起こりやすい」と言われるのか 21 type Article { title: String! } スキーマ定義 「スキーマで定義したfiledをどのように 返すか」を実装するresolver Article: { title: () => { // ここでDBから取得したArticle 1レコードが持つ // titleカラムのデータを返す処理 } } 素朴な実装をしていると、「Articleを複数一気に取得するようなQuery」が投げら れたとき、「Articleの1件取得」を何度も行ってしまう(N+1) 「単一のfieldをどのようにresolveするか」という実装と、「それが一気に複数回呼 ばれることがある」というユースケースの想定に思考のギャップが生まれやすい?

Slide 22

Slide 22 text

#Offers_GraphQL実践LT 任意のクエリをクライアントが投げられることへの配慮 22 ● スキーマ定義において、ネストした構造を作ることがで き、親-子-親という再帰できる構造も作りうる ○ ex: 記事の著者 / 著者が書いた記事一覧 ● 結果として、「特定の記事の、著者の記事一覧の、それぞ れの著者の、記事一覧の...」というクエリが書けてしまう ● クライアントが任意のクエリを投げられることで、ネスト があまりに深いクエリや、膨大なエンティティを取得しよ うとするクエリが、悪意を持つ者から投げられうる type Article { title: String! author: Author! } type Author { articles: [Article!]! }

Slide 23

Slide 23 text

#Offers_GraphQL実践LT 「良さ」 「難しさ」 を踏まえた スタディサプリでの実際 03 23

Slide 24

Slide 24 text

#Offers_GraphQL実践LT 前提: スタディサプリを取り巻く状況 24 ● 「スタディサプリ」と一口に言っても、高校生向け/中学生向け/小学生向けのそ れぞれのプロダクト、ToBサービスにおけるプロダクト、学校の先生向け機能、 コーチングプランのコーチ向け機能、社内向け機能...など、数多くの事業ドメイ ンとそれに対応するマイクロサービス群がある ● CTOや組織全体のリードアーキテクトはおらず、チームが裁量を持って個々の サービスについての意思決定を行うことが多い ● 今日話すスタディサプリの事例も、社内でGraphQLを扱う3チームからのエ ピソードを持ち寄っている

Slide 25

Slide 25 text

#Offers_GraphQL実践LT スキーマ駆動開発実例 25 figmaや GitHub issueで 画面仕様確認 Webフロントエンド/ iOS/Android/サーバサイドの チームメンバーでスキーマ定義 スキーマを満たす resolverの実装 得られた型をもとに 画面の実装

Slide 26

Slide 26 text

#Offers_GraphQL実践LT N+1はセオリー通りData Loaderによる対応が多い 26 ● DBアクセスやHTTP requestなどのN+1を起こされたくない処理をbatch化 するData Loaderという仕組みを導入することが多い ● 「GraphQLだと逆にN+1に気を遣うので案外混入させない」という声も。 ● 一方で、以下のようにスタンスが二分されるトピックであると最近知った ○ 「デフォルトではData Loaderを導入しない」派 (場合によってData Loaderを入れてN+1を減らすことがチューニング) ○ 「デフォルトでData Loaderを導入する」派 (場合によってData Loaderを外してオーバーヘッドを減らすことがチューニング)

Slide 27

Slide 27 text

#Offers_GraphQL実践LT 参考: Data Loaderに対するスタンスのアンケート 27 票数少なくてすみません...

Slide 28

Slide 28 text

#Offers_GraphQL実践LT 実践Observability: Datadogでのquery別tracking 28 各query/mutationを個別に Datadogで観察できるようにしている

Slide 29

Slide 29 text

#Offers_GraphQL実践LT 実践Observability: /api/graphql/{query名} 29 ● APIエンドポイント別にSLI/SLOを計測するという慣習を維持するため にクライアントは /api/graphql/{query名} を叩くという運用を行っ ているチームもいた ● このとき /api/graphql/ 以下のルーティングはすべて無視されて、実 際にリクエストのハンドリングを行うのは単一のgraphql controller ● 用途が限定されている場合においては、これで十分なケースもありそう

Slide 30

Slide 30 text

#Offers_GraphQL実践LT 実践エラーハンドリング 30 ● GraphQLの性質上、部分成功/失敗が起こり得ると書いたが、本当にそ の部分的な成功/失敗がユーザーにとって意味があるか(あるいは、シス テムが部分成功をサポートするコストメリットがあるか)というのは実は ユースケース次第。 ● スタディサプリ中学講座では、いわゆるHTTPステータスコードが500番 台になるようなものは"errors"に詰める / Mutationにおける400番 台になりそうなエラーは、Mutationの返り値型をErrorとのUnionにす る...という方針。

Slide 31

Slide 31 text

#Offers_GraphQL実践LT 実践エラーハンドリング: 参考記事 31 ※スタディサプリ参画者のquramyさんの記事や、今日のイベント登壇者であるqsonaさ ん(は、実は元スタディサプリのGraphQL推進者だった!)による反応記事に、このあたり のエラーハンドリングに対する考え方が言語化されている https://note.com/qsona/n/n45ebcb391d0b https://quramy.medium.com/graphql-error-下から見るか- 横から見るか-3924880be51f

Slide 32

Slide 32 text

#Offers_GraphQL実践LT あらかじめ登録されたオペレーション以外を弾く: Persisted Query 32 https://blog.studysapuri.jp/entry/2023/01/27/100000 ● 本番環境ではpersisted queryによってあら かじめ登録されたクエリのみを許容している ● クライアントコードにおける新たなクエリを persisted query化するコマンドや、漏れを検 知するGitHub Actionを利用 ● HTTP GETでpersisted queryに付与され るパラメーターが長過ぎるエラーに対応したこ とも

Slide 33

Slide 33 text

#Offers_GraphQL実践LT Internalなシステム連携におけるGraphQL API 33 ● schema stitching: ○ マイクロサービスの提供するスキーマを合成してapi-gatewayから提供 https://blog.studysapuri.jp/entry/2023/06/05/graphql_type_merging_1

Slide 34

Slide 34 text

#Offers_GraphQL実践LT 個人的な感想: 難しかった...(特にType Merging) 34 https://the-guild.dev/graphql/stitching/docs/approaches/type-merging

Slide 35

Slide 35 text

#Offers_GraphQL実践LT Internalなシステム連携におけるGraphQL API 35 ● 一方で、Go製のマイクロサービス群におけるschema stichingや Federationの採用を検討した別チームは、利用しているライブラリではう まく扱えなさそうということがわかったとのこと。 ● Ruby→RubyのInternalな通信において、OpenAPIよりもGraphQLを 好んで採用したチームも。

Slide 36

Slide 36 text

#Offers_GraphQL実践LT 得られたインサイト 04 36

Slide 37

Slide 37 text

#Offers_GraphQL実践LT スキーマ駆動開発ができる組織/関係性を築く 37 ● 「クライアントが必要なものをリクエストする」という特性上、「何がスキー マにあってほしいか」というスキーマ定義は、クライアント開発の一貫とし てやれそうに思う(ときもある) ● 一方で実際には、DB設計まで見越したGraphQLスキーマ設計をした方 がいいシーンもあり、結局クライアントとサーバサイドの開発者が膝を詰 めて議論する必要があった ● 理想的にはクライアントサイドとサーバサイドが同一チームでやれると良い し(もちろん、そういった区分すらなければ問題はない)、せめてロールの 違いを超えて議論がしやすい組織構造や関係性だと良さそう

Slide 38

Slide 38 text

#Offers_GraphQL実践LT エコシステムへの依存に自覚的になる 38 ● HTTPのハンドラをライブラリなしで書けるような言語もある中で GraphQLを選ぶということは(それなりに重い)依存先を1つ増やすこと ● 言語によってGraphQLエコシステムの発達度合いは異なるので、選択でき るライブラリとやりたいことのバランスは確認したい ○ ex: その言語の型システム上の恩恵は受けられそう?Schema first or DSL first? SchemaのFederationはできる?サポートしているdirectiveは? ● 「ビジネスロジック」やそれが守る「データ」に比べると、相対的にGraphQL エコシステムの方が廃れる可能性の方が高いので、ロジックとGraphQLの 密結合を避けた設計を意識したい

Slide 39

Slide 39 text

#Offers_GraphQL実践LT 引き出して初めてうれしいGraphQLパワー 39 ● 個人的にRESTの考え方は以前から好きでそれは今でも変わらない ● 一方でGraphQLという複雑系を持ち込むことで、考えることが減った り、開発体験が良くなったりというメリットを確かに体感できた ● ただし、(どんな技術でもそうだが)「GraphQLとりあえず入れてみた」だ けで得られるメリットは少なく、GraphQL特有の課題を乗り越える工夫 とチームの習熟が必要不可欠だった ● バランス取る発言が目立ったかもしれませんが、僕は今ではすっかり GraphQL好きです

Slide 40

Slide 40 text

#Offers_GraphQL実践LT ご清聴ありがとうございました! 40