自己紹介とセッションのあらすじ

Ken Ogura氏(以下、Ogura):ではOguraから「『Rust製の業務WebアプリケーションをRustでリプレイス』のドサクサでBFF/FEを大整理した話」をします。

私の自己紹介。フロントエンドエンジニアをやっています。もともとはバックエンドでRustバリバリ触ってたんですが、いろいろな事情で今はフロントエンドを触っています。あと趣味でいうと競技プログラミングが得意です。あとYouTuberもやってます。

私の(発表)時間はちょっと短いので、早めにスススといきます。あらすじでいうと、受発注プロダクト(として)、もともとリプレイスする前のものがありました。BFF(Backend For Frontend)とフロントエンドの開発体験がちょっと下がり始めていたというのがあります。そこでバックエンド刷新することになり、BFF/FEもこれみよがしに整理してやろうとなったというのがあらすじです。

(スライドを示して)今回話すのはここなので、Rustの話はこれからしばしばなくなってきますが、ちょっと我慢してください。

大整理1:GraphQLをコードファーストに統一

まずやった大整理その1です。「GraphQLをコードファーストに統一」というものがありまして。GraphQLに馴染みがない方のために、あとBFFという概念にもあまり馴染みない方もいるかもしれないので、そこも含めて説明します。

(スライドを示して)まず、一般にAPIってこういうかたちをしていますね。クライアントがあって、サーバーがあって、リクエストを飛ばせばレスポンスが返ってくるというふうになっています。

この2人がちゃんと会話ができるためには、間にスキーマが必要です。要は「リクエストはこういう型でお願いします。そしたらレスポンスこういう型で返します」みたいな情報があって、クライアントとサーバーがこれを参照することによって、ちゃんと整合性の取れた会話が取れるわけです。(スライドに)描いてあるのは抽象的な図です。

実際のリプレイス前、リプレイス後も同じですが、このようなスタックでやっていました。

左にあるのがフロントエンドです。右にあるのがBFF。BFFは今はただのサーバー、バックエンドだと思っても大丈夫です。GraphQLというフレームワークを使ってコミュニケーションを取っていました。

リプレイス前のプロダクトだと、最初はこのような依存関係でした。つまり、GraphQLのスキーマを書くわけですね。「こういうクエリをしたら、こういうリクエストを返しますよ」という型を書いてあげて、その定義したファイルをフロントエンドもバックエンドも参照して実装していくわけです。バックエンドはそのとおりにサーバーの処理を書いていくわけです。こういうのをスキーマファーストといいます。

この逆で、バックエンド、サーバー側がそのサーバーのコードを書くと、そこの型情報からGraphQLのスキーマを作ることもできるわけです。こういったものをコードファーストと言ったりしますが、諸事情でコードファーストとスキーマファーストが混ざってしまっていたというのが、リプレイス前のフロントエンド事情になっていました。

これだと依存関係がゴッチャになったり、ディレクトリ構造も少しなんか汚くなってしまって、開発体験が下がるようになっています。

「混在しているとマズいのでどちらかに統一しましょう」ということになって、今回のドサクサでとりあえずコードファーストに統一することにしました。 ポリシーが統一されて開発体験が向上しました。これは局所的に見た時の話ですね。

今回したかった話はなぜコードファーストにしたのかということで、その謎を解明するためにドメインの奥地に向かってみましょう。

なぜコードファーストにしたのか?

(スライドを示して)全体像を見てみるとこうなっています。今までフロントエンドとBFFの話しかしてこなかったんですが、本当はマイクロサービスに散らばったバックエンド群とgRPCでやり取りしてというものがあって。

先ほどとは様子が違いますが、右側もやってることは結局同じで、クライアントとサーバーがあり、あるスキーマに従ってやり取りしてるというふうになっております。

(スライドを示して)この中で、先ほどのスキーマとクライアント、サーバーの依存関係みたいなものを矢印で描いてみるとこうなるわけですね。かつてはこのような依存関係になっていたわけです。

左側のほうは先ほど説明しましたが、右側も同様にProtoBuf(プロトコルバッファ)で定義したスキーマがあって、それをBFF(が参照する)。今度はバックエンドから見たらnestはクライアントなわけですね。クライアントとサーバーが別のものを参照しているとなっていました。これで開発体験が悪くなっていました。

これをどう変えたかというと、GraphQL、nestはBFFですが、ここの依存関係をこうしたわけですね。こうするとすごく矢印の向きがいい感じがするわけです。

先ほどのスライドでも出てきたある図が思い浮かびますが、同心円を描きたくなるわけです。

こうすると、依存関係がすべてプロトバフの方向に向いてくれます。依存の方向性が統一されるのは、すごくクリーンな感じがしますね。いわゆるクリーンアーキテクチャというものも、「依存性を一方向にしましょうね」という思想が元ですから。

今回はアーキテクチャではありませんが、依存する先が1ヶ所ということはいいわけです。フロントでもTypeScriptを使っているし、バックでもRust使っているということで、複雑なドメインモデルに対応するためにすごく型が強い言語を採用してる節があります。

この時に大事になってくるのが、全体で一貫性が取れてることです。これが先ほどみたいに定義が2ヶ所でされており、その間の整合性を取らないといけないとなると、まぁコンパイルが結局全体で通ってればいいですが、通すのが大変になってきます。

