サーバクライアント型のシステムでGraphQLの採用を避けた方が良いと思う理由

はじめに

GraphQLは非常に高機能なツールである一方、万能が故に本来必要でないケースでも気軽に利用されている印象を受けます。 筆者は同じ組織で同じ目的のために構築されたサーバとクライアントの通信において、GraphQLを採用すべきでないと考えており、本記事ではそのポイントを整理していきます。 なお、本記事ではこのような1対1型のシステムをサーバクライアント型とよびます。

念のために記載しますが、GraphQLは素晴らしい技術であり、本記事はGraphQLを批判するものではありません。

TL;DR

  • (本当の意味で) Code Firstな開発を進めることが難しい
  • サーバクライアント型のシステムでは概念的に両者は一体であるため、両者間の結合を抽象化しない方がよい
  • ドメイン駆動設計を実践するに当たって、ドメインから視点をブラし、モデル駆動開発を阻害する要因となる

開発チームの想定

本記事での検討は以下のような開発チームを想定しています。

  • toB向けの業務システムの開発するチーム
  • 開発プロセスにスクラムを採用している

価値観

技術的なプラクティスは、その前提となる価値観によって選定されるため、 検討に先立って、まずは前提となる価値観を整理していきたいと思います。

まず一番大切なこととして、そもそもエンジニアリングの目的はなんでしょうか?

筆者の考えとしては、シンプルにユーザへ価値を提供をすることです。 エンジニアが業務としてシステム開発を行う場合、所属組織の事業推進の一助を担っているため、ユーザ価値の最大化へ貢献していくことが求められます。 そして、技術はそのための手段であり、手段目的は逆転させるべきではありません。

概ねこういった価値観は実際に多くの開発エンジニアと合意しやすい内容だと感じています。

一方で、(筆者を含め)多くのエンジニアは技術的な詳細に対して、より強く興味や魅力を感じて意識を集中させてしまいやすい傾向があります。

では、どのようにすれば、日々の開発業務を目的と紐付けていくことができるでしょうか?

ドメイン駆動設計(以下DDD)はこの難しい課題に対しての答えとなるひとつの言語化された体型として、実際多くの組織に受け入れられています。

DDDでは、上記のような視点の変化を与えてくれます。第一に技術的な詳細ではなくドメインに着目します。 ドメインを分析し、顧客に提供する機能とその実現方法をモデル化することで、ドメインモデルを構築します。 ドメインモデルはのほかのすべてのレイヤーから隔離されており、ドメインモデルの検討時には技術的な詳細を切り離して考えることができます。こうして構築されたモデルを利用して、モデル駆動でシステムの構築を進めます。

DDDを念頭に、筆者の価値観としては 技術的な課題 < ドメインの課題 という順番が前提になっており、開発エンジニアの思考やシステムの設計・開発に関しても、同じ順番・重要度で検討するべきだと考えています。

GraphQLが必要になる状況

GraphQLに関する詳細な情報はほかの多くの情報源に譲るとして、 ここで議論したいポイントとしては、GraphQLBFFとして機能する点です。

従来のクライアントサーバアーキテクチャでは、クライアントとサーバが1対1で対応していました。

flowchart LR
    クライアント --要求--> サーバ

しかし、異なる目的のクライアントが同じサーバを利用するようになると、サーバ側がそれぞれのクライアントのニーズに合わせきれなくなってきます。

flowchart LR
    クライアント1 --一度で取得できると良いなぁ...--> サーバ
    クライアント2 --データの構造が扱いにくいなぁ...--> サーバ

このような場合は、BFFを構築するメリットがあります。 サーバはリソースを抽象化(GraphQL Schemaで定義)して配信し、クライアント側はそれぞれニーズに合わせてクエリすることができます。

flowchart LR
    クライアント1 --クエリ1--> BFF
    クライアント2 --クエリ2--> BFF
    BFF --リソース--> サーバ

このように、GraphQLは異なるニーズをもった複数のクライアントが存在する場合にメリットを発揮しますので、GraphQLの利用を検討する場合は、システムの全体構成を考慮しておくことが重要です。

例えば、あなたがGithubで働くエンジニアでありAPI開発している場合は、無数のクライアントが多岐にわたるニーズでAPIをコールすることが予想されるので、GraphQLの利用は魅力的な選択肢になります。

一方で、GraphQLのように抽象化して柔軟性を実現する技術には、本来的に高機能であり、その分の複雑性を秘めているので、必要のない状況で利用するといわゆるオーバーエンジニアリングな状態になります。

本記事では、このようなサーバクライアント型のシステムにGraphQLを採用する是非に関して検討していきます。

Code First で APIを構築する

Code Firstとは、コードからAPIのスキーマ(IDL)を生成する方式です。
(IDLとはGraphQL, OpenAPI, Protocol Buffersなどです)

