はじめに

この記事は [CyberAgent Developers Advent Calendar 2021 – Adventar]  21 日目の記事です。他にも面白い記事がたくさん紹介されているので、ぜひご覧ください。

はじめまして。株式会社MG-DX でバックエンドエンジニアをしています、齋藤(@ranksai)です。MG-DX では医療・薬局の DX 化を掲げ、オンライン診療や処方せん事前送信、オンライン服薬指導を行える薬急便というサービスを提供しています。最近 MG-DX ではある課題を解決するために GraphQL を導入しました。そこでこの記事では GraphQL を導入した話をバックエンド側からの目線でしていこうと思います。

既存開発環境で発生していた課題と問題

図1 MG-DX 開発フロー

MG-DX ではスクラムで開発を進めていて、開発案件から画面デザインを出し、その仮デザインを元にキックオフをおこなっています。図1 が開発フローを表した図です。案件があると画面デザイン、キックオフと続きます。その後キックオフで決まった詳細仕様からフロントエンド、バックエンドでそれぞれ実装を進めます。その後決められたテスト期間でテストを行い、問題なければリリースされます。このフローを 2 – 3 週間で繰り返しています。またバックエンド設計はマイクロサービスで運用していて、それぞれで API を実装してあります。APIの管理は OpenAPI で管理しているため、フロントエンドとバックエンド間でそこを担保にしつつ開発を進めています。

このような開発フローを続けているとバックエンド側で以下のような課題や問題がありました。

  • 細かい API 修正をすると影響がフロントエンドまで伝播してしまう
  • ボトムアップな開発になることがある(MG-DX の開発ではトップダウンが良い)
  • 開発進行のズレが生じてしまう
  • 画面意識が薄れてしまう

まず 1 つ目に挙げた影響がフロントエンドまで伝播してしまうことです。これは OpenAPI で管理しているのでそういうものだとは思いますが、バックエンド都合での細かい API 修正をするたびに OpenAPI の読み込みをし直さないといけないです。レスポンスを修正したり、パラメータを追加したりなどの影響をフロントエンド側がもろに受けることになります。修正内容によってはバックエンド都合のものが多いのでその度に読み込みし直すのは正直面倒です。

ボトムアップな開発になる・開発進行のズレが生じてしまうのはバックエンド側で OpenAPI の追加が遅れてしまい、フロントエンドとバックエンドの開発進行にずれが生じてしまうことです。 OpenAPI の管理はバックエンド側が行っているのですが、更新が遅れてしまうとフロントエンドでジェネレートすることができず、開発にズレが生じてしまいます。ズレが生じたことで、その後にあるテストや QA にまでスケジュールの影響を与えてしまうことがありました。

最後にバックエンド側の画面意識についてです。処理が複雑になると複数のマイクロサービスの API を様々に呼び出すため、単一の API がどの画面を生成するのに呼び出しているのか分からなくなることが発生しました。またそれによって実際の画面がどうなっているのかという意識が薄れてしまい、今どのような画面になっているのかわからない状態になることがありました。

GraphQL 導入のメリデメ

上で挙げたような課題・問題があり、MG-DX では GraphQL の導入を検討しました。GraphQL を導入する上でのメリットデメリットはいくつかありますが、簡単にまとめると以下のようになります。

  • メリット
    • 挙げた課題・問題は解決
    • GraphQL によるクライアント側の恩恵
  • デメリット
    • バックエンド側の開発工数増加
    • 学習コスト
    • BFF として挟むことによる遅延
    • DB のアクセス数増加

メリットとしては最初に挙げた課題や問題が解決することとクライアント側のGraphQLにおける恩恵があると思います。GraphQL を BFF として間に挟むため、バックエンドの API 変更をフロントエンド側が意識する必要がなくなり、変更による影響が伝播しなくなります。これによって API に手を加えやすくなります。またGraphQL のスキーマベースで開発を進めることでトップダウンな開発や開発スケジュールのズレが減ります。またスキーマ作成時に画面を意識しながら行うため、バックエンド側の画面意識が高まります。

クライアント側の恩恵として GraphQL を入れることで画面(コンポーネント)ごとに 1 対 1 対応のクエリを書くことができます。これによって欲しいリソースを欲しい状態で受け取ることができるようになります。また GraphQL はキャッシュ周りが強力であることが知られています。この辺の恩恵を受けることができるのも良いと思います。

