Railsプロジェクトをモジュール分割して見通しをよくする
今年もRubyKaigiが始まりましたね!noteはrubyスポンサーとして協賛しています。三重の会場にきている方は、ぜひnoteのブースにも足を運びください。
さて、noteはRuby on Railsを用いたwebサービスとして2014年にリリースされました。現在でも継続してRailsのコードベースを利用しています。
しかし、多くの機能がリリースされ、開発者も増えたため、モノリスの巨大化が進んでおり、開発効率に影響が出始めていました。
今回はそれらの問題を解消するために、noteが継続的に取り組んでいる・取り組んできたバックエンドの改善プロセスについて説明していきます。
モジュールでサービスを構成する
モノリスは大きくなるとメンテナンスが難しくなります。Railsは、MVCの各層に全てのドメインがフラットに並び、レイヤごと・レイヤ間の結合度が高くなる設計思想で、巨大モノリス化への対処が難しい類のFWです。
巨大モノリスへの処方箋として、マイクロサービスへの移行を推進する事例が数多く紹介され業界内で認知されています。noteでもマイクロサービスへリアーキテクチャする方向性をまずは調査したのですが、現在の開発組織規模に比してオーバースペックで飛躍が大きいと判断しました。もう少し連続的、漸進的に構造変化を実施できる枠組みがないか検討しました。
結論として、モノリスを維持しつつも、内部では複数のモジュールに分解し、モジュール間のシンプルな依存によってアプリケーションを構成することを検討しました。モジュール間は疎結合・高凝集に分離し、各モジュールには担当チームが紐づいている状態を目指します。
業界的にはモジュラモノリスと呼ばれているコンセプトです。詳しくは後述しますが、開発者の認知負荷を下げて、将来の更なる根本的なサービス分割施策の布石にすることを目的にします。
noteのモノリス分割の歴史
さて、ここで一度話を変えて、noteの過去のRailsシステム分割の試みを簡単に振り返ります
データ基盤
impやクリックといったトラッキングデータの収集、分析、可視化もRailsアプリで行なっていましたが、アクセス増と業務の複雑化に対応するため、データ基盤を新規構築して移行を実施しました。2018年ごろになります。こちらは、データ関係のドメインを垂直方向に切り出した形になります
Nuxt.jsへの移行
その後、2019年頃からはviewの分離を始めました。Angular.jsで構築されたviewのレイヤはRailsに相乗りし、rubyと密結合していました。それをNuxt.jsベースのプロジェクトへ分割しました。その結果、Railsと結合していたHTML特有の処理を分離し、Railsを純粋なAPI提供の責務だけにできました。こちらはレイヤごとに水平方向に切り離した形になります
Frontend単体で機能をホスティング
また、現在では、CSR単体で提供できる機能については、jsのツールチェインでビルドしたファイルを静的配信しサブドメインなどを振ってホストすることで、Railsとは別のコードベース、デプロイラインに切り出しています。noteの新エディタや、法人向けの各種画面はこの方式で運用しています
このように、Railsの巨大化を防ぐ試みはこれまでも色々試され本番展開され、一定の成果があったのですが、それでも昨今のコードベースの肥大化はペースが早く、何らかの対応が必要でした
リアーキテクチャのための試み
モジュール化に話を戻します
モジュール化では、適切なドメインを見出して高凝集に分け、モジュール間の結合を疎にする事が重要です。
しかし、そのような条件を満たすモジュール構成と依存関係を一気に整理する事は困難です。なので、確度の高そうなドメイン境界から切り出しながら、packwerk gemを用いた静的検査でモジュール間の依存を出力し、コードとコード間の関係を理解しながら漸進的・探索的に進めることにしました。
packwerkはRailsアプリケーションのモジュール化を支援する静的検査用ライブラリです。Shopify内のリファクタリングプロセスの一環で作られたツールをOSS提供したプロダクトのようです。
https://github.com/Shopify/packwerk
具体的には…
作業自体はシンプルです。Railsのルートにモジュール用のディレクトリをつくります(eg: Rails.root/packages/) 。その配下に、切り出し対象ドメイン(eg: 会計、法人プラン管理、定期購読、バッジetc)のディレクトリを切り、関連するmodelやlibなどのコードをモノリスからそのディレクトリに移動します。加えて、モジュール用のディレクトリそれぞれにpackage.yml というモジュール間の依存設定ファイルを置くと準備完了となります。
この状態でpackwerkにかけてモジュール⇄コアモノリスの依存を出力します(class/class内の定数が対象)。packwerkで依存チェックをかけると、各モジュールのディレクトリ配下にdeprecated_references.yml という違反リストが出力されるので、こちらを参照して他パッケージへの依存の程度を確認できます。(詳しい使い方はこちら)
理想的には、モジュール間の直接依存は一方向のみか、存在しない状態を目指すべきですが、ファイル移動だけでは相互依存(循環参照)は発生してしまう可能性が高いです。まずは違反の量と質をみて結合度・凝集度の程度を現状理解し、依存解消の道筋を考えていくことが重要で、初期フェーズでは完全に無くすことにこだわる必要はないです。
出てきた依存はどう削減するのか?
依存削減の手法はいろいろ考えられますが、大まかには以下のような手段の合わせ技になると思います
packwerkで用意されているpublic interface(app/public)のみでモジュール間のやりとりを集約させ他は削除していく
各パッケージで共通して依存させる共通platform用のモジュールを用意し、enforce_privacy: false に設定する
モジュール間のやりとりを pub/sub に変更する
コードを分解して適切なパッケージに移動させる(移行過程で、この責務は違うclassに負わせたほうがいいな….と気付くことが多々あります)
欠かせないチームの協力
切り出しには対象ドメインの担当チームの協力が必要です。PoCに協力してもらったチームの尽力で徐々に本番展開も進み、現在ではコアモノリスの約20%を10個近くのモジュールに切り出しました。メンバーからも、作業対象が明快になり認知負荷が減ったなど好評を得ており、引き続きこの路線で切り出しを進め、依存関係の削減を後に本格的に進める予定です。
モジュール化の実務、Railsでの実現手法については仔細に語るとかなり多くの論点があり、これだけでnoteが何本も書けそうなくらいのネタなので、詳細は別の機会にまとめて情報共有したいと思っています。
データ構造の刷新作業とモジュール化
既存のモノリスからモジュールを切り出すだけでなく、新規機能や根本的なリメイクを行う際も、新規モジュールを追加しpackwerkで他から守ることで疎結合高凝集に作りやすくなります。
その利点を活かして、現在企画中のデータ構造を大規模に変更するリファクタリングでも新規モジュールに機能を移設して徐々に切り替えていくアプローチを取っています。こちらも進捗があり次第、別記事でご紹介したいと思います。
その他の重要なトピック
モジュール化の作業は、classの適切な名前空間化とセットで行うのが有効です。namespaceをつけてチームとモジュールに紐付け、ファイルが迷子にならないようにするのが良いと考えています
デッドコードや未使用になった仕様はモジュール化の作業でノイズになりやすいので、事前に積極的に削減しておくのが好ましいです
まとめ
マイクロサービスやSOAのような分散環境に移行するにしろ、結局は適切なドメイン分割、コード分析、チーム担当分けといった前段階の設計が重要になってくると思います。モジュール化はそういった前工程を低リスクで実施できる施策の一つと捉えています。有効に適用できるかはケースバイケースですが、一度検討してみてもよいかもしれません。