アーキテクチャ設計時のさまざま分割単位
アーキテクチャ設計時といえば、どのような単位で分割していくかは設計の肝となります。
マイクロサービス vs モジュラーモノリス vs モノリシック、ManyRepo vs MonoRepoなど、さまざまな観点で線を引くことが可能ですが、これらは混合されることが多々あります。
たとえば「マイクロサービスだからリポジトリを分けている」のような意見を聞いたことがありますが、これらは別のレイヤーの話です。
仮にマイクロサービスを選択するにしても、リポジトリについてはフラットに考慮する必要があります。
この記事では、より整理した形でアーキテクチャ設計についてまとめていこうと思います。
どこで線を引くことができるか
アーキテクチャ設計において線を引けるポイントは、以下の4つが考えられます。
- インフラストラクチャ
- ライブラリ
- ディレクトリ
- リポジトリ
インフラストラクチャ
誰もが頭に浮かぶのはマイクロサービス vs モノリシックの対立でしょう。
マイクロサービスという概念が出てきてから5年以上経ち、さまざまな知見が共有されてきました。
とはいえいまだに新規サービス開発時には議論の的となる、流派の分かれる領域でもあります。
ただマイクロサービス化の議論の際に、メリットとして語られる点は必ずしも「インフラストラクチャの分割」出なければいけないというものでもありません。
たとえば書籍『マイクロサービスアーキテクチャ』ではマイクロサービス化のメリットとして組織面の一致や合成可能性なども語られていますが、単一のインフラストラクチャのままライブラリを分割したり、リポジトリを分割することで成しえることは可能です。
むしろ純粋に『インフラストラクチャの分割』だけを考えたときに、分ける主たる理由はほぼサービスのスケーラビリティのみとなるのではないでしょうか。
つまるところサービスの負荷が耐えられなくなったときに検討すべき点であり、初期からマイクロサービス化するのはアンチパターンとされている現代のトレンドにも一致します。
ライブラリ
プログラミング言語によって呼び方は異なりますが、JavaScriptにおけるパッケージであったり、Javaにおけるライブラリであったり、RubyにおけるGemであったり、バージョン管理できる単位のことです。
Shopifyがマイクロサービスからモジュラモノリスへ移行した記事も話題になりましたね。
ライブラリが分かれていることで異なるライブラリへの依存関係が複数ある際に便利になりますし、依存関係がシンプルになることで依存先の変更可能性も上がります。
たとえばReactのバージョンを上げる際に、影響範囲が10ファイルなのか、100ファイルなのか、1000ファイルなのかで変更コストは大きく異なります。
10ファイルごとにライブラリが分かれていれば、その10ファイルの動作さえ担保されていれば気軽に更新ができるわけです。
とはいえライブラリが分かれていることで開発体験が落ちる側面もあるため、分ければ分けるほど良いわけではありません。
JavaScript/TypeScriptであればlernaやturborepoのようなツールもありますが、そうしたツールを使い開発効率を低下させない仕組みは導入したほうが良いでしょう。
ディレクトリ
一番楽な分割単位はディレクトリでしょう
モジュラモノリスであれ、細かく分かれていることは多少なりとも開発効率の低下につながります。
実際にジュニアエンジニアが多いチームでturborepoを使った開発を行った際に、シニアエンジニアの介入が必要になるケースは度々ありました。
ディレクトリを分かれているだけというのは誰にでも理解しやすく、扱いやすいのは間違いありません。
ただCIやデプロイが全部実行されてしまい変更箇所に対してのCI/CDコストが大きくなってしまう問題が起きてしまうなど、ある程度の規模になったらDXが悪化してきます。
私はTypeScriptであればturborepoで初期からある程度packageを分割するほうが好みですが、とはいえまずはディレクトリの分割だけで本当にダメなのかを検討してから始めるのが良いように感じます。
リポジトリ
最後がリポジトリです。
個人的にはほとんどのケースにおいてモノリポを推しているんですが、モノリポについて誤った意見を聞くことがあったので、よく聞かれた質問ベースで記述していきます。
CI/CDが柔軟にできないのではないか?
たとえばGithub Actionsでは、特定のディレクトリ配下が変更されたときのみ実行するActionを定義することができます。
そのため変更されたサービスやライブラリのみテストを実行したり、デプロイすることは可能です。
むしろturborepoなどのビルドツールを使用すれば変更されたpackageの依存グラフを見て、それに依存しているpackageでビルド等を実施することができるため、より柔軟性の高いコントロールができる印象です。
デプロイの際もデプロイの順番をコントロールすることができ、『バックエンドのデプロイ前にフロントエンドをデプロイしてしまってエラーが発生した』などのミスも防ぐこともできます。
つまり横断的なフローの自動化がよりやりやすくなります。
もちろんマージトリガーでデプロイする場合は『モジュールAとモジュールBを変更したけどモジュールBだけデプロイしたくない』『今回のデプロイはモジュールBから先にデプロイしたい』みたいなケースは対応できないため、過度な自動化により柔軟性が落ちる側面があるのは間違いありません。
ただその柔軟性は人的ミスの温床にもなるため必ずしも柔軟であれば良いとは言えませんし、モノリポであっても個別にデプロイできなくなるわけではありません。
レビュワー選定の自動化がしづらいのではないか?
CODEOWNERSファイルで設定することでディレクトリ単位でコードレビューのルールを設定可能です。
複数チームのPRが混在して見づらいのではないか?
そういった側面があることは否定できません。
ただPull Request Labelerアクションなどで、「特定のディレクトリ配下に変更が加わった際にラベルをつける」とすることでPull Requestにラベリングすることができます。
ラベルでフィルタリングすれば実質リポジトリが分かれているのに近しい体験は得られるでしょう。
むしろ関連した変更が1つのPull Requestに含まれることで、Atomicに変更が追いやすいメリットのほうが大きいと感じます。
すぐに限界がくるのではないか?
自分の経験上限界が来たと感じたことがないので、どこで限界が来るかはわかりません。
コードベースが巨大になりすぎてcloneやpushにめちゃくちゃ時間がかかる、みたいになったらもう限界かもしれません。
ただGoogleの規模でさえモノリポで10年以上運用できてるので、少なくともベンチャー企業の規模でそれを懸念する必要はないのではないとは思いますし、限界が来れば分割すれば良いのではないでしょうか?
ではどのようなケースで分割すべきか?
リポジトリごとに閲覧権限を管理したいケースかなと思っています。
たとえば『インフラの構成管理は社員にしか見せたくない』みたいなケースはモノリポでは実現不可能です。
もちろん確実にコードが共有されない全く異なるサービスにおいても分割しても良いかもしれませんが、1サービスしか運用してないベンチャーとかであれば1企業1リポジトリで十分ではないかと思っています。
おわりに
当然ですがアーキテクチャ設計の分割単位については、各チームやプロジェクトの状況により異なるため、一概に何が正解とは言えません。
特に細かい分割単位はより複雑になるのが課題ですが、周辺ツールの発展によりそのペインが軽減されることでほぼ利益のみ享受できる状況もあります。
インフラストラクチャ、ライブラリ、ディレクトリ、リポジトリそれぞれのレイヤーごとにその特性やメリット・デメリットを理解し、適切な分割単位を選択することが求められます。
Discussion