
【Go】バックエンド社内ライブラリを作成しました
🎄 この記事は『カンリー Advent Calendar 2024』の12月10日分の記事として執筆しています
こんにちは!
株式会社カンリーでバックエンドエンジニアとして働いている菅野(@imsugeno)です。
この記事では、弊チームのバックエンド開発において発生していた重複コード問題に対するアクションとして、社内用の共通ライブラリを作成・導入してみた件についてお話します。
前提
チーム体制
私の所属しているカスタムシリーズチームはDEV2というチームに所属しており、DEV2の別チーム(関連する別プロダクトを扱うチーム)としてカンリーホームぺージチームがあります。
これら2チームでは合計5つのリポジトリを管理しており、うち1つのリポジトリがどちらのプロダクトからも参照されるバッチ処理を管理するためのリポジトリとなっています。

それぞれのチームは5名ほどの開発者と、兼務のQAエンジニア・デザイナー・PdMで構成されています。
使用技術
全てのバックエンドコードは以下の点で技術的に統一されています。
Goで記述されている
利用ライブラリの差異はある
オニオンアーキテクチャである
細かいディレクトリ構成の差異はある
DDDは導入しておらずドメインモデルはDTO的に扱っている
スクラム・リリース運用
2週間スプリントのスクラム開発であり、Sprint1で機能Aを開発した場合、Sprint2で機能AのQAを実施し、Sprint3以降でリリースを行います。

ブランチ運用
ブランチ運用はGitLab Flowをベースとした運用となっていますが、各ブランチに対応する環境で発生したバグに対するhotfixを、各ブランチに直接取り込むようにしているという点は本家と異なっています。

共通ライブラリ導入の経緯
プロダクトの拡大と重複コードの管理コスト増加
前提で説明したように、Dev2チームには2つのプロダクトと2つの開発チームが存在しています。
これは元々そうだったわけではなく、カンリーホームページのみだったところから、サービス間や基幹システム等とのデータ連携の需要から、カスタムシリーズというプロダクトが後続で開発されたという経緯があります。
データ連携を行うということは既存のデータモデルやビジネスロジックを参照することが必然的に多くなり、そうすると発生するのがリポジトリ間にまたがる重複コードです。
共通してアクセスするテーブルのドメインモデル
共通しているService層のビジネスロジック
独自のエラー構造体 etc...
などが複数リポジトリの間で重複しました。
元々1チーム3リポジトリの管理であったことから、共通ライブラリも視野に入れつつも、手動で重複コードの同期を行える範囲だったためそのようにしていました。
しかし前述の通りプロダクトやチームが拡大することで重複コードの管理コストも肥大化しました。
開発意思決定とステークホルダー調整
Dev2チームでは中長期改善タスク検討会というイベントを行っています。
月1回ペースで実施
Dev2チームの開発者全員参加
開発上の問題とその改善案を共有して取り組む優先度を決定
既に取り組んでいる課題の進捗共有
この会でまず開発チーム内のみでの問題意識の共有と開発意識決定を行いました。
ですがそれなりの規模の開発となるため、実際に取り組む上では開発メンバー以外のステークホルダーとの調整も必要となります。
例えばプロダクトマネージャー組織では、月単位での中規模〜大規模なプロダクトバックログの実施計画を立てているため、チームのPdMへの説明とPdM組織内での合意形成を行ってもらう必要がありました。
また弊社ではOKR制を導入しており、規模の大きな開発は開発組織全体の四半期Objectiveに影響するため、組織目標にも組み込む必要がありました。
技術的な検討事項と発生した問題
移行対象の整理
共通コードは基本的にファイル単位で存在していたため、どのファイルをライブラリ側に持っていくかを選定する作業となります。
全てのリポジトリを調査して、現状重複が発生しているコードが移行対象となりました。
DTO(gormで扱う構造体)
DBにおいてJSON形式のデータを格納するために定義された構造体
共通して参照されるビジネスロジック
固有のドメインに依存しないhelper関数
独自定義したエラー型に関連するパッケージ etc…
検討事項
リポジトリごとのDIライブラリの差異をどのように吸収するか
DIライブラリはgoogle/wireを利用しているリポジトリとuber-go/fxを利用しているリポジトリが混在
どちらのリポジトリも、作成したDIコンテナをモジュール化して別のDIコンテナに取り込む機能があるため、それぞれのDIライブラリ毎のDIコンテナを作成する関数を共通ライブラリ側に作成し、参照側で読み込むことで解決(ex. fxであればModules)
ローカル環境の整備
プライベートリポジトリをローカルから参照できるようにするのは手間だし、まだリモートにプッシュしていない開発中に困ってしまう
go.workを用いたWorkspace Modeを活用し、開発時のみローカルにクローンしてきたライブラリを参照するようにして解決
デプロイ時にプライベートリポジトリを見に行けるようにする
Pull RequestにおけるCIで開発中の共通ライブラリを参照側が見に行けるようにする
など、様々な検討事項が上がりました。
中でも厄介だったのは「タグ・ブランチ・リリース」の運用です。
タグ・ブランチ・リリース運用
開発開始当初は共通ライブラリのブランチ運用は以下を想定していました
共通ライブラリはmainブランチ一本のトランクベース
featureを取り込む度にタグ打ち、参照側は常にバージョンアップ
2つ目の運用は手動では手間がかかるので様々な自動化やCIの仕組みを検討していました
Renovateによって参照側がバージョンアップをデイリー実行
共通ライブラリ側のGitHub Actionsで、タグ打ちをトリガーに参照側のバージョンアップデートPRを自動作成
バージョン上げPRのテストがコケたらSlack通知
しかしこれらの仕組みを取り入れて試しに運用を始めると、以下のような問題点が発生してしまいました。
go.modが頻繁にコンフリクトする
共通ライブラリ側でmainブランチにマージされる度にバージョンを上げていたので、他の開発者の変更が入るたびにコンフリクトしてしまう
都度go getコマンドを叩く手間が発生し、これが割と面倒
hotfix時の運用を完全に想定できていなかった