デメリットとしてはまず開発工数が増えることです。API の変更の場合はさほど工数は増えませんが、新規追加をする場合は通常のマイクロサービスにおいて追加するに加えて GraphQL サーバーの実装もすることになります。また新規導入の学習コストもあります。プロダクトとして新規で扱う技術だったため、この辺の学習や実装パターンなどもコストになりえます。さらに MG-DX では API を各マイクロサービスで提供しているため GraphQL を導入する際には BFF として実装すると思いますが、このように BFF としてマイクロサービスとの間に 1 つホップが入ることになるため、その分の遅延が当然発生します。また DB のアクセスも既存のマイクロサービスで提供しているときよりも増えることが想定されます。

図2 REST API と GraphQL を用いたときのリクエスト参考

このようなメリット、デメリットがあることを念頭におきつつ、MG-DX では導入することに決めました。その理由としてはメリットとして上げた課題、問題の解決が大きいこと、またフロントエンドから複数 API を実行するより GraphQL を用いてそこを 1 回で済ませることで遅延が気にならないということがありました。図 2 では既存の REST 環境と GraphQL 移行後の環境におけるリクエストの参考図です。既存環境では欲しいリソースごとに複数サービスの API を実行する必要がありましたが、GraphQL にすることで 1 リクエストで取得することが可能になります。それによって GraphQL にした方が UX として見ると良くことがわかりました。

ただ今回の GraphQL 導入では全ての API を GraphQL で挟むのではなく、新規機能サービスにおいて GraphQL で挟むようにして導入することになりました。これは既存の API 全てを対応すると工数がかなりかかってしまうこと、新規機能サービスが外部 API を実行する必要があったためそこをうまく隠蔽できることがありました。

図3 新規機能 API シーケンシャル

シーケンシャル図が図 2 になります。結果として実装したシーケンシャルでは GraphQL で外部 API を呼ぶことはなかったですが、サービスで外部 API を隠蔽することで意識せずに開発することができました。また外部 API のトークン管理もサービスで行うことでフロントエンドの関心から離しています。

GraphQL サーバー実装にあたって

MG-DX のマイクロサービス群は Go で記述されているため、GraphQL サーバーも Go で書けると良いなと思っていました。そこでスキーマベースで開発できそうなライブラリをいくつか選定し gqlgen を用いて実装することに決めました。gqlgen はスキーマからコードを生成することができ、GraphQL のスキーマから構造体を生成してくれるため Go の型付けによってクライアントとの通信に用いるオブジェクトを担保することができます。このスキーマベースの開発は最初に挙げていた開発のズレを減らしていたり、画面意識をもたせることに活躍しています。

ここからは個人的に良かったと思う点を紹介したいと思います。gqlgen を用いた GraphQL サーバーの実装は他にも色々記事があるためそちらを参考にしてみてください。

スキーマの記述ルールを決めた

それはそうだろと思われそうですが、やってよかったこととしてGraphQLのスキーマの記述ルールを決めたことがあります。記述ルールといっても文法的な話ではなく、MG-DX 独自のこういう場合はこういう定義をしようといったものやこういう命名をしようというルールです。これによって基本的に誰が書いても同じようなスキーマを作成することができます。属人化も減らすことができますし、レビューもスムーズになります。ただそれが絶対正しいというわけではないです。具体的に決めたルールの一部をいくつか紹介します。

  • 型は基本的に NULL 許容しない(! をつける)
  • Mutation では prefix を操作に応じて指定した単語にする
  • それぞれの prefix に応じて戻り値の型が決まる
  • ネストされたオブジェクトの場合は操作に応じて戻り値の型が決まる
  • Queryではprefix を使用しない
    • リソース名をそのまま Query 名にする

どれも基本的なルールですが、これを設定するだけでもスキーマ記述の可読性が上がったり属人性がなくなったりします。型は基本的に NULL 許容をしないようにしています。NULL を許容するときはその NULL 自体に意味があるときだけにします。Mutation の操作では指定した prefix をつけることでどのような操作なのかわかるようにしています。またその prefix に応じて戻り値の型を決めることで大体はその中でスキーマを作成することができます。基本的には Object! を返すのですが、削除した場合やネストされたオブジェクトの生成などで型を変えています。具体的にはネストされたオブジェクトの生成時は親オブジェクトを返すようにするといった形です。また Query の操作では無駄な prefix をつけずに何のリソースなのかわかるようにしています。

