GraphQL Gateway - アプリ向けに API を公開する

Wantedly では、システム内部のマイクロサービス間通信に Protocol Buffers / gRPC を利用しています(『protobufスキーマとgRPC通信』の章)。

では、他のマイクロサービスではなく、Webアプリやモバイルアプリに向けて API を提供する場合についてはどうすると良いでしょうか?

この章では、アプリから使えるシステムの API(まさに "Application Programming Interface" です)を用意する際に使う、GraphQL Gateway について概説します。

GraphQL Gateway とは?

GraphQL Gateway とは、システムの中に数あるマイクロサービスの一つで、アプリ向けに GraphQL API を提供するものです。

基本的に、アプリからシステムに対しての API 呼び出しは全て GraphQL Gateway が引き受けることを想定しています(まだこのアーキテクチャに移行していない機能は多いので、それらは直接各マイクロサービスの REST API を叩いています)。したがって、システムの中でも前段に立つコンポーネントであると言えるでしょう(「技術とアーキテクチャ」の章の図も併せてご参照ください)。

理想状態として、この GraphQL Gateway はシステムが持つドメインオブジェクトとそれに対する操作を全て提供します。従って、アプリから見ると、システムのインターフェイスは GraphQL になっており、そこでできることは GraphQL スキーマを見ることによって探索できる状態になります(実際には、GUI である GraphQL Voyager(内部向けリンク)を利用することになるでしょう)。そして、スキーマを見ながら、アプリは自ら欲しいデータをクエリによって選択的に取得します。

動作としては、GraphQL Gateway は受けたリクエスト内容をもとに後段のマイクロサービスに対して gRPC の呼び出しを行います。この結果を集約し、オブジェクトグラフを構築してレスポンスとして返却します。後述するように、この作業はフレームワーク化されているため、全てをマニュアルで記述する必要はありません。

なぜ GraphQL なのか?

マイクロサービス間通信に Protocol Buffers / gRPC を使っていながら、なぜアプリとの通信では GraphQL を利用するのか。この「目的」の部分を理解することが、GraphQL で実際にスキーマ設計を行う際などにも非常に重要となります。

それは一言で言えば、ビジュアルデザインやレイアウトデザインに依存しない、情報設計の抽象度で API を提供することで、全体の ソフトウェアデリバリのパフォーマンスを最大化する ためです。

どのような問題を解決できるか

具体的に見ていきましょう。技術とアーキテクチャの章の図からもわかるように、Wantedly には複数のアプリが存在します。

Visit サービスに関して言うと、ツー・サイド・マーケットプレイスのサービスのため、個人ユーザーと企業ユーザーの2種類が存在します。これらはそれぞれ別の Web アプリを利用します。また、個人ユーザー向けには Web アプリと同じ機能(あるいはそのサブセット)を iOS アプリと Android アプリにも提供しています。つまり、マルチ・プラットフォーム展開をしています。

サービス特性上、これら複数あるアプリに同じコンテンツが表示(あるいは編集)されます。例えば、個人ユーザーが作成した「プロフィール」や、企業ユーザーが作成した「募集」などです。

つまり、情報としては同じ情報がさまざまなアプリに出ることになります。ただ、画面の大きさ(モバイルかどうか)や個人ユーザー向けか企業ユーザー向けか、などによってレイアウトや情報量は変わります。

こういった、いろいろあるアプリごとに API を作っていては、その度にバックエンドとフロントエンドでのコミュニケーションが発生してしまい、ソフトウェアデリバリのパフォーマンスが下がってしまいます。

新機能を追加する例

一つの理想として言うならば、ある機能を新たに追加する際、次の図のようにシステムの実装は最初の一度きりであるべきでしょう。例えば Visit に対して「副業意欲・転職意欲」を表示・入力できるようにするとします。これの開発のタイムラインを単純化して図示すると、次のようになります:

ここで見落としがちなのは、アプリのエンジニアだけで実装できることで、「実装期間の短縮」「コミュニケーションコスト」だけではなく、バックエンドも含めた「リソースの調整」が不要となる点です。こういったリソース・アロケーションの疎結合化が、組織全体のソフトウェアデリバリのパフォーマンスを押し上げます。

これを実現するためにシステムの設計・実装は、は“いま目の前にあるアプリ”のことだけでなく、実装しようとしているオブジェクトが“将来的に別のアプリや別の画面で使われるときのこと”も想定する必要があるので留意しましょう。

グロースを行う例

別の例として、こういった機能のゼロ→イチのフェーズだけでなく、グロースのフェーズでも GraphQL によって情報設計のレベルで分離することはソフトウェアデリバリの速度を向上させます。