特にhotfixの運用は致命的であり、これを避けるためにhotfix用のブランチを生やす方法も検討しましたが複雑なブランチ運用となることが予想され、逆に運用ミスで先祖返りが発生しやすくなるのではという結論に至りました。
開発生産性を上げるための共通ライブラリが、逆に開発生産性を下げてしまうという状況が発生してしまったのです。
問題の解決と得られた恩恵
共通ライブラリのブランチ運用を利用側と合わせた
解決方法としては単純で、共通ライブラリのブランチ運用を利用側と同じmain/staging/productionの3ブランチ構成に合わせました。
それぞれのブランチはDEVELOPMENT/STAGING/PRODUCTIONとった具合に別のデプロイ先に対応しており、参照側のプロダクトがデプロイを行う際、Dockerfile内でgo.modのreplaceを使って参照する共通ライブラリのブランチを切り替えるようにしています(git上はgo.modでは固定したバージョンを参照しています)。
特定のバージョン (またはすべてのバージョン) のモジュールの内容を、別のモジュールのバージョンまたはローカルディレクトリに置き換えます。 Go ツールは、依存関係を解決するときに置換パスを使用します。
コード削減効果
得られたコード削減効果としては、全リポジトリのPRで行数を合計すると
+13,052行 -72,774行
となりました!
clocコマンドで共通ライブラリ自体のコード数を見てみると
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Go 380 3535 690 28341
YAML 2 4 3 167
JSON 4 10 0 125
Markdown 2 26 0 41
make 1 6 0 28
Dockerfile 1 3 0 11
-------------------------------------------------------------------------------
SUM: 390 3584 693 28713
-------------------------------------------------------------------------------
とのことで、大体4~5万行ほどの共通コードをこの世から消すことができました!
終わりに
共通ライブラリを導入してみましたが、共通コードを消す目的は達成できたものの、微妙だった点やデメリットも残ります。
2チームが1つのリポジトリを一緒に変更するのでチーム間調整が必須
一方のチームがリリースするがもう一方はリリースしない、みたいな場合にマージしないでもらう必要がある
共通ライブラリのみに変更を行った場合、参照側のデプロイはトリガーされないので手動で実行する必要がある
自動化で改善可能だができていない
go.modの運用が一般的な運用と異なる
基本的にはやはりタグで運用すべき
そもそも共通化にかなりコストがかかった
既存の他の技術的負債に制約を受ける(ブランチ運用など)
やりたいことは沢山あって
いきなり全部綺麗に、は難しい
目の前の問題を一つ一つ良くしていきましょう
株式会社カンリーでは目の前の問題を愚直に解決していきたいメンバーを絶賛募集中です!
クレジット
TOP画像のGopherくんはtottie000/GopherIllustrationsから使わせていただきました!
The Go gopher was designed by Renée French. Illustrations by tottie.