サーバクライアント型のシステムでGraphQLの採用を避けた方が良いと思う理由
自ブログからの引用です。
はじめに
GraphQL
は非常に高機能なツールである一方、万能が故に本来必要でないケースでも気軽に利用されている印象を受けます。
筆者は同じ組織で同じ目的のために構築されたサーバとクライアントの通信において、GraphQL
を採用すべきでないと考えており、本記事ではそのポイントを整理していきます。
なお、本記事ではこのような1対1型のシステムをサーバクライアント型
とよびます。
念のために記載しますが、GraphQL
は素晴らしい技術であり、本記事はGraphQL
を批判するものではありません。
TL;DR
- (本当の意味で)
Code First
な開発を進めることが難しい -
サーバクライアント型
のシステムでは概念的に両者は一体であるため、両者間の結合を抽象化しない方がよい - ドメイン駆動設計を実践するに当たって、ドメインから視点をブラし、モデル駆動開発を阻害する要因となる
開発チームの想定
本記事での検討は以下のような開発チームを想定しています。
- toB向けの業務システムの開発するチーム
- 開発プロセスにスクラムを採用している
価値観
技術的なプラクティスは、その前提となる価値観
によって選定されるため、
検討に先立って、まずは前提となる価値観
を整理していきたいと思います。
まず一番大切なこととして、そもそもエンジニアリングの目的
はなんでしょうか?
筆者の考えとしては、シンプルにユーザへ価値を提供をすることです。
エンジニアが業務としてシステム開発を行う場合、所属組織の事業推進の一助を担っているため、ユーザ価値の最大化へ貢献していくことが求められます。
そして、技術はそのための手段
であり、手段
と目的
は逆転させるべきではありません。
概ねこういった価値観
は実際に多くの開発エンジニアと合意しやすい内容だと感じています。
一方で、(筆者を含め)多くのエンジニアは技術的な詳細に対して、より強く興味や魅力を感じて意識を集中させてしまいやすい傾向があります。
では、どのようにすれば、日々の開発業務を目的
と紐付けていくことができるでしょうか?
ドメイン駆動設計(以下DDD)
はこの難しい課題に対しての答えとなるひとつの言語化された体型として、実際多くの組織に受け入れられています。
DDD
では、上記のような視点の変化を与えてくれます。第一に技術的な詳細ではなくドメイン
に着目します。
ドメインを分析し、顧客に提供する機能とその実現方法をモデル化することで、ドメインモデルを構築します。
ドメインモデルはのほかのすべてのレイヤーから隔離されており、ドメインモデルの検討時には技術的な詳細を切り離して考えることができます。こうして構築されたモデルを利用して、モデル駆動でシステムの構築を進めます。
DDD
を念頭に、筆者の価値観としては 技術的な課題 < ドメインの課題
という順番が前提になっており、開発エンジニアの思考やシステムの設計・開発に関しても、同じ順番・重要度で検討するべきだと考えています。
GraphQLが必要になる状況
GraphQL
に関する詳細な情報はほかの多くの情報源に譲るとして、
ここで議論したいポイントとしては、GraphQL
がBFF
として機能する点です。
従来のクライアントサーバアーキテクチャ
では、クライアントとサーバが1対1で対応していました。
しかし、異なる目的のクライアントが同じサーバを利用するようになると、サーバ側がそれぞれのクライアントのニーズに合わせきれなくなってきます。
このような場合は、BFF
を構築するメリットがあります。
サーバはリソースを抽象化(GraphQL Schema
で定義)して配信し、クライアント側はそれぞれニーズに合わせてクエリすることができます。
このように、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
をよく実践してくためには、エンジニアは最初にドメインからの視点で物事を考えてから、次第に技術的な詳細に思考を移していくのが基本的な流れになります。
今回のテーマで考えると以下のような順番です。
ドメイン
には重要なドメイン知識がカプセル化され、ユースケース
が一連の作業をまとめ上げてユーザの操作と合わせます。コントローラ
は外部からユースケース
を呼び出すための単なる薄い変換層として実装されます。
一方で、GraphQL
(OpenAPIでも同様)の通常の利用方法として、スキーマから書き始めると、この順番が逆転して、通信方式
に近いレイヤーから考えていくことになります。
本来コントローラ
はドメインオブジェクト
を使って表現されたデータを通信可能な方式(JSONなど)へ変換する役目を持っており、その構造のモデルはDTO
などとしてコントローラ
に定義されます。そのため、IDL
の定義(GraphQL Schema
など)はわざわざ別途で記述する意義が薄く、本来はDTO
から導出(自動生成)することができます。
このように、ドメインからの視点で開発を進める場合にAPIスキーマを考えること自体が単純なオーバーヘッドにもなりますが、保守性に影響を与えたり、もっと重大な副作用としては本来と逆の視点が要求されることでドメインから視点をブレらしてしまうことがあげられます。
Code Firstに対する誤解
一口にCode First
といっても、大きく分類して以下の2つがあると考えています。
- CodeからSchemaを生成するもの
- CodeでSchemaを書くもの
本記事ではオススメしたいのは、1
の本来的な意味でのCode First
を指します。2
の場合は利用しているツールがTypeScript
などの汎用言語になっただけで、Schema First
で開発している場合と本質的やっていることは変わらないからです。
しかしながら、Nexus
, Pothos
, tRPC
などの有名はOSSは2
の方式であることが多いです。そのため、Schema First
と比較して大きなメリットが得られずに疲弊するケースが多いように感じています。
筆者としては、OpenAPI
をCode First
で開発するツールとして tsoaを利用することが多いです。NestJS
などでもCode First
な開発を進めることができますが、tsoa
の方がより少ない注釈(デコレータ)で自然にCode First
な開発を進めることができます。
参考: Code Firstで疲弊しないOpenAPI活用方法
ちなみにあくまで筆者の推測ですが、GraphQL
の方が1
のCode First
ツールを開発しにくいと考えています。OpenAPI
の場合は通信に載せるPayload
が基本的にJSON
なので、バックエンドがNode.js
で書かれていれば、DTO
をほぼ機械的にJSON変換することができます。一方、GraphQL
のQuery
やType
などは独自の概念であるため、異なる概念間でのマッピングはどうしても必要になるものと思われます。
GraphQLはリソースを抽象化する
GraphQL
とOpenAPI
を比較する場合には大きなパラダイムの違いがあることを認識しておく必要があります。それは、何で抽象化するか? の違いです。
GraphQL
やREST 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
の代表的な図を元にイメージしてみます。
物理的に考えるとクライアント
とサーバ
はそれぞれの円を構成し、円と円の間は青い層(Webの領域)が隔てています。
しかしながら、概念的には両者は同じシステムの一部であり、ドメイン
を共有しているはずですし、ユースケース
はweb
により分断されなければ本来は一体となっていたはずです。
そのため、システム全体の概念としては青い矢印で接続されて1つの円を構成します。
そして、このような構成を実現したい場合、Web通信の技術的な詳細は隠蔽しつつ、バックエンドを隠蔽しないようなアーキテクチャが求められます。両者の概念的な結合は意味を持っており、ユースケース
を分断するようなBFF層
はシステムのセマンティクスを破壊する可能性があります。
Simple と Easy
技術選定をする際には、Simple
とEasy
の違いを意識することが重要だと考えています。
それぞれの説明として、著名なt_wadaさんのツイートを引用させていただくと、以下のように整理されています。
Easy: 手数の少なさを重視(そのかわり覚えることが増え、特定の状況には強いが他には弱い設計になる)
Simple: 覚えることの少なさを重視(そのかわり手数が増えたり、自分で組み合わせたりしなければならない)
例えばフレームワーク
とライブラリ
を比較した場合、フレームワーク
はEasy
にあたって、手数を少なく目的を実現させることができますが、内部は高度に複雑でシステムとの結合度は高くなります。
一方で、ライブラリ
はSimple
にあたり、概念的により単純で自由度が高く、システムとの結合度を低く保つことが可能ですが、目的の達成までの手数が増えます。
一般論としては、安易にEasy
に依存することはシステムの柔軟性を低下させるので、できるだけSimple
に保ちつつも、開発生産性とのトレードオフを考えるながら用法要量を守ってEasy
を適用していくのが賢い戦略です。
筆者としては、このSimple
とEasy
の対比が、OpenAPI
とGraphQL
の対比に似ていると考えています。
GraphQL
はOpenAPI
と比べてよりフレームワーク
的な性質を持っており、システムとの結合度が高くなります。
OpenAPI
を使ってNodeJS
でサーバを構築することを考えた場合、express
のようなごくシンプルなフレームワーク
からより高機能なものまで幅広に選定することができます。また、利用方法も一般的なWebの知識があれば、すぐに理解することができます。tsoa
を利用した場合はサーバ側のボイラープレートを自動生成することができますが、express
内部以外のコアなコードは数百行ほどで済みます。
一方、GraphQL
は本来的に高機能であり、利用する場合には遥かに巨大なMiddleware
に依存する必要があります。
AWS
の場合、AppSync
を構築・運用するだけでも多くのノウハウが要求されます。それに付随して、テスト環境・テストデータの用意、デバッグ方法、通信のモックなども、それぞれ独自のプラクティスを学ぶ必要があります。
フロントエンドはGraphQL Schema
に対してQuery Document
を別途定義する必要がありますが、fragment colocation
( 参考 )などをする場合は、IDL
がコンポーネントレベルまで結合します。
こうなると、本来はフロントエンドにとってもドメインが重要ですが、GraphQL Schema
との結合が強くなることで意識の外に追いやられてしまう危険性があります。
フレームワークを利用する際は、注意して取り組まないとフレームワーク由来の構造が顕現しやすく、本質的なレイヤーが失われます。
また、巨大な複雑性に依存することで思わぬ対応工数や学習コスト・認知不可の増大をもたらすこともあります。
フレームワークは一見すると銀の弾丸
に見えることがありますが、このようなトレードオフを受け入れるだけのメリットが得られるか?現時点のプロダクト戦略と一致するか?を慎重に検討する必要があります。
また、フレームワークはシステムの柔軟性をある程度限定しますが、DDD
ではモデル駆動
を妨げるような制約が発生しないかを注意する必要があります。
終わりに
長文にもなって、多分に読みにくさのある記事になってしまったと思いますが、以上のような理由から筆者としてクライアントサーバ型のシステムではGraphQLを採用することは避けた方が良いと考えています。
もちろんEngによって異論反論が多々あると思いますが、一個人の意見として整理してみたいと思い記事にまとめてみました。今後も、周囲のEngと議論しつつ気づいた点をUpdateしていければと考えています。
最後まで読んでいただいてありがとうございました!
Discussion