💰

巨大リポジトリから会計システムを分離して開発生産性をあげた話

2023/02/01に公開

株式会社ココナラ DevOps開発グループ 業務システム開発チーム 所属のもりしたです。
ココナラでは主に経理業務で利用する会計システムの保守・改善を行なっています。
今回はわたしが所属するチームが担当する会計システムのコードを巨大リポジトリから分離し、開発生産性をあげた話をご紹介します。

巨大リポジトリから会計システムを分離する

図中の「large repo」が巨大リポジトリ(以降、large-repo)。
「accounting repo」が分離を行う会計システムとなります。

before
リポジトリ分離前

after
リポジトリ分離後

plantuml
@startuml
' リポジトリ分離前
node "GitHubリポジトリ" {
 component "large repo" as large_repo
}
node "api server" as api_server
node "batch server" as batch_server
node "accounting server" as accounting_server #orange

large_repo --> api_server : デプロイ
large_repo --> batch_server : デプロイ
large_repo --> accounting_server : デプロイ

@enduml
@startuml
' リポジトリ分離後
node "GitHubリポジトリ" {
 component "large repo" as large_repo
 component "accounting repo"  as accounting_repo #skyblue
}
node "api server" as api_server
node "batch server" as batch_server
node "accounting server" as accounting_server #orange