ページネーション

GraphQL でページネーションの実装は公式でいくつか紹介されています。(執筆時で 3 パターン)基本的に GraphQL のページネーションでは Cursol Pagination と Offset Pagination があります。 公式では Cursol を用いたページネーションが強力であるという書き方がされていますが、無理に Cursol 型を採用する必要はないと思っています。時系列データなどのページネーションではその効果を非常に発揮すると思いますが、単純な検索においては Offset を用いたページネーションで問題ないと思います。MG-DX でも Relay-Style Cursor Pagination は使わず、Offset Pagination を使用しています。これは無理に採用する必要がないと感じたのと、既存のマイクロサービスで提供している API や外部 API との兼ね合いからこのようになりました。

エラー定義

GraphQLでは操作が失敗したかどうかを HTTP ステータスコードから判断するのがその性質上非常に困難なため、個別のエラーコードを提供するのが望ましいです。エラーコードの考え方は GraphQL に限ったことではなく REST API などでも使われると思います。エラーコードの定義は GraphQL のスキーマに組み込みそれを返すようにします。そうすることでどこで何のエラーが起きたかわかるようになり、フロントエンド側で次の遷移制御を行えるようになります。このようなエラーコード構造をスキーマで表現しようと思うと以下の 2 パターンがあると思います。

  • エラーコードを全体の共通のものとしてスキーマ上で定義する
  • Mutation や Query など操作ごとにエラーを定義する

どちらの手法もエラーコードといを定義することには変わりないです。GraphQL のスキーマでエラー型を定義し、そこに一つひとつエラーを定義します。エラーの型が1つであるため、そのエラーが複数の場所で発生する場合は、同じ内容のエラーでも違うエラーコードを生成する必要があります。
例えばAサービスのトークン有効切れとBサービスのトークン有効切れの違いがわかりやすいと思います。
どちらも有効期限切れなため、同じエラーコードでも良さそうですが、サービスが違うためエラーコードが a/id-token-expired b/id-token-expired のように複数設定する必要があります。(サービスごとに共通の動作を期待するのであれば、異なるサービスでも同じエラーコードで良いと思います。)このようにすることでクライアント側ではこのコードが返ってきたらサービスでのトークン期限切れということがわかり、次に行ってほしい操作をユーザーに促すことができます。(トークンの有効期限切れの場合は再ログインを促すなど)この辺の実装は GraphQL アドベントカレンダーに似た記事を書いたので良かったら参考にしてください。

まとめ

  • GraphQL を導入したことで課題や問題が解決した
  • 既存環境からの移行はかなり大変
    • REST 等の環境から移行工数が非常に大きい
  • 導入してみて
    • 導入コスト(新規プロダクトなら)低め・移行コスト高め
    • UXは上がった
    • 数十 ms ~ 数百ms くらいの遅延が入る
      • BFF として間に入ったことによる
      • 複数回様々な API をフロントエンドから実行するよりは早い

最後に

この記事ではGraphQLの導入契機や導入時に気をつけたこと、導入した結果をお伝えしました。まだ本番リリースされていないため、運用して結果どうなったのかはまだこれからです。またどう実装したかなどの具体的な実装まで触れることができなかったため、その辺が詳しく知りたいという方がいらっしゃいましたら、また別の機会でお話できればと思います。

MG-DX では様々な技術を通して医療機関や薬局の DX を図っていきたいと考えています。もし興味のある方がいらっしゃいましたらぜひお声がけください。

最後までご覧いただきありがとうございました。


2021/12/27 追記

株式会社サイバーエージェント AI事業本部では、サーバーサイド/ML・DS志望の23~24卒以降の学生の方を対象に3Dayインターンシップを開催予定です。
弊社に興味のある方、是非下記よりご応募ください!
https://www.cyberagent.co.jp/careers/students/event/detail/id=26886

2021年度新卒入社のバックエンドエンジニア。 株式会社 MG-DX にてバックエンドエンジニアとして従事。GraphQLの導入や新規連携基盤など幅広く手掛けている。