LayerXバクラクのシステム構成の変遷 - REST APIからGraphQLの活用へ
LayerXは全ての経済活動をデジタル化するというミッションのもとに3つ事業をやっていまして、今回は法人支出管理サービスのバクラクについて話します。
バクラクのビジネスとしては、BtoB取引のマーケットのセンターピンである請求書受取から、いくつかバックオフィス向けにプロダクトを提供しています。 今のところ半年に1個のペースでプロダクトをリリースしています。
バクラクのシステムの変遷について、最初の受取請求書に関してはGraphQLではなくRESTAPIで開発していました。 サーバーがgo-swaggerで Goのコードを生成してスキーマ駆動開発をやっていた形になります。
バックエンドで感じたREST APIの2つの課題
REST APIでプロダクトを開発しましたが、やっていく中でバックエンドでは問題が大きく2つありました。1つ目が 、バクラクのサービス上1画面でいろんなリソースを取得する必要があるのですが、 複数回フロントエンドからリクエストしなければならないアンダーフェッチングの問題が起きました。
リレーションされたレスポンスも全部返すことで対策してましたが、不要なデータもレスポンスとして返るオーバーフェッチングの問題も起きていました。 全部リレーションされたデータをバクラクで返すようにしてたので、これはRESTfulAPIの仕様的にRESTfulリソースベースではないと。
また、フロントエンドのためにデータを返すため、バックエンドのロジックの見通しが悪く、フロントのUIの都合に引きずられた実装になったという問題がありました。
たとえば、部署を持つユーザーの一覧を返すAPIを作る時に、 このユーザーを全部取ってきて、N+1を解消するためにユーザーのグループのIDをmapにしてfor文で回して、バッチゲットでグループの一覧を取ってきて、またループして ユーザーの情報中にその部署の情報を埋め込んで返すみたいな、すごい種類の長いコードを書いてたりとか、N+1を解決するために個別にQueryを書いてました。
フロント側はVuexで実装していたのですが、 仕訳に必要な取引先の情報や勘定科目などのマスタ情報をVuexの中で持たせて画面ごとに取得しないようキャッシュしていて、リソースごとにLoadingやError Handlingのテンプレート的な実装を多くしていました。
そうしたらVuexならではなんですが、Vuexの世界に持っていった時に文字列でVuexのストアの情報を参照したりするので、タイプミスや誤変換で動かないとか、 Mutationがあった後に更新状態の反映漏れでVuexのストアの中身がリストに追加されない状態になっていました。
そういった状態を踏まえて、 以降はプロダクトごとにGraphQLを導入しまして、 バックエンドはGoで開発してるのでgqlgenというラベルを使ってGraphQLのAPIを使っています。
プロダクトごとにREST APIからGraphQLに切り替えたメリット
先ほど挙げたバックエンド側のアンダーフェッチングやオーバーフェッチング問題はGraphQLを使うことで解消され、 画面に必要な情報が1度のリクエストで取得されるようになりました。
GraphQLに変えたことによって、この右側のコードのように部署を持つユーザーの一覧を返す時も コードがかなりシンプルになっていて、Model Resolverでリレーションされたデータの解決も容易になりました。 フロントはApolloで実装しているのですが、 先ほど挙げたようなVuexのキャッシュの更新の問題とか、Loadingの管理やErrorの管理がApolloの方に移譲できるので、 フロントエンドの設計もかなりシンプルにできました。
また、先ほどの課題の改善について個人的に良かった話で、 バックエンドの実装に関しては2つあります。
1個目に関して、Model Resolverでモデルのリレーションが解決していくので、部署とそのユーザーのリレーションの他にも担当部署やグループ の情報を持たせたい時に新しくResolverが追加されます。Model Resolverの奥の処理は共通化できるので、実装すればするほど生産性や再利用性がかなり高くなると感じています。
2個目は、このモデルは単位 が落ちずに特定のフィールドに対してもResolverを実装することができて、たとえばPreSigned URLを発行して返す負荷のかかる処理も、フロント側から要求された時だけ することもできるので非常に便利だなと感じてます。
あと後方互換性をCI上で検知する仕組みで破壊的変更があった時にすぐ気付ける状態を作れるので、 安定してリリースできます。 さらに、スキーマ駆動開発の中でエンジニアでお互いにレビューしたスキーマの情報を正として実装していくので、手戻りが起きにくくて設計の段階から齟齬が起きないところも良いですね。
GraphQLを使用するにあたり、工夫が必要だったこと
逆に工夫が必要だった点は、GraphQLと同じエンドポイントに大量にリクエストが来たりするので、パフォーマンスのモニタリングやトラブルシュートをする時にデバックしやすいように、データブックにバグ情報を連携する必要があるところですね。
こんな感じでLayerXではプロダクトごとにGraphQL APIを提供して開発をやっていました。 コンパウンドスタートアップという形でプロダクトを展開していて、プロダクト間の連携をする中でプロダクトをまたいであらゆるリソースにアクセスする必要が出てきたので、 プロダクトごとにGraphQLを持つ形だと問題が起きました。
今はサーバー間で通信して、プロダクトごとにウェブのフロントが各APIに通信しているのですが、このままサーバー中心で連携していくと それぞれのプロダクトの形が異なるGraphQLのスキーマが増えててしまうことや、そもそも1個のフロントが複数のエンドポイントを叩いていて認知負荷が高くなるリスクがありました。
GraphQLによる認知負荷増大リスクを防ぐために、Enablingチームを発足
そういった問題を解決するために、Enablingチーム(プロダクト開発チームとの協業を通じて新たな障害や課題を仕組み化を伴って解決するチーム)を作って、Layeroneというプロジェクトが発足して開発を行っています。
今はプロダクトごとにGraphQLのエンドポイントを立てるのではなく、各マイクロサービスの前段にGraphQL Gatewayというゲートウェイを置いています。 Web Fontendとモバイルアプリがあったりもするんですが、全てのクライアントがこのGraphQL Gatewayを参照してリソースにアクセスするアーキテクチャにすることで、認知負荷が高いという先ほどの問題に対処している状態です。
GraphQLではなくRESTのゲートを立てるという話も選択肢にありますが、ここでも再度GraphQL Gatewayを採用しています。 GraphQLの方がリソースの解消がかなり簡単ですし、多様なフロントエンドからさまざまなコンテキストでアクセスが来たりもします。
まとめとして、LayerXではGraphQLを導入することでアンダーフェッチングやオーバーフェッチングの問題が解消されました。 リレーションの解決が非常に楽なので、 リレーションされたレスポンスを返すコードがシンプルで、生産性が高く可読性の良いコードが書けています。 コンパウンドスタートアップなので連携そのものがプロダクトで、Gatewayを立てて開発をやっています。
リリースして3年の中で日々アーキテクチャが進化していますが、こういった進化ができたのは、テクノロジーや基盤に投資する姿勢や文化がLayerXに浸透しているからだと思っています。 このような文化観は非常に大事だと振り返って思いました。
パネリストによるGraphQLについてのディスカッション
――ここからは、運営の方で用意したテーマに沿って、先ほど発表していただいた3名のパネリストによるディスカッションを行います。まず一つ目のテーマは「GraphQLはどういった現場フェーズの会社だと向いていて、どういった活用が想定されるか」です。
内山さんいかがでしょうか?
内山:GraphQLはクライアントの画面の要請に設計の力学を映しやすい技術なので、それにバックエンドの人も応えやすいですね。 大きい会社やチームよりは、 比較的小規模なチームに合うと思っています。
――続けて松本さん、いかがでしょうか。
松本: 自分も内山さんと概ね同じで、0→1の規模が小さい場合は 導入しやすいと思いますね。 先ほどの発表にもあったマルチデバイスとか、うちみたいにプロダクト間の連携があって、いろんなコンテキストでリソースを取得する場合にも非常に良いと思います。
――続いてqsonaさん、お願いします。
qsona: たとえば、今どの現場にReactが向いてるかと言われたら、ほぼ全ての現場ですという答えになるんですよ。 別にjQuery使わなくていいからReact使ってくれというのと同じ感覚で、 GraphQLを使ってくれという気持ちです。
――続いてのテーマは「立ち上げでは向いてたり、技術スタック含めて採用するケースがあるけれども、REST APIを採用してるサービスが途中からGraphQLに移行するメリットはあるのか」ですが、 今度はqsonaさんいかがでしょうか。
qsona:SHEのRailsアプリとかはREST APIがあります。ただ、GraphQLをやっていきたいのでちょっとずつGraphQLを増やしてます。 元々RESTだった画面を 機能改善のついでにGraphQLに置き換える感じならいけると思います。
―― 続いて松本さん、いかがでしょうか。
松本: GraphQLのメリットが享受できるなら一気に置き換える必要は無くて、徐々に入れていくのは全然ありだと思います。 GraphQLだから1画面1リクエストという制約は無いので、 リスクと生産性のバランスを考えながら適用していけば良いと思いますね。一般的なリファクタリングの方針の決め方とあまり変わらない気はします。
――続いて、内山さんにもこの辺りをご意見いただければと思います。
内山:既存のAPIを完全に置き換えるよりは、段階的な 移行がセオリーになると思います。 ただ、本当にRESTに明確な問題や課題があって、GraphQLに移行した時にコスト的なメリットが出るかは 懐疑的ですね。少なくとも慎重に考えた方がいいですね。
――続いてのテーマは、「逆にGraphQLを使わない方が良い現場や状況があるか」ですが、 内山さんからお願いします。
内山: なんとなくGraphQLが良いらしいから入れてみました、というスタンスでいくと、予期しない場合の対応とか、悪意あるQueryが投げられたりする場合があるので 覚悟が無いと危ないですね。あとGraphQLエコシステムとの相性があまり良くない言語を使う怖さはありますね。
―― 松本さんはいかがでしょうか。
松本: 自分もほぼ同意見でして、スキーマ駆動開発をしなきゃいけないので、サーバとエンジニアとの距離が遠い状況は合う合わない以前の問題かなと思いますね。 距離が遠いと、スキーマの定義とかの認識をすり合わせるコミュニケーションに結構コストがかかりそうですね。
内山:開発は画面設計から始まることが多くて、実際はDB設計しながらスキーマを考えることが必要だと思っていて、フロントエンドとバックエンドの距離が近い方が 良いスキーマが作れる感覚があります。
――qsonaさんもそのあたりはいかがでしょうか。
qsona:GraphQL APIはユースケース駆動のAPIを作るには向いてないんです。 なので、GraphQLを使えない状況は2個あって、1個はそもそもユースケースでしかAPIを作れないケース。 もう1個は画面設計を見る一方でDBを見ながらリソースを定義しに行く場合で、 使い捨て前提ならユースケースベースの画面にAPIから入った方が楽なケースがあるとは思います。
視聴者からの質問に答える質疑応答タイムへ
――ここからは、視聴者の方からの質問に回答していきます。まず1個目の質問ですね。 「GraphQLでN+1を解決する場合、Data Loaderを使わずにどのように解決するのでしょうか」とのことで、これは内山さんの資料にありましたね。
内山: 基本的にはN+1を解決する場合はData Loaderやその手のバッチ処理が必要になりますが、N+1を放置する方がいることを僕は最近知ったんです。 Data Loaderって実装次第ではオーバーヘッドがそれなりにあるんですね。その待ち時間がN+1にかかるパフォーマンスの悪さとどちらが悪くなるのかをちゃんと計測して、そこにData Loaderを入れるのか入れないかの判断が必要で、場合によってはN+1の方が良くて、Nが2とか3なら大したことないという話がありました。
―― 続いて「認可周りはどうやって実装していますか」ということで、 松本さんいかがでしょうか。
松本: 認可はディレクティブとかで結構やってますが、問題とかは全く無いですね。
―― 内山さん、コメントで「ディレクティブをやってもヒットしなかった」と書いてありますが、実際どうやっているかを最後に聞ければと思いますが、いかがでしょうか。
内山: Resolverの中に入ってからビジネスロジックとして認可を書いたりとか結構泥臭くやってます。認可と言ってもどのレイヤーで止めたいのかが微妙にユースケースによって変わるんです。 なので、場合によってはMutationとかQueryのAuth ErrorみたいなResultを返している部分もあるし、Resolverの中に入ってから 「このユーザーは権限が無い」と言って止めることもあります。認可は割とケースバイケースになりがちで、個別にやるしかないと個人的には思います。
―― 最後の質問です。 「テストはリクエストベースでやるのか、モデル含めてどうテストしてるのか」とのことで、 内山さんからお願いします。
内山:実際にGraphQL QueryやMutationを投げて意図した通りの結果が返ってくるかを、リクエストスペックを書きながら検証しています。 GraphQLのレイヤーとビジネスロジックのレイヤーを分けて、モデルはモデルとしていつも通りテストするように意識しています。
――続けて松本さんいかがでしょうか。
松本:うちも結構似てますが、テストに100%工数を割けてない実態もあるので、 ユースケースとかユースケースのサービスのレイヤーに絞って今テストをしていますね。 なので、GraphQLだからこうしてるという感じではないです。
――続いてqsonaさんいかがでしょうか。
qsona: 基本的にサーバーサイドのテストはリクエストにいかにもフロントエンドが投げそうなクエリを書いてチェックしているので、 結果をアサーションする必要はなくて、型が間違ってたり NOT NULLのはずがNULLになっていたりする場合はリクエストが成功しないんです。
ただ、お悩みポイントとしては投げる側です。 タイプごとのテストはちょっと書きにくいので、ペアレントオブジェクトを指定してタイプごとにテストを書いて、 1段階目までちゃんとresolveされれば良しとしています。
――それでは最後に株式会社overflowさんからのお知らせをお願いいたします。
大谷:弊社では、ITエンジニア、デザイナーの副業転職サービスOffersを提供しております。現在登録者限定で過去のOffersのイベントアーカイブ動画も全て見放題となっております。
また、開発生産性を可視化するサービスOffers MGRも提供しております。あらゆる開発ツールとデータ連携を行うことで、組織のコンディションや現場の状態把握が可能となります。
14日間無料のトライアルプランもリリースしております。ぜひこの機会にご活用ください。
――本日のイベントはこちらで終了とさせていただきます。 登壇者の皆さん、視聴者の皆さんありがとうございました。