これは直接的にはGraphQLと関係ありませんが、Code Firstで開発を進めることによって、スムーズにDDDを実践してくことができると考えています。

価値観の章で述べたように、DDDをよく実践してくためには、エンジニアは最初にドメインからの視点で物事を考えてから、次第に技術的な詳細に思考を移していくのが基本的な流れになります。

今回のテーマで考えると以下のような順番です。

flowchart LR
  ドメイン --> ユースケース --> コントローラ --> 通信方式

ドメインには重要なドメイン知識がカプセル化され、ユースケースが一連の作業をまとめ上げてユーザの操作と合わせます。コントローラは外部からユースケースを呼び出すための単なる薄い変換層として実装されます。

一方で、GraphQL(OpenAPIでも同様)の通常の利用方法として、スキーマから書き始めると、この順番が逆転して、通信方式に近いレイヤーから考えていくことになります。

本来コントローラドメインオブジェクトを使って表現されたデータを通信可能な方式(JSONなど)へ変換する役目を持っており、その構造のモデルはDTOなどとしてコントローラに定義されます。そのため、IDLの定義(GraphQL Schemaなど)はわざわざ別途で記述する意義が薄く、本来はDTOから導出(自動生成)することができます。

このように、ドメインからの視点で開発を進める場合にAPIスキーマを考えること自体が単純なオーバーヘッドにもなりますが、保守性に影響を与えたり、もっと重大な副作用としては本来と逆の視点が要求されることでドメインから視点をブレらしてしまうことがあげられます。

Code Firstに対する誤解

一口にCode Firstといっても、大きく分類して以下の2つがあると考えています。

  1. CodeからSchemaを生成するもの
  2. CodeでSchemaを書くもの

本記事ではオススメしたいのは、1の本来的な意味でのCode Firstを指します。2の場合は利用しているツールがTypeScriptなどの汎用言語になっただけで、Schema Firstで開発している場合と本質的やっていることは変わらないからです。

しかしながら、Nexus, Pothos, tRPC などの有名はOSSは2の方式であることが多いです。そのため、Schema Firstと比較して大きなメリットが得られずに疲弊するケースが多いように感じています。

筆者としては、OpenAPICode Firstで開発するツールとして tsoaを利用することが多いです。NestJSなどでもCode Firstな開発を進めることができますが、tsoaの方がより少ない注釈(デコレータ)で自然にCode Firstな開発を進めることができます。

参考: Code Firstで疲弊しないOpenAPI活用方法

ちなみにあくまで筆者の推測ですが、GraphQLの方が1Code Firstツールを開発しにくいと考えています。OpenAPIの場合は通信に載せるPayloadが基本的にJSONなので、バックエンドがNode.jsで書かれていれば、DTOをほぼ機械的にJSON変換することができます。一方、GraphQLQueryTypeなどは独自の概念であるため、異なる概念間でのマッピングはどうしても必要になるものと思われます。

GraphQLはリソースを抽象化する

GraphQLOpenAPIを比較する場合には大きなパラダイムの違いがあることを認識しておく必要があります。それは、何で抽象化するか? の違いです。

GraphQLREST APIはリソースを抽象化する手法です。GraphQL Schema上ではリソースをtypeとして整理して、queryによってアクセス方法を定義します。

一方で、OpenAPIを利用するシステムでは、純粋なRESTful APIであるものは極稀で、一般的にはRPC(関数)として抽象化します。

この辺りは以前記事にもまとめました。 GraphQLとOpenAPIを比較する時は「REST API」に注意する

つまり、OpenAPIを利用する場合、サーバクライアントのニーズに直接的に答えるような関数を整理することでHttp通信の内容を隠蔽しますが、サーバ側のインターフェイスは抽象化されずそのまま公開されるので、概念レベルでのサーバ・クライアント間の結合度は強くなります。
一方で、GraphQLの場合はサーバリソースを抽象化して配信し、クライアントは自身のニーズに合わせてQuery Documentを組み立てることができるので、概念レベルでの両者の結合度は低くなります。

不要な抽象化層がスクラムチーム分断する

前章では、GraphQLを利用した方がよりサーバ-クライアント間の関係性が抽象化されることを説明しました。抽象化は技術的な詳細を切り離して疎結合化させるのに有効な手法です。


ここで明確しておきたいのが、サーバクライアント型のシステムにおいて、バックエンドは技術的な詳細ではないということです。 ホストされる環境が違ったり、記述されている言語が違ったりという技術的な差異はあり得ますが、概念的には両者は同じニーズのために開発された一体のシステムです。

もう一つ前提としておきたいのは、スクラムガイドにある通りスクラムチームはフルスタックであることです。一つのチームが顧客への価値提供の全てをフルスタックに担っていきます。その人数はよくピザ2枚数ルールと言われるように、8名前後とされており、そのうちエンジニアは4名前後でしょうか。 そのような環境では、それぞれのエンジニア個人においても、技術領域を限定せずに変幻自在な貢献が求められます。