それに対して一方向性が保たれていると、プロトバフをいじれば、そこに向かって矢印が伸びてる人はみんな巻き添い食らうわけです。それを順番に、前のほう、中心のほうからエラーを直していけば、ちゃんと筋の通ったプロダクトができるようになっていて、これもある意味生産性向上につながりました。

というわけで、このようなものを意識してバックエンドの人とコミュニケーションすると、結局自分たちに返ってくるんだとわかってうれしかったという話です。

今回の大整理1のまとめをすると、実装方法のポリシーが統一されてうれしいというのもあるけれど、構造がクリーンになった。そのため、責務の切り分けなどを意識しやすくなるわけです。で、すごくよかったという話がありました。

大整理2:gRPCスキーマとBFFを同じリポジトリで管理

大整理その2。もう1個、大整理の話をします。リポジトリを1個にしたという話。モノレポ化して開発体験が向上した話があります。

そんなに深く話しませんが、(スライドを示して)もともとこういうふうに分かれてました。特に分かれてた部分が、gRPCのスキーマとバックエンドとフロントエンドたちが別々のリポジトリに分かれていて、お互いにサブモジュールのかたちで参照しているようになっていたんです。

正しい状態だとは思いますが、更新が大変なんです。gRPC、プロトコルバッファの定義を変えたとなったら、他の2つのリポジトリでもサブモジュールの更新をしなければいけません。

それを忘れていて「動かない」と言って「なんでだろう」と調べてみたら、ただ更新していなかっただけというのはちょっとゲンナリします。

それを忘れてなかったとしてもPRが3倍発生するので、すごく大変です。レビューの時間が取られちゃうと。CIなどもちょっと複雑になってきたりして、なんか嫌だなーとなってたところでモノレポ化して、すごくすっきりしたという話がありました。

この時の教訓ですが、クリーンアーキテクチャみたいなものとか、いろいろ話されてる、名前がついてるパターンに従って教科書的なサービス作りをしようとしていますが、そこでよく引っかかるのが、モノレポ化と疎結合性は相対する。反対の概念なんじゃないかという気がしますが、実はそうじゃないということが今回の統合でわかりました。

というのも、Backend for Frontend(BFF)をバックエンドとフロントエンドの間に持つのは、ある意味「疎結合な状態を保ちましょう」という気持ちの表れです。「それを全部一緒のリポジで管理するというのはどういうことだ」となります。

そのあたりのサービスの立ち上げなどは、CIで出し分けてるわけです。デプロイする時は完全に3つの仮想化された違うイメージが吐き出されて、それが別のコンテナで立っているわけですから。サービスとして動いてるものは疎結合になっています。

それに対してリポジトリが一緒かどうかは、そのサービスの疎結合性とはあまり関係ないですよね。ちょっと気を使う必要はありますが、別に保ちながらモノレポであることはできるので、疎結合にしないといけないということでサブモジュール地獄にする必要はないということを、今回学びました。

GraphQL周りのその他の整理

その他にも整理をしています。GraphQL周りの整理が多いです。

今までデファクトリになってるコンベンションがいろいろありましたが、それにあまり従わずにgRPCと同じ名前を付けるようなことをやって読みにくくなっていたので、コンベンションに従うようにしました。

GraphQLの思想としては、オブジェクトがたくさんあって、その間のつながりを記述するという感じのイメージがあります。そうすると、バックエンドにRustのマイクロサービスがたくさん立っていますが、それをオブジェクトのつながりというかたちでまとめあげるのがBFFの責務だということがだいぶ明確になって。

しかも、それを意識してない人でもそこにあるGraphQLの他のクエリやミューテーションの実装を見れば、「ああ、そんなふうに書けばいいんだ」とすごくすっきりするということがあり、すごくよかったです。

(スライドを示して)真ん中のものは反省です。GraphQLの専門用語が入っていてわからない方はちょっと申し訳ないですが、GraphQLのいいところは、フィールドレベルでリゾルバを書いたり、データローダーを使ったりして、N+1問題の解決の層をもう少しフロント側に寄せることができることです。

あまりその機能を使っていなかったせいで、バックエンドにいろいろなお願いを今までしてしまっていた過去があり、それと決別できてよかったです。

他にもCIをきれいにしたり、テストをちゃんと書くようにしたり、細々とした整理をしていました。

依存性を意識すると開発体験が向上する

というわけで、そろそろ終わりなのでまとめに入ります。ドメインを型で守る時、型定義の一貫性を保つため、依存性を意識すると開発体験が向上するという話。

要は、先ほどのクリーンアーキテクチャのように、同心円があって、どこに型のオリジンがあるのかを意識して、そこに向かって依存するようにコードファースト・スキーマファーストを混ぜていきましょうということです。その結果、開発体験が実際に向上したというのが1個目。

2個目が、先ほどお話ししましたが、リポジトリをモノレポにするかを別々に分けるかということとサービスの結合度はまた別の概念だから、そこはちゃんと切り離して考えるといいよということ。

今回の私の意見としては、モノレポにするかどうかは、開発体験のウェイトを大きく検討していいです。PRが分かれちゃうと嫌だとか、シェアが面倒くさいことのほうが、サービスの結合度よりはモノレポにするかどうかに関わってくると思うので、そこを意識してみてください。

ということで以上です。ありがとうございました。