例えば、コロナ禍にあってカジュアル面談のオンライン化のニーズが増えた結果、募集の一覧画面に「オンライン面談できるよ!」というタグを出す A/B テストをするとしましょう。この場合、「募集」という GraphQL オブジェクトのフィールドとして「オンライン面談できるかどうか」があるでしょうから、Web アプリの GraphQL クエリを少し編集するだけで実装ができるため、実装からリリースまで1日以内に行うことが可能になるでしょう。

以上は、アプリのソフトウェアデリバリに着目した例ですが、技術領域を跨いだコミュニケーション回数の削減は当然バックエンドの負担も減らすため、バックエンドのシステム改善を加速させることが可能となります。

どのように問題を解決するか

技術的になぜ GraphQL が前述のような問題を解決できるかというと、「スキーマ」や「クエリ言語」といった仕組みを仕様に取り込むことで可能になっています。

これは責務の観点から説明すると、「どの範囲のデータをクライアントが必要とするか」の詳細をサーバーサイドではなく、クライアントサイドがより決められるようになっている、ということでもあります。

付随的に、「スキーマ」「クエリ言語」の仕組みがあることで、サーバーサイド・クライアントサイドにおける型定義の生成などの開発者体験の向上が可能になっています(これ自体は、Protocol Buffers からも open API 経由で似たようなことはできますが)。

GraphQL Gateway の開発フレームワーク

上記のようなことを実現するための GraphQL Gateway のフレームワークについて概説します。

基本的に、GraphQL Gateway は GraphQL Helix という GraphQL サーバー上で Nexus というスキーマ構築ライブラリを利用して記述します。ただし、背後のマイクロサービスとの繋ぎ込みを簡易にするため、Protocol Buffers のスキーマを活用しているという特徴があります。それをここで説明します。

概念的には、GraphQL スキーマはオブジェクトグラフとそれに対するエントリポイントに分けて考えられます。さらに、オブジェクトグラフの方は、ノードに相当する「オブジェクト」とエッジに相当する「リレーション」があります。そして、エントリポイントは、クエリとミューテーションの二種類が存在します。したがってスキーマとして定義すべき要素は次のようになります。

  • グラフ自体…オブジェクト、リレーション

  • グラフに対するエントリポイント…クエリ、ミューテーション

このうち、一番基礎となるのがオブジェクトですが、これは wantedly/apis (internal) で定義した Protocol Buffers のメッセージ定義から(コメントなどのメタデータも含めて)自動的に生成されます。なので、GraphQL スキーマを定義する順序としては、

  1. オブジェクトの定義を Protocol Buffers 上できちんと固めて apis にマージする

  2. それを参照しつつリレーション、クエリ、ミューテーションがあれば GraphQL Gateway 上で定義する(実装もここで行う)

の2ステップとなります。なお、「あれば」と書いたのは、例えば既存のオブジェクトに新しくフィールドを追加するだけのことも多いためです。この場合は、Protocol Buffers からのオブジェクト生成を再実行したコードをマージするだけになります。

定義の流れとしては以上のようになります。実装としては、リゾルバ(GraphQLオブジェクトをどのように取得するかのロジック)を書くことになり、そこに gRPC の呼び出しコードを書く(gRPC の RPC はステップ1で一緒に定義してしまうことが多い)という形になります。次のコードは、projects というクエリの定義と、対応するリゾルバの実装になります。

// src/schema/projects/projects.ts

export const projects = queryField("projects", {
  type: nonNull(list(nonNull("Project"))),
  description: "募集一覧に表示する募集の一覧",
  async resolve(_root, _args, ctx, _info) {
    const req = new ListProjectsRequest();
    const resp = await ctx.dataSources.projects.project.promises.listProjects(req, ctx.getGrpcMetadata(info)); // 新しい gRPC サービスが増える場合は、別途 dataSources のマッピングを更新
    return resp.getProjectsList();
  },
});

このように GraphQL Gateway はデータの集約(アグリゲーション)を行いますが、ドメイン知識 (ドメインロジック, ビジネスロジック) は一切持たないので注意しましょう。

以上は概説になります。より実践的なハンズオンはリポジトリ配下のドキュメントにあるので、コードを書く際はそちらを参照してください。

スキーマ設計について

最後に、GraphQL の 最も重要な部分はスキーマ設計 になります。

冒頭、GraphQLを入れる理由としてソフトウェアデリバリの全体的な向上を挙げましたが、それを達成できるかどうかは良いスキーマを設計できるかどうかに掛かっていると言えます。

そして、いろいろなアプリで使われる API ということは、一度出してしまったスキーマはなかなか変更できない、ということも同時に意味します(全く変更できない訳ではありませんが、コストが高いという意味です)。

スキーマ設計は非常に重要なため、別途の章を設ける予定です。ただ、GraphQL スキーマについて一般的なことは、『Production Ready GraphQL』という本の前半部分にまとまっているので、スキーマの設計を多くやる場合は、目を通しておくことをお勧めします。

話を聞きに行きたい

もっと知りたい

最終更新