large_repo -> accounting_repo : ①リポジトリを分離
large_repo --> api_server : デプロイ
large_repo --> batch_server : デプロイ
large_repo -[#red,dashed]-> accounting_server #text:red : ②デプロイを廃止
accounting_repo --> accounting_server : ③分離したリポジトリからデプロイ
@enduml

大きくなったリポジトリ

large-repo はAPIエンドポイントやバッチ処理の実施など多くの役割を持つコードベースが共存したリポジトリです。
わたしが担当する会計システムもこの large-repo の中に組み込まれていました。

creative_team
1つのリポジトリに多くの役割を持つコードベースが共存

1つのリポジトリで管理することはコードが探しやすいなどのメリットはあるのですが、
次のような課題があり、開発スピードの維持・改善のため、リポジトリを分離する対応が必要となっていました。

リリースブロック中はリリースが行えない

completed_tasks
リリースブロック中はリリースが行えない

弊社にはリリースブロックという運用があります。
『01月XX日にユーザー向けに〇〇機能を追加する』という施策のリリースがあった場合、その前週末から他のリリースを禁止するというような運用です。
リリース時に行うコードのマージでコンフリクトが発生することを防ぐなどの目的で行っている運用となります。

施策が多く走るとその数だけリリースブロックの期間が増えます。
最近ではリリースブロックの期間がおわるまでリリースを待つということが増えてきました。

会計システムはお金の動きを管理するシステムであり、不具合は即時での修正が求められます。
リポジトリを分離することで、会計システム以外のリリースによるブロックから解放され、自由なタイミングでリリースが行えます。

デプロイ時間が長い

time
デプロイ時間が長い

リリースの際、CIのパイプラインにて、ユニットテスト( RSpec )を実施していますが、 コードベースが大きくなるにつれ、実行時間が長くなっていました。
リポジトリを分離することで、不要となるユニットテストが削除できるため、デプロイ時間の短縮が見込まれます。

circleci_pipeline
赤枠のapp-testにてユニットテストを実施

RubyやRailsなどのバージョンアップが容易に行えない

update
バージョンアップが容易に行えない

End of Life (EOL) を迎えるRubyなどは適宜バージョンアップを行う必要があります。
その際、バージョンアップを行うことで、これまで動いていたコードが動かなくなる可能性があり、影響調査とその結果に応じた対応を行う必要があります。

コードベースが大きいリポジトリの場合、その影響範囲は大きく、慎重な対応が求められますが、
リポジトリの分離により、影響範囲が小さくなれば、その分だけ調査がやりやすくなります。

会計システムのコード量は他のリポジトリよりも少ないため、リポジトリを分離することで、次のような運用が行えることも期待できます。

  • まずは影響範囲が小さい会計システムからバージョンアップを実施する
  • その結果を踏まえ、large-repoなどのリポジトリのバージョンアップを進める

リポジトリを分離する

実際に作業として行ったことを簡単ですがご紹介します。

作業全体の流れ

開発工程は通常の開発と同じです。

processing

  • 開発
    • 会計リポジトリ
      • large-repo を複製して会計リポジトリを作成
      • 会計リポジトリから不要となるコードを除去
      • コード除去後に更新された最新バージョンのマージ
    • large-repo
      • 会計リポジトリに移動したことで不要となったコードを除去
      • コード除去後に更新された最新バージョンのマージ
  • テスト
    • 自動テストなどを活用して動作検証
  • リリース
    • それぞれのリポジトリをリリースする

リポジトリを複製する

会計リポジトリは large-repo を複製して作成します。

duplicate
リポジトリを複製する

まさにやりたいことが GitHub Docs にまとまっていたため、こちらを参考にしました。
https://docs.github.com/ja/repositories/creating-and-managing-repositories/duplicating-a-repository

リポジトリの複製をコマンドで実行する

# --bareオプション指定でリポジトリのベアクローンを作成する
# 指定するリポジトリは複製元となるlarge-repoリポジトリ
% git clone --bare git@github.com:xxxx/coconala-large-repo.git
Cloning into bare repository 'coconala-large-repo.git'...
remote: Enumerating objects: 158871, done.
remote: Counting objects: 100% (584/584), done.
remote: Compressing objects: 100% (286/286), done.
remote: Total 158871 (delta 365), reused 451 (delta 297), pack-reused 158287
Receiving objects: 100% (158871/158871), 42.65 MiB | 7.15 MiB/s, done.
Resolving deltas: 100% (116408/116408), done.

# クローン完了後のディレクトリに移動
% cd coconala-large-repo.git 

# --mirrorオプション指定で複製先リポジトリにミラープッシュ
# 指定するリポジトリは複製先となる会計リポジトリ
# 複製先リポジトリは空の状態で事前に作成済み
% git push --mirror git@github.com:xxxx/coconala-accounting-repo.git
Enumerating objects: 158871, done.
Counting objects: 100% (158871/158871), done.
Delta compression using up to 12 threads
Compressing objects: 100% (39740/39740), done.
Writing objects: 100% (158871/158871), 42.64 MiB | 3.54 MiB/s, done.
Total 158871 (delta 116408), reused 158871 (delta 116408), pack-reused 0
remote: Resolving deltas: 100% (116408/116408), done.
To github.com:xxxx/coconala-accounting-repo.git
 * [new branch]          feature/****** -> feature/****** #<= 存在するブランチの数だけコピー

リポジトリ複製後は、不要コードの削除を進めます

慎重にコードを削除する

duplicate
コードを削除する

当初はクラス単位の粒度で分離後に参照していないコードを削除しようと考え、依存関係の有無を1つずつ調べていましたが、とても非効率でした。
1つでもメソッドや定数を参照している場合、削除ができません。 また手動での調査のため、完璧を目指すと時間がかかってしまいます。

このため、クラス単位での削除は諦め、メソッド・定数レベルで不要なコードを削除する方針に変更しました。
これにより、依存関係が剥がしやすくなり、コードの削除のスピードが上がりました。

Rubyは動的型付け言語

プログラム言語がRubyだったことには少し苦労しました。
対象リポジトリのプログラム言語はRubyです。
Rubyは動的型付け言語であり、コンパイルレベルでコードの誤りを検知してくれません。

  • 参照しているクラス・メソッドを削除してもコンパイルレベルで検知できない
  • IDE(Rubymineを利用)の機能で依存関係を追っても正確な情報が得られない

大きな規模でコードを削除する際は、ざっくりと目星をつけた不要コードを削除し、その中に必要なコードがあったら、元に戻すということを繰り返したいです。
その点では、Javaなどの静的型付け言語は参照されているコードを削除してしまうと、コンパイルレベルでエラーとなるので、分かりやすいです。

一方でRubyは必要なコードを消しても何も教えてくれません。
また、IDEの機能にてメソッドの参照先を追ってもすべてが網羅された参照先の一覧が得られない場合があり、参照先が0件であっても、削除可能と判断しきれませんでした。

ユニットテストは大事

愚直に要不要のコードの切り分けを手動で行いましたが、やはり誤って削除してしまっていないか?は心配です。
弊社では実装を行った場合、 RSpec にてユニットテストを書く方針となっているため、ユニットテストがパスすればOKと判断できます。

とはいえ、一部にはカバレッジが低い箇所があり、目安の1つとしてテスト結果を確認しました。
自信を持ってリファクタリングが行えるようにユニットテストをしっかりと整備することは大事だと感じています。

最新バージョンをマージしないといけない

リポジトリ複製後、 large-repo に追加された新機能があるため、リリースまでにマージを行う必要がありました
リポジトリ間でのマージを行います

version_control

クローンした会計リポジトリにてマージを行う

# 複製元リポジトリをリモートリポジトリに追加
% git remote add large-repo-origin git@github.com:xxxx/coconala-large-repo.git

# 複製元リポジトリと同期をとる
% git remote update
Fetching origin
Fetching large-repo-origin
remote: Enumerating objects: 9990, done.
remote: Counting objects: 100% (1674/1674), done.
remote: Compressing objects: 100% (400/400), done.
remote: Total 9990 (delta 1375), reused 1511 (delta 1274), pack-reused 8316
Receiving objects: 100% (9990/9990), 2.35 MiB | 3.86 MiB/s, done.
Resolving deltas: 100% (7109/7109), completed with 231 local objects.
From github.com:xxxx/coconala-large-repo
 * [new branch]          feature/new_branch #<= 増えたブランチが同期される
 
## マージ用の開発ブランチを作る
% git checkout -b feature/try_merge

## 複製元リポジトリの本流ブランチを取り込む
% git merge large-repo-origin/develop

マージの実行は簡単に行えましたが、コンフリクトの解消には少し手間がかかりました。
主には会計リポジトリから削除した不要なコードに対して、複製元で修正が加えられたものです。
初回のマージはリポジトリ複製から2ヵ月ほど経ってから実施したため、大量のコンフリクトが発生してしまいました。

リリース時のコード差分

会計システムのPullRequestを参考に貼り付けます。
大半は不要コードとして削除したため、赤文字の削除行は −235,600 という大きな数字となりました。

accounting_pullrequest

課題は解決できたのか?

巨大リポジトリからの会計システムの分離により、課題は概ね解決することができました。

リリースブロック中はリリースが行えない

巨大リポジトリの修正状況に影響を受けず、所属チームにてリリースタイミングをコントロールできるようになりました。

デプロイ時間が長い

会計システムのデプロイ時間は 6分 程度です。
分離前と比べると、1/5 ほどの時間で完了するため、 万一の場合にもリバートを高速に行なうことができるようになりました。
一方で巨大リポジトリのデプロイ時間はあまり変化がありませんでした。
ユニットテスト数が減ったことで若干は短縮されたと思いますが、体感はあまり変わっておりません。

RubyやRailsなどのバージョンアップが容易に行えない

会計システム分離後、さっそくバージョンアップを開始しました。
影響範囲が限定できるため、トライ&エラーがしやすくなっています。

最後に

今回は会計システムのコードを巨大リポジトリから分離し、開発生産性をあげたという話をご紹介しました。
当初は1つのリポジトリでのコード管理で十分だったものが、規模の拡大につれて、本記事であげたような課題を抱え始めるというのは開発の現場ではよくあるテーマかと思います。
本記事が何かしらの参考になりましたら幸いです。

ココナラでは技術的な課題がまだまだあります。
わたしが所属します DevOps開発グループ 業務システム開発チーム も一緒に課題を解決していただけるエンジニアを募集しております。

少しでも興味を持っていただけましたら、以下のカジュアル面談応募フォームからご連絡をいただけますと幸いです!
https://open.talentio.com/r/1/c/coconala/pages/70417

Discussion