こんにちは、マンガアプリチームのエンジニアの
id:kouki_dan です。『Inside GigaViewer for Apps』の連載12回目のこの記事は、出版社向けマンガビューワのアプリ版である「GigaViewer for Apps」(以下 GigaApps)のAPI通信にGraphQLをどのように活用しているか、採用経緯から開発の工夫までをご紹介します。
GigaApps以前のGraphQL検証
GigaAppsを作り始める前、マンガアプリチームではまだGraphQLを使っておらず、1画面1APIとして、RESTでの通信を行なっていました。当時、主に2つの課題がありました。1つはAPIリクエストごとにオブジェクトを手書きしていたためボイラープレートが多くなってしまっていたこと。もう1つは、APIで使われなくなったフィールドも後方互換性のために残り続けてしまうことでした。
これらの課題を解決するため、2020年12月にGraphQLの導入に向けた検証プロジェクトが立ち上がりました。期間は1ヶ月で、モバイルアプリのエンジニアとサーバーサイドのエンジニアが集まり、GraphQLが課題解決に有効かを確かめるのが目的です。主な検証内容はバックエンドのPerlでGraphQLを扱えるかどうかの技術調査や、モバイルアプリ側で利用するGraphQLクライアントのライブラリの選定・検証です。他にも、GraphQL導入後にモバイルアプリの設計がどのようになっていくかも考えて、方針を立てていました。
検証は、まず実装済みの画面を一つ選び、「GraphQLで再実装する」というテーマを設定することから始めました。そして1ヶ月をかけて、このテーマに対する技術調査や設計の検討をさまざまな角度から行いました。
この検証の結果、GraphQLは当初の課題を解決でき、モバイルアプリとサーバーサイド間の通信レイヤーとして十分実用的であると結論付けました。そして、これ以降マンガアプリにおけるAPIレイヤーの標準として採用されていくことになりました。この後に作られたGigaAppsでは最初からGraphQLを前提として設計を行なっていて、サーバーとの通信はほぼGraphQLで行なっています。
この検証の流れは、2022年のHatena Engineer Seminar #21 「GraphQL 活用編」でも詳しく話しているので興味があればご参照ください。
GigaAppsにおけるGraphQL
はてなのマンガアプリチームにおける、2021年以降の開発では、通信レイヤーはGraphQLが標準になっていました。既存のAPIを積極的にGraphQLで置き換えることはありませんでしたが、新規に作成するAPIはほぼすべてGraphQLで実装されています。GigaAppsの開発プロジェクト開始時点でそのような状況であったため、新しく作られたGigaAppsでも自然とGraphQLが採用されました。
Apolloを使った設計
クライアントのGraphQLライブラリにはApolloを使っています。ApolloはGigaAppsの設計の中心となっており、アプリの至るところで使われています。まず、APIリクエストが必要な機能を作るときは、GraphQLオペレーションを書くところからスタートします。これにより、対応するデータを表すデータモデルがApolloのコード生成機能により自動生成されます。
サーバーサイドから受け取ったデータを表示するだけの画面では、Apolloが自動生成したモデルをそのままビューのレイヤーまで受け渡して処理しています。多くの画面は、この方法で実現されていて、GraphQLオペレーションとビューを書くと画面が完成します。
一方、ビューワのように複雑なロジックを要する画面では、自動生成されたモデルを、アプリ内で独自に定義したモデルに詰め替えてから利用することもあります。
汎用的な処理は共通のコンポーネントで楽をする
モバイルアプリを作っていると、画面に関わらない共通の処理というものがよくあります。ページネーションはその典型的なもので、画面の一番下までスクロールした時に、次のアイテムの追加読み込みをするという要件はさまざまな画面で発生します。
GraphQLにおけるページネーションは Connection Modelとして表現されています。この形を簡単に扱えるように、汎用化したコンポーネントがiOS/Androidそれぞれに準備されています。このコンポーネントを使うことで、ページネーションのような典型的なロジックを最小限の追加コードで実装できるように工夫しています。
ノーマライズドキャッシュによる画面間での情報の伝播
データをGraphQLを通じてApolloで取得することにより、取得したデータはApolloのキャッシュレイヤーに入ります。このキャッシュはノーマライズドキャッシュと呼ばれ、複数のクエリを跨いでデータをキャッシュします。また、クエリ結果に含まれるデータが別のクエリにより更新された時に通知を受けてデータを更新する機能も含まれているため、ある画面でのクエリを他の画面に反映させることが簡単に実現できます。
これはアプリではよくあるユースケースで、たとえばビューワでお気に入りをした結果を1つ前の作品詳細に反映させたいことなどがあります。Apolloのキャッシュを使えるので、1つ前の画面に戻った時に毎回新しいデータを取り直す必要はありません。他の画面でのクエリ結果を、スタック上に読み込んでいる全ての画面に反映できるため、追加の処理を書かなくとも、自動的に最新の情報で画面が構築し直されます。
GraphQLでのスキーマ駆動による開発スタイル
普段の開発スタイルもGraphQLを中心に設計されています。はてなのマンガサービス開発は、組織図上ではアプリとバックエンドでチームが分かれています。 Slackなどでコミュニケーションは気軽に取れるようになっていますが、チームが違うとデイリースタンドアップで会話をする機会などが生まれにくくなってしまいます。
ここでのやり取りにもGraphQLが一役買っています。具体的には、スキーマ駆動で開発が進行していくため、早期に仕様が明確になり、アプリとバックエンドの2つのチームが独立して仕事を進められます。
普段の開発では、プロジェクトのキックオフ後は最初にアプリとバックエンド双方のエンジニアが集まりGraphQLスキーマを決定させます。この時の方法は、同期的に話していく場合や、PR上で非同期に行う場合のどちらもあります。案件次第で適切な方法をエンジニアが考えて取り組みますが、最初にスキーマを決定させる流れはどの案件でも同じです。
スキーマを最初に決めることで、お互いの認識齟齬を防ぎ、考慮できていなかった要件についても明確にすることができます。また、スキーマを確定させた後はアプリとバックエンドが分業できるようになり、別のチームにいてもそれぞれ独立して効率的に動くことができます。アプリエンジニアは、決まったスキーマをもとにクエリを書き、それをもとにモックオブジェクトを作ることでサーバーサイドの実装完了を待たずに開発を開始できます。
もちろんスキーマを決めて終わりではなく、大きめの機能を作っている間は定期的にアプリとバックエンドのエンジニアを含めて話す時間を設けたり、必要だったらSlackのハドルやミーティングなどは適宜行なっています。チームは分かれていますが、適切なコミュニケーションを取りつつ、効率的に進められている印象があります。
それぞれのメディアで共通のGraphQLスキーマ
マルチテナントアプリであるGigaAppsでは、複数のテナントでスキーマを共通にしています。マンガを提供するサービスにおいて、基本となるモデルは同じと考え、それをGraphQLスキーマを通してアプリで利用する形になっています。同じGraphQLスキーマをもとにアプリを作っているため、APIレイヤーを含めてコードベースを共通化できます。
とはいえ、メディアごとに提供されている機能は違います。メディアごとに有効な機能を定義できるようにしていて、それをGraphQLスキーマ上では、hasFeatureというカスタムディレクティブを使用して表現しています。これは『Inside GigaViewer for Apps』連載の第4回目である、様々なマンガアプリを素早く開発できる「GigaViewer for Apps」のしくみ バックエンド編でも詳しく説明されていますので、こちらもご参照ください。
キャッシュの利用とデータの鮮度をコントロール
マンガアプリでは、データが頻繁に更新されます。1日1回はコンテンツの更新がありますし、閲覧数やコメント数などは、高頻度で数が増加していきます。先ほどノーマライズドキャッシュの利点について述べましたが、キャッシュの積極的な利用と、データの鮮度維持はトレードオフの関係にあります。キャッシュを活用しようとすると、データの鮮度が犠牲になってしまうからです。マンガアプリでは最新のデータを提供するということも大事なので、サーバーサイドからアプリ側のキャッシュを制御できるように、クエリの発行時に以下のような工夫を加えています。
- GraphQLオペレーションを発行ごとに、オペレーションをキーとして、サーバーサイドからのHTTPのレスポンスに含まれているCache-Controlヘッダーの値をバリューとするデータを保存しておく
- GraphQLオペレーションの発行前に、オペレーションごとにキャッシュの有効期間を取得する
- キャッシュの有効期間が過ぎていた場合は、アプリケーション側でキャッシュ使用のキャッシュポリシーを選択していたとしても、サーバーに最新の値を問い合わせる
これらを、ApolloのInterceptorで実現しています。サーバーサイドから柔軟にアプリ側のクライアントキャッシュを制御できるようになり、負荷に応じたキャッシュ期間の変更や、メディアごとに要求されるデータの鮮度を満たした形でのキャッシュ利用を実現しています。
フィールドごとにTTLを指定できるようにするなどの方法も考えられますが、今回の用途ではオペレーションごとにサーバーサイド側でキャッシュの有効期間を指定する設計で十分なので、このような形で実装されました。
終わりに
ここまで、GigaViewer for AppsのAPIレイヤーを支えるGraphQLについて紹介してきました。GraphQLは機能開発の効率化や、モバイルアプリのチームとバックエンドの チームを繋ぎ、スキーマ駆動で独立して効率的に開発を行うためにも活用されています。マルチテナントとして開発しているGigaViewer for Appsとしても、GraphQL中心の設計で作られていることを紹介してきました。また、マンガアプリという特性の中、キャッシュをうまく使って最新の情報を提供するためにInterceptorを独自に実装している点もご紹介しました。
はてなでは、マンガサービス以外でもGrpahQLを活用している例が多くあります。このエントリでは、マンガアプリチームにおける活用について説明しました。
連載企画『Inside GigaViewer for Apps』では、多様なテーマで過去取り組んできた技術的チャレンジを紹介していきます。
id:kouki_dan
斉藤 洸紀(さいとう・こうき)。2019年1月入社。マンガチームでスマートフォンアプリケーションエンジニア/シニアエンジニアを務める。
Twitter: @kouki_dan
GitHub: kouki-dan
blog: Lento con forza