Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

技術的負債解消チームで1年半活動して分かった、動くコードを削除する大変さ

この記事はMoney Forward Engineering 2 Advent Calendar 2023の14日目の投稿です。

前日はnakamoriさんで Airflowの新機能Airflow Datasetによる新しいデータリネージでした。

はじめに

こんにちは!HiroVodka です。 私は2022年6月1日にマネーフォワードへ中途入社し、そこから約1年半の間、『桃園の誓いアーキテクチャ』と呼ばれている技術的負債解消活動を行ってきました。

桃園の誓いアーキテクチャについては、CTOのブログを読んでいただければと思います。

CTOになって5年経った感想、振り返りなど - Money Forward Developers Blog

色々あるので細かいことは省きますが、当時は一つのサービス(主にマネーフォワードME)から発行されるSQLがDBサーバーの負荷を上げてしまって全てのサービスが停止(あるいはスローダウン)してしまうということが頻発していました。 バラバラのタイミングでリリースされたサービスたちが、障害時には同時に停止することから、社内ではこれを桃園の誓いアーキテクチャと呼んでいます。(三國志をご存知でない方申し訳ありません。本題からそれるので、桃園の誓いアーキテクチャのついてはこれ以上触れません)

DB分割も終盤の局面になってきていて元々の課題であったDBサーバーの負荷で全サービスが停止するようなことはほぼ起こらなくなっていますが、創業から4年半かけて積み上げた技術的負債が5年かけても返済しきれていないという事実は考えさせられるものがあります。

『CTOになって5年経った感想、振り返りなど』から引用

このCTOのブログ記事が書かれたのは2021年12月30日です。

この記事が書かれてからすでに2年が経過し、少しずつ負債の解消ができていますが、いまだに完全には解消されていません。

なぜここまで実現に時間がかかっているのか、外部から見たら不思議に思うかもしれません。 少なくとも私はマネーフォワードに入社するまで不思議に感じていました。 ただ、実際に負債のど真ん中で活動してみないと見えてこない課題が1年半の返済活動を通じて分かってきたので、今回はその一部である『共通化されたRailsEngine分離』に関して書こうと思います。

私の所属している技術的負債返済チームに関して

マネーフォワードでは前述した桃園の誓いアーキテクチャを脱却するために、技術的負債返済専門のチームが存在します。(私はこのチームに所属しています) このチームは『わり算チーム』という名前で、共通化されたDBをそれぞれのサービスへと分割していくことから『わり算』と命名されています。

弊社の負債解消活動の進め方として、おおよそ3パターンに分かれます。

  1. 通常のサービス開発チームが、独自に桃園の誓いアーキテクチャから脱却する
  2. サービス開発チームの中に技術的負債解消専門チームが存在し、そのチームが負債解消活動を専門に行う
  3. 通常のサービス開発チームに派遣される形で、わり算チームのメンバーが負債解消活動を行う

1,2の場合はわり算チームが相談役となり、どのような形で進めていくかを議論したり、他のサービスとの調整周りをお手伝いしたりすることもあります。 3の場合は、わり算チームが負債解消方法の検討、実装、リリースまでを担当します。

共通化された RailsEngine

全てのサービスが一つのDBを参照しているということは、そのDBに対する共通の処理はどこかにまとまっているほうが便利です。 そのため、マネーフォワードには共通処理を司るRailsEngineが社内Gemとして公開されています。

参考

マネーフォワードが運営する全 Ruby on Rails アプリケーションの基板となるエンジンの開発を担当しています。 エンジンの Rails を 4.2 にアップグレードした話 - Money Forward Developers Blog

サービス初期の段階では、それぞれのサービスで共通化された同様の処理を再実装しなくて良いため、とても合理的な判断だったはずです。 ただ、それは同時に、このGemを利用しているサービスは共通DBを使い続けるということを意味します。 また、共通Gemに対して修正を行う際は、このGemを使用している全てのサービスが変更を取り込む必要があります。

公式サイトからも分かるように、現在マネーフォワードクラウドには20を超えるサービスが存在しており、作業のために必要なサービス担当者が誰なのか調べて連絡するだけでもかなり大変です。

