苦しんでたどり着いたモジュラーモノリス
この記事は Finatextグループ Advent Calendar 2024の2日目の記事です。
試行錯誤でモジュラーモノリスを選ぶまでに至ったストーリー
プロジェクトの初期、ソフトウェアアーキテクチャを自由に選べるタイミングって楽しいですよね。プロジェクトの輝く未来に向けて、ブログや本で書かれていた新しい技術を採用して、自分の手で一から作っていくあの瞬間。
まだソフトウェアエンジニアとしての経験が浅い頃は、モダンな技術に触れてるって嬉しさでついオーバーテクノロジーになったり、プロジェクトの状況で求められているものよりも難しい技術に挑戦したりしがちです。マイクロサービス最高、DDDだー、Goだー、Rustだーみたいな、そんなトレンドを追いかけたエンジニア本意な技術選定。1人のソフトウェアエンジニアとしては、それで実務経験を積むことができて、次の会社に転職するときの武器になるから美味しいのかもしれません。
でも、それって本当にプロフェッショナルとして仕事をしたと言えますかね?
この記事は、自身への戒めも兼ねて、アーキテクチャ選定の時に注意すべきことを紹介します。これは私のチームで実際に起きた出来事であり、試行錯誤しながら進めたアーキテクチャ選定のストーリーです。
旅の始まりは突如決まったベータリリース
私たちのチームはとあるSaaSシステムを世に出すことを目指してメンバーが集まりました。エンジニアが集まってからおおよそ一年でリリースの日を迎えます。このチームの苦悩は色々あります。
プロジェクト立ち上げのタイミングでは、自社で運用するサービスを開発する想定で開発を進めていました。ですが、想定していなかった早さで案件の獲得に成功し、その案件に合わせてサービスを提供する方針に変わり、急遽ベータリリースをすることになりました。
プロジェクト立ち上げの時に設計の基礎を築いたリードエンジニアは始まって半年ほどで転職。穴を補うために入った経験豊富なエンジニアは他に注力することがあり、リリース後にはゆるやかに離脱。途中で採用されたエンジニアはリーダー不在で方針が不明瞭なまま、何日にもわたって設計の議論。その結果ふわふわとしたアーキテクチャのままリリースまで残り4ヶ月。何もできていない状況のやばさから、とりあえず動くものを作ろうと、お互いの様子を探り合いながら開発が進んでいました。
私がチームに入ったのはリリースの2ヶ月前。リリースを目前に控えながらもまだまだできてない機能だらけで、炎上真っ最中でした。エンジニアは5名で皆がそれぞれの担当する機能開発に奔走し、チームが集まる場では互いの進捗確認だけ。なんとかリリースに必要な機能を揃えるために皆が必死に開発してました。
あるときどうやってもリリースには間に合わないから、ベータ版ではビジネス要件検証のための暫定実装として、本サービスはその知見に基づいてアーキテクチャを根本的に見直すという意思決定をしました。ベータ版のリリースに向けてデリバリーを最優先するというものです。
- 設計の綺麗さはある程度の妥協をして、一人一殺でガンガン機能を開発する。
- 一定のテストコードは書きつつも、スピード重視のコードレビューを通してマージする。
- AuroraDBを使ってる箇所もあればDynamoDBを使ってる箇所もあるのを許容する。
- 全体のアーキテクチャを議論する時間など作らず、エンジニアが思い思いに爆速で開発する.
こんな方針です。
チームとしての初めてのリリースであるため、プロジェクトチーム全体としてなんとかリリースだけは成し遂げようと苦渋の決断をしました。
結果としてなんとかリリースにこぎつけたものの、俗人化したコード、統一感のないアーキテクチャ、疲弊したムードが残り、エンジニアとして敗北感を味わうような状況でした。
本番サービスを見据えたリアーキテクト
一定の運用が回り始めたタイミングで、チームには本番を見据えてアーキテクチャを見直す時間がやってきました。
当時バックエンドでまともに設計できるのは技術に尖ったエンジニア歴20年のAさんと、エンジニア歴5年の私でした。私は過去に0-1フェーズのアーキテクチャ設計から開発した経験と、DDDをチームに導入してテックリードになった経験がある程度で、Aさんと比べるとまだまだ未熟なエンジニアでした。それでも当時のチームの中では私も比較的バックエンドのスキルが高い部類であったことから、Aさんと私がソフトウェアの設計を見直すことになりました。
技術に精通してるAさんは、ベータ版時代のアーキテクチャの問題がコードの再利用性が乏しいことにあると考えて、いかに再利用性の高いソフトウェアを作るかに集中して設計をしていました。私は他にも課題があることを思いつつも、入って3,4ヶ月程度ということと経験の差から、Aさんが考えてる方針で進めるのが良いと考えて、その方針に対して深く議論することなく設計を進めました。ただ設計の大きな方針はわかるものの、私自身は具体的なアーキテクチャのイメージは湧いていなかったため、アーキテクチャの設計はAさんに任せて、私はDBの設計や周辺ツールの環境構築に取り組むことにしました。
後で分かったことなのですが、AさんはGoについては深い知見があるものの、ソフトウェアアーキテクチャ全体に対してはあまり専門的な知識を持ち合わせていませんでした。過去の経験や、ベータ版時代の経験から、再利用性の高さが重要だと考えているだけであり、考え出したアーキテクチャは全て自分の頭で生み出したものでした。
実際にAさんが考え出したものは、技術レイヤーで分割したマイクロサービスでした。具体的には、Entityの永続化を担当するRepository相当のサービス、Entityの値からロジック判断をするUsecase相当のサービス、Usecaseを束ねてAPIに必要な制御を行うOrchestration相当のサービス、APIを受け付けるHandler相当のサービスに分けて、各サービスの再利用性を高めることを志向したものでした。
当時私は、まだ見たことのないアーキテクチャに心動かされ、この設計なら再利用性高く、拡張性の高いサービスを作っていけそうだと心躍っていました。自身でその設計の深く考えることもせず、ハイレベルなAさんの生み出したアーキテクチャなら大丈夫だろうと安心し切ってます。初手マイクロサービスがバッドプラクティスなことは知識として分かっていながらも、「自分たちはベータ版で作ったシステムをリプレイスするだけだから問題ない。Aさんが考えたアーキテクチャはまだ前例がないだけで、これは革新的な設計だ。」みたいなことを思ってました。
このまま進んでいたら、どうなっていたかはわかりません。
ただ、運よくそんなタイミングで、強いエンジニアBさんが私たちのチームに入ってくれました。ここから本当の設計の時間が始まります。
新しい刺激と変化
Bさんは過去に大規模のSaaS開発の経験があり、その会社の中でも組織をリードするような立場にいた人です。Bさんは技術に明るいだけでなく、ビジネスの観点も理解し、マネジメントもできるようなエンジニアです。
BさんはAさんと私から設計について話を聞き、設計のデメリットに課題感を感じ取りました。ただ入社したばかりで信頼関係もまだできていない状況だったので、今までの議論を覆すのではなく、そのデメリットを補うためにどうすべきかにフォーカスして話し合いを進めていました。
そんな自分たちのチームに転機が訪れたのは、全社へ向けたアーキテクチャ設計議論の場です。CTOや他の事業・チームのメンバーに向けて私たちからアーキテクチャを説明し、それに対してフィードバックをもらう機会でした。私たちの会社では、技術選定の裁量がチームに任せられているため、何を使うのも、何のアーキテクチャにするのもチームが決めることができます。ただ、他の事業でやってきたアーキテクチャとは全く違うものを私たちが進めようとしていたので、勉強の意味も兼ねてアーキテクチャ設計議論の場が設けられました。
その会では、「本当に長くその設計でサービスを開発する気はあるのか。」「新しい技術を導入して自己満足し、すぐに転職することを考えているようにしか見えない。」「自分ならこの設計はやらない。」というような手厳しいコメントをもらいました。
ここで私ははたと気づくわけです。
ああ、担当するサービスを本気で成功させる気持ちがあったのかと。
経験豊富なAさんに任せて自分は責任逃れをしていたのではないかと。
いざとなったらなんとかなる、波風起こさずにのらりくらりとやっていこうとしていなかったかと。
本当は、このアーキテクチャに対していけてないと思っていた自分がいたのに、それに向き合っていなかったのです。なんとも自分勝手で、恥ずかしいものです。
技術選定の裁量がチームに任せられているため、外部の人たちが私たちチームの意思決定を遮ることはできません。あくまでも、自分たちの決断が優先されます。なので、手厳しいコメントをもらいながらも、私たちはアーキテクチャのデメリットを認めつつ、そのまま進もうとしてました。
このタイミングで私はじっくり自己と向き合いました。
この意思決定に自分は後悔しないかと。
ソフトウェアエンジニアのプロフェッショナルとして誇れるのかと。
いつまで他人に甘えているのかと。
いや、このままじゃダメだ。
何も変えられなくても、少なくとも自分が後悔しない行動をしたい。
Aさん、Bさんに考えを伝えてもう一度話し合いたい。
今ならまだ取り返せる。
そう考えて、なんとも曖昧な言葉でAさんBさんに向けて次のslackを送りました。
ここから、本当に本当の設計の時間が始まります。
トレードオフと向き合ったソフトウェアアーキテクチャの設計
まず私たちはゼロベースで、何のアーキテクチャが最適なのかを考えることにしました。
それまでは経験ベース、ひらめきベースでアーキテクチャを探していたのですが、それぞれのアイデアを持ち寄って話し合っても視野が狭くなりがちです。
ここでは共通認識を持って進めるために、ソフトウェアアーキテクチャの基礎[1]を各自読んできて、それを元に話を進めることにしました。
この本で書かれている重要なテーマは次の2つの法則に集約されています。
ソフトウェアアーキテクチャの第一法則
ソフトウェアアーキテクチャはトレードオフがすべてだ。
ソフトウェアアーキテクチャの第二法則
「どうやって」よりも「なぜ」の方がずっと重要だ。
この法則をベースとして、比較のための材料がたくさん書かれています。本の詳細については割愛しつつ、ここでは私たちがどのようにしてアーキテクチャを選定していったのかを説明します。
1.モノリシックアーキテクチャと分散アーキテクチャ
はじめにモノリシックにするのか、分散にするのかを考えました。
世間的には数年前にマイクロサービスが流行って、最近はモジュラーモノリスにする人が増えてきて、それからまたマイクロサービスに取り組む人がいてなど、テックブログのソフトウェアアーキテクチャの記事は色々な変遷があります。これらのアーキテクチャ選定の背景にはそのチームの状況、予算、メンバーのレベル感などあり、さまざまな要素を勘案して選定されているものです。単に流行に乗ってやるだけでは、自身のチームにとって最適な判断であるとは言えません。
私たちも当初は技術レイヤーで分離されたマイクロサービスにしようと考えていました。
本には分散コンピューティングならではの課題として、分散ロギング、分散トランザクションが書かれています。こうした課題や利点を書き出して整理した上で、モノリシックと分散を比較して、私たちはモノリシックアーキテクチャを選ぶことにしました。
プロジェクトの状況としては本番サービスの開発に向けて1からシステムを作り直す段階にいたため、スピードを優先することにしました。分散アーキテクチャのパフォーマンス、スケーラビリティ、可用性といった利点よりも、それに付随するデメリットの方を大きく捉えたということです。
チームメンバーやプロジェクトの状況によっては違う決断をしたかもしれません。ですが、何事もトレードオフなので、当時の私たちにとってベストな選択だったと思います。
2.適切なアーキテクチャスタイル選び
モノリシックにすることを決めた後は、また本を読んでさまざまなアーキテクチャスタイルから、チームにとって最適なものを選びました。
その中で比較対象として整理したのが、レイヤードアーキテクチャ、パイプラインアーキテクチャ、マイクロカーネルアーキテクチャ、モジュラーモノリスです。
私たちのプロジェクトでは、金融のコアのロジックは非常に複雑性が高かったため、そこのドメインを分離できることが重要だと考えました。また、企業ごとのカスタマイズ機能もたくさん必要なので、頭をひねりました。
そこで選んだものが、モジュラーモノリスとマイクロカーネルアーキテクチャの合わせ技です。
- 複雑なドメインではDDDを採用してしっかりモデリングした上で開発する。
- 単純なドメインでは3層アーキテクチャでシンプルに開発する。
- カスタマイズ部分はプラグインコンポーネントにして共通インターフェースで扱えるように開発する。
というような方針です。
開発初期は標準機能の開発が優先してモジュラーモノリスでガンガン開発を進め、カスタマイズが発生するタイミングでマイクロカーネルアーキテクチャをモジュール内に取り入れていくことになりました。
※技術選定から1年程度が経過した執筆時点でもマイクロカーネルアーキテクチャは実装せずにカスタマイズ機能も組み込めているため、実質モジュラーモノリスだけで開発してます。
3.モジュラーモノリスのアーキテクチャ
残すはDBの分割やモジュール間の連携などの細かいものです。
これも選択肢を複数書き出して、それぞれのトレードオフを考えて方針を決めました。
- API単位でアトミックなトランザクションを張るためにDBは1つにする。
- モジュール間の連携は、拡張性を持たせるためにシナリオ層でハンドリングする。
シナリオ層は現場で役立つシステム設計の原則[2] にあるシナリオクラスを参考にして考え出したものです。オニオンアーキテクチャでいうところのプレゼンテーション層とユースケース層の間に位置しています。マイクロサービスの文脈でいうところのオーケストレーション相当の責務を担います。
よくあるDDD関連の書籍や記事ではモジュール間結合の話はあまり触れられていないので、ここを自分たちで考えていくのも骨が折れました。
こうして私たちはアーキテクチャのトレードオフをしっかり検討した上で、技術選定を行いました。
出来上がったアーキテクチャを見てみると、非常にシンプルで、わかりやすいですね。
その後の開発
アーキテクチャの方針が決まってからの開発は非常にスムーズでした。会話の中でアーキテクチャの課題が気になる時があったとしても、「それはトレードオフを考慮した上での決断だから今はこれでやろう。」みたいな話ができて、余計な議論に時間を割かずに済みました。
モジュラーモノリスにシナリオ層を設けたのも良い感じに機能しており、アトミックなトランザクションの利便性を享受しつつ、モジュール内でコンテキストに応じた設計を行えています。金融の複雑なドメインではモデリングと開発を繰り返して進めており、これがコアドメインに注力して開発することなんだなと実感できてます。逆に、単純なドメインでは高速に開発ができており、そのスピード感もまた開発を面白くしてくれてます。
振り返ってみると、ソフトウェアアーキテクチャをじっくりと向き合っていた、濃くて充実した時間だったなと思います。単にエンジニアの経験や知見から判断をしていた初期の頃と比べると、何倍も洗練されたアーキテクチャになりました。
私自身も、他人の考えに巻かれようと甘えるのではなく、トレードオフを考慮した上で納得感を持って決断できるようになり、アーキテクトとして一段レベルアップした気がします。
技術選定は単なる個人のスキル向上の場ではなく、チーム全体の成功に直結する重要な決断です。短期的な利益や流行に流されず、長期的な視点でプロジェクトの成功を見据えて、良いシステムを作っていきたいですね。
最後に補足
物語の演出上、登場する人たちに対して過度にデメリットばかり目立つような表現になってしまっています。実際は、めちゃめちゃ強いエンジニアたちばかりで、その人たちが書いたコードは非常に美しいです。特にAさんはバイクも乗れてコードも書ける、裁判に詳しいイケメンです。
実際に採用してる技術をかいつまんで紹介すると
- APIはProtocol Buffersで定義して、Buf CLIでビルド
- ReactとGoのフロントエンド・バックエンド間はConnectを使ってHTTPで通信
- 履歴管理にBiTemporal Data Model
モダンな技術スタックを取り入れつつ、面白みに溢れる構成になってます。こういった技術選定や、洗練されたアーキテクチャはチームで協力し合って作ったもので、互いに信頼し合えるような強いチームで動いてます。
詳細技術に関しては、長くなるので別記事 モジュラーモノリスのモジュール間連携 で説明します。
Discussion