そのため、クライアントから見てサーバは抽象化して隠蔽するべき技術的な詳細ではなく、むしろ結合を明示的に管理し、システム全体として概念的な凝集を構成することが保守性を高めることにつながると考えます。

DevSecOpsなどの考え方からも、モダンな組織では職能型サイロを生むような構造はなるべく避けておきたいところです。

最後に、スタッフエンジニアの言葉を引用させていただきます。

耐久性の高いインターフェースはすべての本質的複雑性を明示するが、偶発的複雑性は示さない。 (p.75). Kindle 版

Clean Architectureに対応させて考える

前節で検討した内容を、Clean Architectureの代表的な図を元にイメージしてみます。

image

物理的に考えるとクライアントサーバはそれぞれの円を構成し、円と円の間は青い層(Webの領域)が隔てています。

しかしながら、概念的には両者は同じシステムの一部であり、ドメインを共有しているはずですし、ユースケースwebにより分断されなければ本来は一体となっていたはずです。
そのため、システム全体の概念としては青い矢印で接続されて1つの円を構成します。

そして、このような構成を実現したい場合、Web通信の技術的な詳細は隠蔽しつつ、バックエンドを隠蔽しないようなアーキテクチャが求められます。両者の概念的な結合は意味を持っており、ユースケースを分断するようなBFF層はシステムのセマンティクスを破壊する可能性があります。

Simple と Easy

技術選定をする際には、SimpleEasyの違いを意識することが重要だと考えています。

それぞれの説明として、著名なt_wadaさんのツイートを引用させていただくと、以下のように整理されています。

Easy: 手数の少なさを重視(そのかわり覚えることが増え、特定の状況には強いが他には弱い設計になる)

Simple: 覚えることの少なさを重視(そのかわり手数が増えたり、自分で組み合わせたりしなければならない)

例えばフレームワークライブラリを比較した場合、フレームワークEasyにあたって、手数を少なく目的を実現させることができますが、内部は高度に複雑でシステムとの結合度は高くなります。 一方で、ライブラリSimpleにあたり、概念的により単純で自由度が高く、システムとの結合度を低く保つことが可能ですが、目的の達成までの手数が増えます。

一般論としては、安易にEasyに依存することはシステムの柔軟性を低下させるので、できるだけSimpleに保ちつつも、開発生産性とのトレードオフを考えるながら用法要量を守ってEasyを適用していくのが賢い戦略です。

筆者としては、このSimpleEasyの対比が、OpenAPIGraphQLの対比に似ていると考えています。 GraphQLOpenAPIと比べてよりフレームワーク的な性質を持っており、システムとの結合度が高くなります。

OpenAPIを使ってNodeJSでサーバを構築することを考えた場合、expressのようなごくシンプルなフレームワークからより高機能なものまで幅広に選定することができます。また、利用方法も一般的なWebの知識があれば、すぐに理解することができます。tsoaを利用した場合はサーバ側のボイラープレートを自動生成することができますが、express内部以外のコアなコードは数百行ほどで済みます。

一方、GraphQLは本来的に高機能であり、利用する場合には遥かに巨大なMiddlewareに依存する必要があります。
AWSの場合、AppSyncを構築・運用するだけでも多くのノウハウが要求されます。それに付随して、テスト環境・テストデータの用意、デバッグ方法、通信のモックなども、それぞれ独自のプラクティスを学ぶ必要があります。 フロントエンドはGraphQL Schemaに対してQuery Documentを別途定義する必要がありますが、fragment colocation ( 参考 )などをする場合は、IDLがコンポーネントレベルまで結合します。 こうなると、本来はフロントエンドにとってもドメインが重要ですが、GraphQL Schemaとの結合が強くなることで意識の外に追いやられてしまう危険性があります。

フレームワークを利用する際は、注意して取り組まないとフレームワーク由来の構造が顕現しやすく、本質的なレイヤーが失われます。 また、巨大な複雑性に依存することで思わぬ対応工数や学習コスト・認知不可の増大をもたらすこともあります。

フレームワークは一見すると銀の弾丸に見えることがありますが、このようなトレードオフを受け入れるだけのメリットが得られるか?現時点のプロダクト戦略と一致するか?を慎重に検討する必要があります。

また、フレームワークはシステムの柔軟性をある程度限定しますが、DDDではモデル駆動を妨げるような制約が発生しないかを注意する必要があります。

終わりに

長文にもなって、多分に読みにくさのある記事になってしまったと思いますが、以上のような理由から筆者としてクライアントサーバ型のシステムではGraphQLを採用することは避けた方が良いと考えています。

もちろんEngによって異論反論が多々あると思いますが、一個人の意見として整理してみたいと思い記事にまとめてみました。今後も、周囲のEngと議論しつつ気づいた点をUpdateしていければと考えています。

最後まで読んでいただいてありがとうございました!