freeeにおけるモジュラモノリスの事例を大規模プロダクトから新規プロダクトまで紹介します。
深いドメインと統合型経営プラットフォームを支える モジュラモノリスの事例 2023.09.12
View Slide
2● 93年生まれ、趣味はゲーム (スマブラ, Apex) ● ヤフー 新卒入社 ○ ヤフーショッピングのモダナイゼーションに従事。モノリスをマイクロサービス化。 ● freee 中途入社 ○ freee人事労務の開発を担当。モジュラモノリス化に貢献。 ● SREへ社内留学 ○ インフラ構築やSLO導入を通してEnabling SREを推進。 ● 金融開発部へ異動 ○ クレカ事業「freeeカードUnlimited」や新規プロダクトの開発に従事。現在はEMを担当。 小倉 陸 (ogugu) 金融開発部 エンジニアリングマネージャー プロフィール画像の トリミング方法 @ogugudayo
301 freeeのプロダクトとモジュラモノリス 02 なぜモジュラモノリスか? 03 モジュラモノリスをどう設計するか? 04 各プロダクトの課題と展望 05 さいごに 目次
4freeeのプロダクトとモジュラモノリス
基盤系マイクロサービス(認証、認可、課金…)freeeカード Unlimited freee人事労務 振込 (仮) freee請求書 freee会計 freee販売freeeは誰でも⾃由に経営ができる「統合型経営プラットフォーム」裏側は、プロダクトごとのモノリス × 基盤系マイクロサービス で構成されるfreeeのプロダクトとモジュラモノリス ︙ ︙
6大規模プロダクトをモジュラモノリスへ freeeのプロダクトとモジュラモノリス 新規プロダクトをモジュラモノリスで freee会計 freee人事労務 振込 (開発中) freee販売それぞれのプロダクトで、協調しつつも独自に進化を遂げてきた
7なぜモジュラモノリスか?
8巨大モノリスなぜモジュラモノリスか? ● 会計・人事労務では、複数の複雑な業務ドメインを扱うようになる ● 1つ1つのドメインは、専門の資格があるような高度なドメイン ● さらに、それぞれのコード行数は約200万行前後 (*) ● 1チームがいくつもの業務ドメインを把握するのは限界だった ● そこで、組織がドメインによって分割された ● 一方で、コードベースはドメインで分割されていなかった ● そのため、お互いの変更影響を恐れながら開発していた ○ 隣のドメインは専門的すぎてまったくわからない… ○ そもそも、自分のチームのドメインも、頭が痛くなる難しさ… 「コードベースをドメインによって分割して、変更影響を明らかにしたい」というモチベーション人事労務開発勤怠管理チーム給与計算チームマスタチーム……(*) 2022年時点で、freee会計は約245万行、freee人事労務は約159万行となっている。
9なぜモジュラモノリスか? ● 1つ1つのドメインは「単体プロダクト」として売り出しても遜色ない作り込み ● いくつかの機能は「モジュール化して他のプロダクトでも再利用」したい ● freee会計からワークフロー機能を切り出す事例 ● 他のドメインから分離して実装していたが、蓋を開くと、望ましくない依存関係が生じており、ロジックの切り出しに難航 ● SQLにおいては、ワークフロードメイン以外のデータとのJOINが想像以上にあり、洗い出しと切り出しに難航 ● そのため、freee会計からのDB分離も難航した ※ 詳しくは「freee 技術の日」のスライドを参照 「将来的なマイクロサービス化に備えて、他のドメインとはしっかり分離したい」というモチベーション
10なぜモジュラモノリスか? ● 金融開発部では、クレカ事業「freeeカードUnlimited」をサービス初期からマイクロサービスで開発してきた ○ 1プロダクトをドメインで分割した5個程度のマイクロサービスで構成 ● なぜ初期からマイクロサービスか? ● リアルタイムな店舗決済(オーソリ)の可用性・耐障害性のため ● オーソリを物理的に分離して、共有インフラの弊害を解消 ● その流れで、それ以外のドメインも物理的分離をおこなったが… ○ DevOpsのコスト増加、非同期連携の複雑性、可観測性の問題… ● 「オーソリ以外は論理的分離で良かったのでは?」 ● 振込サービスの新規開発が始まる ● 法人間の振込処理は、せいぜいその日中に振込処理が終わればよい ● 物理的分離は必要ないので、論理的分離で始めてみよう ※ 詳しくは「freee 技術の日」のスライドを参照
11なぜモジュラモノリスか? ● 「モノリスで始めるか、モジュラモノリスで始めるか」 ○ アーキテクチャは、プロダクト・組織の成長に合わせて「進化」させるのがもっともよい ○ モジュラモノリスではなく、あえて戦略的に非構造なモノリスで始めるのも一考だった ● 改めて、モジュラモノリスで始めるモチベーションを考えてみる ○ 一部の機能は、他のプロダクトからも再利用できるようにモジュール化することを想定したい ○ 支払ドメインに対する他ドメインのコードの影響を特定できるようにして、支払処理のデグレを防ぎたい ○ 開発チームが6人以上になったため、Two Pizza Rule 的には、チームをユニットに分ける日もそう遠くない ○ 長期的なロードマップが明確で、今後の打ち手が決まっていた(手探りなプロダクトではない) ○ 会計・人事労務では、当時、漸進的にモジュラモノリスを浸透させるのに苦戦していた ○ freeeの意思決定の速さやプロダクト・組織の成長スピードを鑑みて、損益分岐点は早く来るだろう ビジネス・プロダクト・組織の観点で肯定的なモチベーションはある
12なぜモジュラモノリスか? ● 一方、「決済代行」領域は未開拓のビジネスなため、ドメイン分析に不安がある ○ 幸い、モジュラモノリスはマイクロサービスと異なり、ドメイン分割がDBを含むインフラまで及ばない ○ アプリケーションレイヤーの変更で済むため、ドメイン分割の軌道修正が利きやすい ○ 逆にその特性を活かして、ドメイン理解と共にモジュール分割を進化させていけるのは大きなメリット ● 後戻りができるのはどちらか? ○ 非構造なものを後から構造化するより、構造化されたものを非構造にするほうが楽だろう ○ どちらか迷ったら、まずは可逆的(後戻りができる)意思決定をするという考え方 ● まずはモジュラモノリスで始めてみよう ○ 開発とドメイン分析が進んでいった結果、プロダクトの性質上、モノリスが最適な可能性もある ○ そのときは、戦略的にモノリスに戻すことも考えよう
13モジュラモノリスをどう設計するか?
14モジュラモノリスをどう設計するか? 1. ドメインを分析し、モジュールの粒度・境界を設計する 2. コードベースをドメインによって分割する 3. ドメイン間の依存関係を定義し、違反する依存関係を検知する 4. 他のドメインから呼び出せる公開インタフェースを定める 5. データベースに対して、ドメインに基づく排他的所有権を定める 最近ではFW化されたOSSが登場しているが、今日は触れない ● Shopify/packwerk : Shopify 製の Rails 向けフレームワーク ● ServiceWeaver/weaver: Google製の Go 向けフレームワーク ● Shopifyの事例に学ぶ ○ Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity ○ [翻訳] Shopifyにおけるモジュラモノリスへの移行 - Qiita
15モジュラモノリスをどう設計するか? 1. ドメインを分析し、モジュールの粒度・境界を設計する ● 粒度と境界を正しく見極めるには、マイクロサービスの考え方を参考にする ● マイクロサービスは、DDDのコンテキスト境界が分割の基準 『DDDのコンセプトである「境界づけられたコンテキスト(Bounded Context)」は、マイクロサービスに決定的な影響を与えた。境界づけられたコンテキストという概念は、分離のスタイルを表している。』 『再利用はたしかに有益だ。しかし、… 再利用は結合という負のトレードオフをもたらす。… 高度な分離を必要とする場合には、アーキテクトは再利用よりも複製を優先する』 (ソフトウェアアーキテクチャの基礎 17章) ● コンテキスト境界とは、「特定のモデルを定義・適用する境界」 ○ 「販売コンテキスト」において「商品」は「価格」「在庫数」「商品説明」… ○ 「配送コンテキスト」において「商品」は「配送先」「壊れ物」「危険物指定」… ● コンテキスト境界の間で、モデルを再利用せずに複製することで、高度に分離する
16モジュラモノリスをどう設計するか? 1. ドメインを分析し、モジュールの粒度・境界を設計する ● 仮に、集約より小さく作ると? ○ モジュール間で1つの集約=整合性を扱う可能性がある ○ 結合度の増加とともに、特にマイクロサービスでは分散Txnを要する ○ 「マイクロサービス」という言葉を誤解して、小さく切りすぎる失敗 ● 仮に、コンテキスト境界より大きく作ると…? ○ モジュール内で複数のコンテキストを扱う可能性がある ○ モジュールの認知負荷と責務が増大する ○ 1つのチームで給与計算・勤怠管理という2つのコンテキストを扱えるだろうか? 「原則として、マイクロサービスは集約よりは大きく、境界付けられたコンテキストよりは小さくする必要があります。」 戦術的 DDD を使用したマイクロサービスの設計 - Azure Architecture Center より集約XモジュールA モジュールB コンテキスト境界▼ 集約より小さく切ってしまった例▼ コンテキスト境界より大きく切ってしまった例モジュールA コンテキストX コンテキストY
17モジュラモノリスをどう設計するか? 1. ドメインを分析し、モジュールの粒度・境界を設計する ● 様々な観点で粒度設計を見直す ○ 他からの影響を分離したい関心事はないか?(関心の分離、変更影響の分離) ○ モジュールを横断した整合性の塊が存在しないか?(密結合化・分散トランザクションの懸念) ○ 高い可用性・耐障害性が要請されるのはどういう業務フローか?(非機能要件での物理的分離) ○ 他のプロダクトから再利用したい基盤的関心事はないか?(将来的なモジュール・基盤化) ● モジュラモノリスは粒度設計の軌道修正が比較的やりやすい ○ 分離の影響がインフラストラクチャまで及ばないため、DBのマイグレーションやインフラの移行が必要ない ○ そのため、イテレーティブに改善する前提で妥協点を探るのもよい ○ 色々と書いたが、何よりもドメインへの理解が大事 ○ 泥臭く、ドメイン専門家との対話や分析→設計→開発を回さないと、ドメイン理解は進まない ■ 「決済代行の事業領域は理解したが、途中で財務会計ドメインの理解が必要なことに途中で気づき、知れば知るほどユビキタス言語を再定義したくなった」など
18モジュラモノリスをどう設計するか? 2. コードベースをドメインによって分割する ● Ruby の名前空間とディレクトリ配置による分離(人事労務の事例) ○ /app/modules 配下をモジュラモノリスの適用範囲とし、直下にドメイン毎のディレクトリを切る ■ 給与ドメインでは /app/modules/payroll とする ■ 既存の Rails Way のディレクトリと明確に区別がつく ○ さらに、クラス名などはドメイン名による名前空間を付与する ■ 給与ドメインでは Payroll::Domain::Models::PayrollStatement とする ● Go の package とディレクトリ配置による分離(金融の事例) ○ トップレベルでドメイン毎の package を切る ○ ワークフロードメインでは /workflow/… とする ○ 各モジュールのコンポーネントを google/wire でDIしていき、最終的に1つのバイナリにしている
19モジュラモノリスをどう設計するか? 3. ドメイン間の依存関係を定義し、違反する依存関係を検知する ● 依存関係を有向非巡回グラフとなるように定めて lint で縛る ○ 相互参照・循環参照によるモジュールの密結合化を防ぐため ○ マイクロサービスに切り出した際には、デプロイのデッドロックの要因にもなる ● Ruby プロダクトの事例(人事労務) ○ 人事労務テックリード keik 自作のrubocop カスタムルール ○ keik/rubocop-dependency ○ 2 で付与した名前空間に対してパターンマッチングしている ● Go プロダクトの事例(金融) ○ 登壇者 ogugu 自作の linter ○ ogugu9/depslint ○ PlantUMLで定義した依存関係とは逆向きの package import を検知 ○ Go は言語仕様で cyclic import を弾くが、パターンマッチはしない Dependency/OverBoundary:Enabled: trueRules:- BannedConsts: AttendanceFromNamespacePatterns:- \AHrMaster(\W|\z)@startuml "Modular Monolith"[workflow] --> [transfer]@enduml
20モジュラモノリスをどう設計するか? 4. 他のドメインから呼び出せる公開インタフェースを定める ● 目的は、モジュール間の依存関係を明確にすること ○ そのために、ドメインによる垂直分割とは別に、技術レイヤーによる水平分割をおこなう ● Rails プロダクトの事例(会計・人事労務) ○ ActiveRecord は association を経由して、他のドメインのデータ・ロジックに無尽蔵にアクセスできる ○ つまり、先ほど設けた依存関係の制約に対する抜け穴となる ○ そこで、Repository パターンを公開 IF として、AR をその外側に持ち出さないようにした ○ Repository は、AR をクエリビルダとして利用し、その結果を PORO などで定義したドメインモデルに変換 ○ 一方、AR の強力な開発体験を潰してしまうというトレードオフがあるので注意 ○ Rails Way では、AR にドメインロジックを凝集させることで、DBとドメインロジックが密結合になる代わりに、強力な開発体験を得る
21モジュラモノリスをどう設計するか? 4. 他のドメインから呼び出せる公開インタフェースを定める ● Go プロダクトの事例 (金融開発) ○ 元々、金融開発では Clean Architecture に基づいた技術レイヤーによる水平分割が浸透していた domain domainmodule A◯✕✕module Badapter adapter○ 最も内側のレイヤーから順に、以下のように package を定義 ■ domain : 業務ロジックを実装する ■ usecase : domain層を組み合わせてユースケースを実現する ■ adapter : 外部コンテキストや永続化層とのやりとりを行う ○ そこで、公開インタフェースを adapter に置くようにした ○ 他のドメインから参照する際は、同じく adapter からのみ参照できるようにして、依存関係を明確にした ○ 他のドメインのモデルは、 adapter の中で自身のコンテキスト向けのモデルに変換 ○ DDDの「再利用より複製」の考えを適用したといえる
22モジュラモノリスをどう設計するか? 4. 他のドメインから呼び出せる公開インタフェースを定める ● ただし、技術レイヤーが増えることによる認知負荷のトレードオフはある ● A Philosophy of Software Design にある 「浅いモジュール」に陥ることもある ○ ソフトウェア設計についてtwada技術顧問と話してみた 〜 A Philosophy of Software Designをベースに 〜 - NTT Communications Engineers' Blog ● 金融開発の例 ○ ある adapter の1処理をそのまま呼び出すだけの usecase 層が存在した ○ Clean Architectureを導入した目的に立ち返る ○ ドメインロジックを外部コンテキストやUIの関心事によって汚さないこと ○ 「外→内」の依存関係さえ守れていれば、適宜 usecase 層はスキップしていいはず レイヤー化の目的を理解して、形骸化したアーキテクチャを実践しないことが大事
23モジュラモノリスをどう設計するか? 5. データベースに対して、ドメインに基づく排他的所有権を定める ● あるデータを触れるドメインを定義して、それ以外のドメインが触れない仕組みを作る ○ データベースを介する結合によって、後々のDB分離が困難になるのを防ぐ ○ 一方、別ドメインのデータを参照するのに実装コストが生じるトレードオフがある ● 始めから単一DBではなく、モジュール毎にDBを分割する手段もある ○ が、境界・粒度の見直しの際にマイグレーションが必要になる ○ 不安ならまずは単一DBで始めたほうがよい
24モジュラモノリスをどう設計するか? 5. データベースに対して、ドメインに基づく排他的所有権を定める ● 会計の事例 ○ ActiveRecord の CODEOWNER を、該当するドメインチームの Github Team にする ○ danger/danger で CODEOWNER が未割当な ActiveRecord があれば WARNING する ● 金融開発の事例 # .tbls.workflow.ymlname: ワークフローモジュールinclude:- transfer_applications- application_comments#...○ k1LoW/tbls というCLIを用いて、テーブル定義からER図を自動生成 ○ モジュールごとにER図を分けるため、config もモジュールごとに用意 ○ 設定の中で、出力したいテーブルを列挙する ○ どのモジュールにも設定されないテーブルがあれば、CIを失敗させる ○ これによって排他的所有権の設定を担保している
25モジュラモノリスをどう設計するか? 目的とトレードオフを理解して、組織でどれをどこまで実践するかを見極めよう ● 1. ドメインを分析し、モジュールの粒度・境界を設計する ○ → イテレーティブに見直していく前提で妥協点を探るのがよい ● 2. コードベースをドメインによって分割する ○ → やらないと意味がない ● 3. ドメイン間の依存関係を定義し、違反する依存関係を検知する ○ → コードを分割しても、これをしないとモジュール性が破綻しうる ○ → 実装・レビューで見落とさなければ不要 ● 4. 他のドメインから呼び出せる公開インタフェースを定める ○ → モジュール性が高まるが、ドメイン横断の実装コストとレイヤリングによる認知負荷が生じる ○ → ActiveRecord を遮蔽してモジュール性を担保するのもよいが、 ActiveRecord のメリットを潰すことになる ● 5. データベースに対して、ドメインに基づく排他的所有権を定める ○ → モジュール性が高まるが、ドメイン横断のデータ取得が難しくなる ○ → 将来的に切り出しを想定しているなら、DB分離が困難になるため、あらかじめやったほうがよい 特に、4, 5 はプロダクト・組織・ビジネスニーズに応じて選択するとよい
26各プロダクトの課題と展望
27各プロダクトの課題と展望 ● モジュラモノリスの導入によって… ○ お互いの変更に怯えることなく、ドメインチームが自律的に開発できるような土台が整った ○ マイクロサービス切り出しに向けた準備が整った ○ モジュラモノリス自体が、ドメインとその境界を意識するきっかけになった 一方、進めていくと課題はある
28各プロダクトの課題と展望 ● 機能開発を進めると、思わずモジュール間が密結合になることがある ● 金融開発での例 workflow○ workflow モジュール: 振込申請のワークフローを扱う ○ transfer モジュール: 申請承認後の支払処理を扱う ○ 要件:「支払処理のステータスを申請に紐づけて見れるようにしたい」 ○ 支払処理が進むごとに transfer → workflow へステータスを同期したことで密結合に… ○ 「transfer と workflow は1つのモジュールにまとめるべきだろうか?」 ○ 「いや、これは本当に必要な依存関係なのか…?」 ○ 知りたいときだけ workflow から問い合わせれば十分だろう ○ さらに、本来、支払処理は他の関心の影響を受けたくない ○ その意味でも、依存関係の方向は transfer ← workflow のほうがいいのではないか ○ …といった具合に、依存関係を見直すことになった transfer必然的な依存関係か、偶発的な依存関係かを見極めることが重要workflowtransfer都度、ステータスの同期必要に応じたステータスの問い合わせ
29各プロダクトの課題と展望 ● 大規模なモノリスのモジュラモノリス化がなかなか進まないという課題 ● freee会計 ○ 技術レイヤーの水平分割を作り込みすぎてしまった ○ 「浅いモジュール」に見る「認知負荷の増大」というデメリットが目立った ○ 現在はアーキテクチャをより簡略化して、移行のハードルを下げている ● freee人事労務 ○ Rails Way に浸ってきたので、マインドはそう簡単に変わらない ○ そこで、既存の Rails Way から飛躍しすぎないアーキテクチャを心がけた ○ ただ、それでも浸透させるのは難しかった ○ そこで、品質改善チームがオーナーとなり、各ドメインチームとのコラボ開発を通して Enabling を実施 ○ 現在では、モジュラモノリスに則ったコードが一定増えてきている
30各プロダクトの課題と展望 ● そもそもシステム全域に浸透させる必要があるか? ○ ビジネス・開発組織のニーズ次第 ● freee人事労務 ○ freee会計とは異なり、組織・ビジネスの都合上、マイクロサービス化の必要性が薄くなった ○ ドメインが深い部分は、引き続きモジュラモノリスを採用したい ○ 設定系などのシンプルな CRUD は、Rails Way を活かしたほうがよい面もある ○ 今後は、目的に合わせたアーキテクチャ選択を啓発していく予定 ● アーキテクチャを独自に作り込みすぎないこと ● 適切に技術支援(Enabling)すること ● アーキテクチャはあくまで手段であって、それ自体が目的化しないようにすること
31さいごに
32さいごに freeeのエンジニア組織は 「スモールビジネスを、世界の主役に。」というミッションを実現するため、 高い品質で、スピーディにモノづくりできるよう、心がけています。 ご興味あればぜひカジュアルにお話しましょう。