Monolith→MultiRepo→MonoRepoにいく上でのリポジトリ戦略

松本宏太氏:「Monolith→MultiRepo→MonoRepoにいく上でのリポジトリ戦略」ということでSCOUTERの「@kotamat」が登壇したいと思います。

SCOUTERの紹介ですが、転職回りのサービスを展開しています。

「SCOUTER」はエージェントを誰でもできるようにするサービスです。

他には「SARDINE」というサービスをやっているんですが、これは実際にエージェント業をやっている企業向けの業務管理ツールです。

あとは現在事前登録中ですが「back check」という、面接や履歴書ではわからない転職者のリファレンスチェックサービスをやっていたりします。

基本的に、弊社はずっとLaravelとVueでやってきていて、そのあたりの話をさせていただ来たいと思います。

今回はLaravelとVueの話は出てきません(笑)。

今日しゃべる内容としては、この「リポジトリ戦略って何?」と、「どういう戦略を今までとってきたのか?」という部分の紹介と、実際の事例、メリット・デメリットといった話をしたいと思います。

リポジトリ戦略とは何か?

「リポジトリ戦略とは?」から説明させていただきます。弊社の場合関連するユーザーがすごく多く、四すくみになっているパターンが多かったりして、それぞれがデータをやりとりする仕組みになっています。

以前「LaravelとNuxt.jsを同一レポジトリで管理する」という内容で登壇しておりましたが、やはりそれぞれのユーザーで同じドメインの処理の編集が発生するので、その領域の責務を凝縮しようと思ったときに、それぞれユーザーのリポジトリとドメインのリポジトリを分割する、みたいなことをこの前のLaravelのカンファレンスで話をしました。

これ見ていただくと線が多くて複雑な感じになっているのですが、こういう背景があったので、コードベースの拡大に伴って重複コードの扱いが大変になってきたという課題がありました。

弊社ではリポジトリ戦略をいくつか取り入れてきており、最初はこのMonolithから始まって、MultiRepoから、今はMonoRepoを検討しているような状況です。「これ何なんだろう?」ということを調べていくなかでわかったことを共有したいと思っております。

Monolithicとは何か?

「Monolithicって何?」というところに関して。いわゆる一般的な構成かなと思います。Monolithicという命名が正しいかどうかはわかりませんが、いわゆるLaravelのアプリケーションを初期インストールするときに、LaravelのComposerのcreate appみたいな感じで入れたときのデフォルトの構成です。

そうすると、ちょっと小さいですがappとか、composer.jsonだったり、package.jsonがあって。appの中にいろいろなコードが入ってたりとか、publicの中にstaticなものが入ってたりとか、なんかいろいろ入っています。

先ほどお話した関連するユーザーが多いよところに関して、各アプリケーションで当時発生した問題としては、複数のアプリケーションでコードをシェアできないという問題が発生しました。

重複するコードがそれぞれのアプリに記述されて、例えばユーザーの取得処理がそれぞれに記述されたり、求人データの取得が、それぞれを編集しないといけない感じで、1個の機能をリリースするたびにいろいろなリポジトリを参照していました。

機能の変更に伴って各アプリケーションのコードを変更しなければならないため、過去のコードがどうしても残ってしまうということが発生していたので、その後MultiRepoという戦略をとりました。

MultiRepo

これは何かというと、共通しているコードをパッケージ化します。重複しているコードは単一パッケージの中で管理して、それをcomposerやnpmが、LaravelやVueは対応していると思いますが、これのリポジトリでバージョン管理をしてやっていくみたいな構成にしました。

同じような構成ではありますが、例えばnode_modulesの中に、@babelと一緒にうちの@scouterincが、jobやselectionやstylebookがそれぞれ依存してたりとか。あとvendorの中にはPHP用のものが依存してたりという感じになっています。

これ、一見よさそうだなと思ってこの前もLaravelのカンファレンスで発表してたんですが、質問でも「これ、変更するの大変じゃないですかね?」という質問をされて「そうですね」という話になりました。

やっぱりこの開発手順はとても煩雑でした。

レビューする前は、子どものパッケージを更新して、子どものパッケージをdevビルドでpublishしてから、親パッケージをdevブランチにターゲットし、親パッケージをpushしてレビューください、みたいな感じになってしまいました。

リリースする前は、子どものパッケージのバージョンをセマンティックバージョンに編集して、それをpublishしてから、親をセマンティックバージョンに修正して、親をpublishすると。

「ダイヤモンド依存関係の闇」というものがあるのですが、ABCDという依存があったときに、BとCがDに依存していて、Bの更新頻度高いと、D.1が例えばVueだったときに、2.6に上がっていて、Cは2.3のままみたいな感じで、「Aって何のVueのバージョンを参照してるの?」みたいな感じで闇になるということが発生します。

MonoRepo

大手企業ではこういったことが発生してきているので、最近ではありますが、MonoRepoという考え方がメジャーになってきました。

思想としては「1つのリポジトリに全部完結させる」というもので、パッケージの概念は今までどおり残りながら、Gitのリポジトリは1つにします。ルートのアプリケーションは、先ほど言った企業側のアプリケーションも1つのパッケージとして扱って、全部リポジトリに入れます。

