……と思っていたら5ヶ月かかりました.
【基盤開発チーム ブログリレー3日目】
こんにちは,エムスリーエンジニアリングGの榎田です.趣味は数学とテレビゲームです.最近はタクティクスオウガ リボーンを遊んでいます.システィーナをバーサーカーで運用しているのが弊ユニオンの個人的なイチオシポイントです.
閑話休題,ソフトウェア開発において「技術的負債」はつきものです.我々は無限の開発時間が取れるわけでも,神授の智慧を持っているわけでもないので,作ったものは何かしらの要因で負債を抱え,抱えた負債は(多くの場合,知らず知らずのうちに)増えます.負債だらけになってしまったソフトウェアの保守管理は大変ですし,負債になりにくいソフトウェアを作るのも難しいです.ではどうして負債を抱えてしまうのか.負債を返すことの何が大変なのか.負債リスクを予見するのはなぜ難しいのか.
これらの問いに答えることはきっと簡単ではありません.もし簡単なら,我々がレガシーシステムとか技術的負債と呼ばれるものに苦しむ機会は今よりずっと少ないだろうと思っています.それでも,だからこそ,何かしらの答えに対する手がかりを得たいと思うのが人情です.
今回は,私がこの半年で手掛けたとあるアプリケーションの作り直しプロジェクトを紹介します.この実例を通して,特に「負債を返すことの何が大変なのか」について,その手がかりを少しでも,ぼんやりとでも,描画することが本稿の狙いです.
プロジェクト概要
対象となるソフトウェア
- 社内で利用するちいさな管理画面アプリケーション(と,それに付随するバッチプログラム)
- 認証がかかっており,3種類のリソースに関する基本的な CRUD ができる
- オンプレサーバ上でビルド・稼働しており、コンテナ化されていない
- 以下のものは存在しない
- ローカルまたは CI pipeline 上で動作するテスト
- 標準化されたビルド・デプロイ手段
- 依存ライブラリは全く更新されていない
- 直近の過去の変更に際して,本番環境での障害を高い確率で起こしていた
- このシステムの動作の不具合は金銭的なトラブル・ハードクレームに直結する
基本戦略
今回は既存コードをほぼすべて捨て,イチから作り直すことにしました.これは以下の理由によります.
- 以下の理由により,「とりあえずコンテナに固めてビルド・デプロイを標準化する」という戦略に困難がある
- 共有地サーバ上でビルドする際,依存ライブラリの解決に相対パスでのワイルドカード参照を実施していた
- 相対パス参照なので,コンテナに固めるにはビルドに必要なライブラリを全て特定してコンテナに詰めないといけない
- ワイルドカード参照なので,必要なライブラリを正確に特定することが困難
- 共有地サーバのディレクトリには多数のライブラリが置いてあり,全てをコンテナに詰めるのは無駄が多く負債の先送りになる
- アプリケーションの規模が小さいので,イチから作り直しても現実的な時間におさまりそうだと見込まれた
- 3種のリソースに関するCRUDならびに認証ができれば最低限動く
「今回は」と書いたのは,常にこうすべきだとは思っていないからです.既存コードが負債にまみれていても,今動くならばそれは資産でもあります.その資産の一切を捨てるリスクを取るべきかは案件によるでしょう.今回の案件でも,既存アプリケーションから CSS だけはほぼそのまま拝借でき,かなりの時間短縮になりました.場合によっては,コードを最大限活かした状態で CI やテストをあとづけで整備したほうがよいこともあると思います.
当初の見積もり
最初に述べた通り,今回は3種類のリソースのCRUDを作らねばなりませんでした.そこで試しに1種類のリソースに関するCRUDを実装したところ,この時点で1ヶ月かかりました.これを元に,「1種類で1ヶ月なら,3種類だと3ヶ月か.なんだか遅い気がするけど,慣れればもう少し早く書けるだろうし,諸々足して悲観的に見ても4ヶ月はかからず終わるだろ〜」と思いました.
期間
最初の Pull Request を出したのが昨年の12/27で,作り直した版の本格稼働が始まったのが今年の6/2です.おおよそ5ヶ月ほどかかった計算になります.当初の悲観的な見積もりよりも1ヶ月以上膨らんでいることがわかります.
大変だったポイント
当初の見立てよりも時間がかかった要因は何だったのでしょうか.いくつか要因として考えられるポイントを列挙します.
ユニットテスト
今回のアプリケーションに要求される品質を考えても,書かない選択肢はないので書きました.今回既存のテストが使い回せなかったので,もちろん書き直しです.書き終えた現在は C1 カバレッジで約80%の状態になっています*1.
テストを書く過程にそれなりに時間がかかっていた気がするので,気になって cloc でおおざっぱに行数を数えてみたところ次のようになりました*2.
cloc ./**/main --------------------------------------------------------------------------------- Language files blank comment code --------------------------------------------------------------------------------- Kotlin 68 607 457 3600 cloc ./**/test ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Kotlin 25 1041 173 6280
テストコードの行数がメインコードの1.5倍以上あります.もちろんそもそもテストコードの行数は嵩みやすいなどの事情はあると思いますが,それでもテストコードを書いた時間はメインコードを書いた時間と同じか,それ以上かかっている可能性さえあります.また,単体テストを書いている最中に「単体テストが書きにくいのは設計が悪いのが原因だ,設計を変えたほうがテストも書きやすいし構成の見通しも良くなる」と気づいて設計に手を入れたこともありました.
当初「1機能1ヶ月」を「なんだか遅い」と悲観的に思っていたのですが,これらのことに時間を使っていたことを踏まえるとむしろ当然だったのかもしれません.むしろ「手早く終わるように見えても、テストコードにかかる時間がメインコードより大幅に少ないなら,それはテストが不充分なことのシグナルかもしれない」と考えたほうがいいのかもしれません.いくら時間が惜しくても,テストを書かなければ動作を保証することが困難となり,そのアプリケーションは廃墟まっしぐらです.また,既存コードに機能するユニットテストがあれば,それは(特にリファクタリングをする際の)強力な武器になると思います.
見積もり漏れ・未知事項のキャッチアップ
どうしても出てきます.今回の作り直しでは例えば次のようなことがありました.
- 既存アプリケーションは認証のためにある社内ライブラリを使っており,作り直しの際もそのライブラリをそのまま使おうとしたので,1〜2日で終わると思っていた
- ところが,その認証ライブラリの仕組みが古い(いわゆる OpenID Authentication 2.0 と呼ばれている仕様の上に作られている)ことに後から気づいた
- 更にこの認証ライブラリを保持したまま新しく作り直すのが難しいとわかった
- 結局,認証を OpenID Connect を用いる形に刷新すると決定
- …したが,今まで私は OIDC に関する実装をしたことがなかった
- 結局キャッチアップから実装まで含めて2週間ほどかかった
委細を含めて把握できていれば見積もりに含めそこねることは少ないですが,一方で詳細にあかるくないものを正確に見積もるのは不可能です.
たくさんの細かな改善作業
期間中に大小さまざまな Pull Request を出しました.個数を計算したところアプリケーション本体のリポジトリに対して合計82個の Pull Request が出ており*3,そのうち19個(約23%)はアプリケーション本体と直接は関係しない,開発上の細かな改善に関するものでした.具体的には以下のような内容が含まれていました.
- Renovate 導入
- test coverage の計算と line visualization の設定
- ローカル起動時に使う S3/DynamoDB のセットアップ
- それぞれ MinIO と DynamoDB local を使っています
- Gradle でローカルサーバを立ち上げるときに DynamoDB のマイグレーションも自動で走るよう Gradle script を書きました
- ローカル起動時に認可・認証をスキップして各種機能が使えるようにする
- 認証の絡まない挙動を確認したいときにまで OIDC での認証をするのは手間なので
- pipeline からデプロイできるための設定
- ドキュメント更新
Pull Request の個数と消費時間はもちろん比例しませんが,大雑把に見て10%から20%,だいたい半月〜1ヶ月はこのような改善作業・足回りの整備に割かれていると見て大きくは外さないでしょう.細かな改善たちは一切当初の見積もりに積んでいなかったのですが,まとめると2週間以上の工数になっていると考えると,無視できない影響を与えるとわかります.
もちろんこの2週間をケチる形で改善をしないことも可能ですが,その決定と引き換えに今回作り直したものが「負債」と認識されるまでの期間が短くなると期待されます*4.したがってここで手を抜くのは本末転倒だと思います.
他の仕事
長い期間に渡ってひとつの仕事だけができるとは限りません.もちろんエンジニアとしてはひとつの仕事に集中できるのが理想ですが,火急の案件が来る可能性をゼロにすることは不可能です.今のところ私は「そういうものだ」と割り切っています.
私の例に関して言うと,4月前半の2週間ほどは他プロダクトのDBに性能劣化が起き,その原因調査と改善をしていました.その期間はこのプロジェクトの作業は進んでいません.
まとめ
今回のプロジェクトをまとめます.
- 当初は3ヶ月強程度で終わると思っていた
- もっと早く進められるのでは?とも思ったが,ユニットテストの拡充をしながらだと1機能1ヶ月程度が妥当であった
- 当初は見積もり損ねた項目があり,それらに追加で時間がかかる形となって5ヶ月経過した
- 未把握な事項へのキャッチアップで2週間
- 細かな改善で2週間
- 他の仕事で2週間
こうしてみると,ただひとつの要素が作業の工程を圧倒的に短縮することもなければ,単独でシステムの品質を大幅に向上させる銀の弾丸もないのではないか,と思われます.ここに挙げた要素たちはノーコストで実施できるものではないものの,いずれもソフトウェアの品質を向上させ,負債を軽くするための手がかりだと思います.様々な要素をひとつひとつ丁寧に組み上げることが結局は王道で,したがって総合力を問われると考えています.
他方で,ここに挙げたような要素たちをより定量的に分析・研究する動きもあるのではないか,それらを踏まえれば今回の実例からもより強い主張や教訓を引き出せるのではないか,と思うのですが,その方面には私は明るくありません.これらに関する定量的な分析や,よりたしかな経験事実をご存知の方がいらっしゃいましたらぜひ教えてほしいと思っています.
We are hiring!
エムスリーではさまざまなプロダクトが長期で運用されており,その運用の中でここに書いたような総合力を問われる場面も出てきます.総合格闘技の腕に覚えのある方のご応募をお待ちしています.
*1:ここではあくまで「テストを書いてない」というわけではない,ということを提示するために数字を出しています.しばしば言われるように,カバレッジの数値が高いことそれ自体が品質を保証するものではなく,またカバレッジの数値を上げることそれ自体を目標にすべきではないです.いくらカバレッジが高くても例えば flaky test が含まれる状況は好ましくないですし,ユニットテストが存在せず E2E テストしかないという状況も往々にして苦しみを産みます.そのような状況を産まないよう,設計はそれなりに検討してありますが,そのことまで本稿で書いていると長くなるので省略します.
*2:この結果を見ればわかるように,作り直し後は Kotlin で書いています.Kotlin を採用した理由に強いものはなく,チーム内で「何がいいですか」と聞いて挙がった言語の中から選んでいます.チーム内で扱える人が多い言語を採用したほうが負債になりにくいだろうと考えています.
*3:実際にはインフラを構築するための Terraform もあります.それも含めるともう数個増えます.
*4:他方で自分が作ったものが「負債」と認識される日が必ず来るとも思っています.