そういった理由も重なり、負債解消活動の一つとして、共通Gemからの分離を進めました。

共通Gemから分離するということは、各サービスのGemfileから共通Gemを削除することを指しています。

この状態から

gem 'moneyforward_common' # 共通Gem

この状態になればGem分離OK

#gem 'moneyforward_common'

マネーフォワードでは、Gem分離の方法として、Gem側に記載された処理を各サービスのリポジトリに直接実装する方法を取りました。 社内では『共通Gem持ち帰り』と呼ばれ、共通Gemを使用しているサービスは基本的に全てのサービスが、この共通Gem持ち帰り作業を行うことになります。

ただ共通Gemに記載されているコードを全てサービス側リポジトリに持ち帰るだけであればそこまで工数は多くないかもしれませんが、実際に作業してみると色々な苦難が発生します。

動いているが不必要なコードを削除する大変さ

共通Gemに実装されている処理は、Gemを使用していた当時の段階で、いわゆる『各サービスが共通として実装していなければいけない処理』が多いです。 一部、特定のサービス用に実装されている処理は存在しますが、ほとんどが上記の処理です。

ただし、当時から実装や設計の方針が変わっていき、共通Gemに定義されているがサービス側が使用する必要がない処理も共通Gemに存在しています。 そのため共通Gem持ち帰りは、ただGemのコードをサービス側リポジトリにコピーするだけではなく、『使っているか使っていないかよく分からないコード』を選別する必要があります。

そのままコピーするだけでも、各サービスは動きます。ただし、必要のないコードをサービス側リポジトリに持ち帰ることで、後述するDB分割の際に邪魔なコードが増えることになります。

例えばこのようなコードが共通Gemに存在していたとします。

class User < ActiveRecord::Base
  has_many :invoice # invoice テーブルは共通DBに存在している
end

この has_many :invoice は、サービスAでは使用しますが、サービスBには必要のないコードです。

もしサービスBにこのコードをそのままコピーするとどうなるでしょうか。

コピーすること自体は何の問題もないので、サービスBは正常に動き続けます。 この後、invoiceテーブルを共通DBから削除(別サービスDBへの移動)しようとしたときに問題が発生します。

サービスBがinvoiceテーブルを使用しているのか、使用していないのかによってinvoiceテーブルを共通DBから削除できるかどうかが決まるからです。 もしサービスBがinvoiceテーブルを使用している場合、共通DBを参照しない形でinvoiceの情報を取得する必要があります。 サービスBがinvoiceテーブルを使用していない場合、共通DBからinvoiceテーブルは削除されても問題ないことになります。

そのため、不必要なコードがGem持ち帰り先のサービスに残ることで、後々の共通DB分割作業の際に再度調査の工数が発生してしまいます。 これは結局のところ、不必要なコードが『負債』としてサービス側に残り続けることとなります。

なので、共通Gem持ち帰りの際は『不要なコードをいかにしてサービスへ持ち込まないか』を考えることが重要になると感じました。

そして、個人的に思ったのは、安定して動くコードを追加することに比べて、安定して動いているコードを削除することの方が圧倒的に大変だということです。

1日かけてコードを調査し、不必要なコードを削除したところで、外から見たアウトプットは、ただ共通Gemから1つのモデルがサービス側リポジトリにコピーされただけになります。 新規のコードを100行書いたほうが、圧倒的にアウトプットを出しているように見えます。

また、調査をせずに適当にコードをコピーしても、動くことには動きます。むしろ、そのままコピーすれば今の状態と同じなので、バグが発生する確率は極めて低いです。 ただ、後の工程で誰かが調査し、結局コードを削除する必要があります。

そして、どれだけ気をつけて、テストコードを追加して、打鍵テストを実施したとしても、コードを削除することで、今現在安定して動いているサービスを壊すこともあります。 ただ、どのみち削除しなければいけないコードなので、今私がコードを削除して壊れるか、後日別の誰かがコードを削除して壊すのかの違いしかないのかなと思います。(もちろん、後で作業する人が事前にバグを見つける可能性もあります)

このようなコメントを残して、とりあえずコピーすることだってできる。