構造としてはこんな感じになっていて、package1とかpackage2とかがそれぞれcomposer.jsonとかpackage.jsonを持っていながら、ルートのところにもcomposer.jsonとかpackage.jsonがある形になっています。

ルートのほうには全体のパッケージ、例えばVueとか、それぞれがVueに依存しているのでそういうところを入れつつ、パッケージごとの細かい依存はこっちに入っているみたいな感じになっています。

モノリポ化をすることによって解決できることにはこのようなことがあると思います。

バージョンが単一化させれてダイヤモンド依存の解決ができるところだったり、コードの共有のところ、例えばstylebookとか、そういうところをいろいろなリポジトリで広範囲に適用できたり。あとは依存性の管理が簡潔になったり、全体に波及する部分的な変更が容易になります。

あとは、大規模なリファクタリングやモダン化をするためのツールがだんだん作れるようになったり、ほかのチームとの連携が柔軟性が増したり連携できるようになります。

依存性管理の簡潔化

ちょっと紹介すると、依存性管理の簡潔化で、もはやセマンティックバージョンを管理するのは不要になります。都度都度のテストを通れば最新を使うようにすればいいです。

OSSとかは、外部に公開するパッケージに関しては、バージョンを切っていたりします。Laravelだと「illuminate/」というパッケージがRead Onlyのリポジトリで公開されたり、Babelだと「@babel/babel/」が公開されています。

あとは、すべてのパッケージにまたがる機能の名前空間を変えようと思ったときに、例えば「job」を「jobs」にすると、この変更自体はむちゃくちゃ簡単なんですが、すべてのパッケージがそれを依存していて、それぞれを変えているとめちゃくちゃ時間かかったりします。

1つのGitのリポジトリになるので1つのcommitで全部変えられます。こうすることで一斉置換してもテストは落ちないので、一気にリファクタリングができます。

あと、うちはまだやらないとは思いますが、Googleなんかはコードの解析をやっていて。どういう変更がバグになりそうかを機械学習したり、どういう言語がどこのプロジェクトで使われているかとか、脆弱性のパッチの適用が、1つのリポジトリで管理すると可視性が高くなり、やりやすくなります。

あとは、チーム間でいろいろなリポジトリを使っていて、1つの機能を作るにあたっても様々なパッケージに依存しなきゃいけない場合、例えばこれで言うと、求人と管理者がそれぞれのチームで使っていて依存していますが、1つの施策ごとのプルリクを作れるようになるので、境界がすごく柔軟に変えられたりします。

3つのデメリット

「デメリットってあるの?」というところについては、1個目がやっぱりコードベースがでかくなるところです。

どうしてもコードベースが大きくなっちゃうので、そこはツールの導入や規則で防ぐ必要があると思っています。

Google製のツールでいうとBazelがあったり、変更のあったところだけをビルドして、テストして、デプロイするといったことをしたりとか、cloneするときは先頭だけ引っ張ってくるようにするといった工夫が必要になります。

また、依存性の解決が簡単になりすぎる問題を挙げられています。簡単に依存性を追加できるようになってしまうので、適当に「依存性入れようぜ」みたいな感じで追加されてしまう懸念があります。最低限の依存だけに絞っていて、「このAPIはここでしか使われないよ」みたいなことを明示しないと、よりまたカオスになります。

あと、コードが社内に公開されてしまいます。1つのGitのリポジトリになるので全部見えちゃうんですけど、オープンな会社の社風じゃないとそもそも入れられません。

「じゃあ実際どこで使っているの?」というと、いろいろな企業で使われています。

OSSでいうと、VueだったらBabelに依存してたりとか、LaravelだったらSymfonyとかに依存していますがこれらはMonorepoで構成されており、Reactもこの構成です。

便利なツール

便利なツールの説明です、LaravelとVueに近しいところをちょっと紹介しようと思います。

「Lerna」はnpmのパッケージのモノリポに適したものになっていて、すべての一斉のpublishやコミット、pushが簡単にできます。既存のGit履歴もそのまま使えるメリットがあります。

あとは、Monorepo BuilderがSymplifyというオーガナイゼーションにあります。これはComposerベースのモノリポ化ツールで、親ディレクトリcomposer.jsonが自動生成されるんですが、コミットしないようにしてね、って書いてあったりします。これも一斉リリースができます。

あとは、Laravelのコアのところで使われているのが、このsplitsh/liteです。これはGoで作られているのですが、CLIベースでコードをディレクトリごとに分割して、Read Onlyなリポジトリにpushしてくれます。OSSでも分けたいニーズあったりするので、そういうところに使えたりします。

まとめ

まとめです。

事業の特性として、この依存性がキーになってくるプロダクトでは一考の余地があるかなと思っています。うちのアプリだといろいろなパッケージを参照するので、そういうところでは割と使えるかなと思います。

あと、issueトラッカーがGitHubのissueに依存していると、そこらへんも影響されるので、開発フロー全体の設計をしっかり考えた上で導入を検討する必要があります。また、単一のリポジトリだけでいいようなプロジェクトで別にわざわざこんな構成にする必要はないなというところがまとめです。

SCOUTERではエンジニアを募集しているので、もし興味ある方いればお声がけいただければと思います。

以上です。駆け足でしたが、ありがとうございました。

(会場拍手)