class User < ActiveRecord::Base
  # TODO: 使ってなかったら、いつか消す
  has_many :invoice # invoice テーブルは共通DBに存在している
end

コードの追加より、コードを削除するほうが大変という感覚は、実際にマネーフォワードで技術的負債返済に関わるまであまり意識したことはありませんでした。 今後コードを書くときに『このコードは必要がなくなった時、消しやすいかどうか?』という視点を持つことができたのは、とても良い経験になったと思っています。

とはいえ、妥協することも

ここまで色々と書いてきてアレですが、実際には妥協して実装することも多々あります。

一番多いのは、もともと共通Gem側に存在するコードをリファクタリングせずにそのままコピーする場合です。 共通Gem自体、開発されたのがかなり前であったり、当時のアーキテクチャにとって良い書き方をされていたりするのですが、リファクタリングが必要な処理はかなり残っています。 共通Gem持ち帰りの作業のコンテキストに『共通Gemのリファクタリング』を入れるか入れないかは、その時のリソースやサービス側の状況によっても変わります。

Kaigi on Rails 2023 の発表で『技術的負債の借り換え on Ruby and Rails update』という発表で『技術的負債の借り換え』という表現がありました。

リファクタリングせずにコードを持ち帰るのは、まさしく借り換えという表現がぴったりで、今よりかは悪くならない(共通Gemが持ち帰れる)が、負債は残ったまま(リファクタリングされていないコード)という状態になります。 もちろん、最低限自動テストを書いておくことで、後のリファクタリングをやりやすいようにするなどの工夫はしますが、毎回完璧な状態で負債を解消しているわけではありません。

今回の共通Gemへの依存を『高い金利の借金』と捉えるなら、対応方法によって以下のようなイメージになると思います。

  • 不要なコードが削除され、リファクタリングもされている状態 -> 完済
  • 不要なコードが削除されているが、リファクタリングはされていない状態 -> かなり安い金利に借り換え
  • 不要なコードが削除されていない -> 少し安い金利に借り換え
  • 何もしない -> 高い金利のまま

完璧な負債解消(借金の完済)と、少し妥協した負債解消(借り換え)をどういったバランスで行うかの共通認識を、作業者とサービス側である程度共通の認識を作っておくことが必要だと思いました。

自動テストについて

コードを削除する大変さに関して書きましたが、一つ重要な点として『信頼して本番にデプロイできる自動テストが書かれているか?』があると思います。 私が共通Gem持ち帰りを担当したサービスに関しては、一貫してテストコードがかなりしっかりと書かれていたので、その点が信頼してコードを削除できることにつながったのかもしれません。

もし自動テストが書かれていなかったとしたら、共通Gem持ち帰り作業はもっと大変になっていたはずです。

この負債からの学び

  • コードは追加するよりも消すほうが大変
  • このコードは消しやすいかどうか?という観点を持って実装することで、後の負債解消に役立つ
    • コードは追加された時点で将来的な負債となり得る
  • 後の誰かに任せるのではなく、私がやり切るという気持ちで負債に向き合う

まとめ

長々と書きましたが、今回記載したコードを削除する大変さには他の要因も含まれていました。

  • そもそもサービスのコード量が多く、また歴史も長い
  • わり算チームが作業する場合、サービス特有の仕様への理解が浅いため、調査に時間がかかる
  • 共通Gemが依存しているGemへ、サービスが暗黙的に依存していた

これらの要因も重なって、共通Gem分離には時間がかかったのかもしれません。

ここまで大きなコストを払って実施してきた共通Gem分離という作業は、実は準備作業に過ぎません。 この時点ではアーキテクチャ自体は何も変わっておらず、むしろここからが本番です。 いよいよ、私たちはここから『桃園の誓いアーキテクチャ』という偉大なる山へと登り始めます。 次に目指すべきは『共通DB分離』となりますが、長くなってしまったので、こちらについては別の記事ご紹介できればと思います。

最後になりますが、マネーフォワードでは一緒に『桃園の誓い脱却』に向けて協力してくれる仲間を募集しております! カジュアル面談も実施しておりますので、ご興味があればぜひお気軽にエントリーしていただけたら幸いです!