Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

レガシーシステムとつきあう

Sansanプロダクト開発部・基盤チームの加畑です。

Sansanプロダクト開発部には、現在約120名のメンバーが所属しています。その中で、私が所属する基盤チームは6名のメンバーで構成されており、データアクセスや認証、メッセージング基盤、CI等開発環境やリリースプロセスなど、アプリケーションの基盤に関わるプロジェクトを主に受け持っています。

担当領域の性質上、システムのレガシー化に向き合う機会が多いです。私を含め複数のメンバーが、常に何かしらレガシー改善に関わるプロジェクトを進めている状況です。昨年の10月に、私が担当したいくつかのプロジェクトについて、レガシーシステムのおそうじという記事でご紹介しました。4月にはレガシー改善に関する勉強会を開催し、はてなさんやYahooさんとも交流することができました。楽しかったです。

前回記事と勉強会では、「レガシー改善のプロジェクトをうまく進めるにはどうすればいいか」という観点で、「計測」「分解」「合意」という言葉を使って整理してみました。そこで今回は、単発のプロジェクトの進め方から少し視野を広げ、「プロダクト及び組織が成長していく中で、レガシーシステムとどうつきあっていけばいいか」という観点で考えてみます。その上で、最近実施してきたレガシー改善の事例についてご紹介します。

レガシー改善の指針

技術的負債という比喩に象徴されるように、レガシーなシステムが開発生産性を落とすことは自明です。そのため、一般的にはレガシーを作り込まないようにすることが定石であり、そのためのプロジェクトマネジメントや技術的な知識が求められます。

一方で、既存のレガシーシステムに対して開発や保守を行う場合も往々にしてありますし、ソフトウェアは本来的に不確実性をはらんでおり、レガシー化しないシステムをつくることは実質不可能です。そこで、既存のシステムがレガシーな要素を含んでいること、また開発すること自体がレガシー化を促進する行為であることを前提として、システムの開発に携わる組織が、中長期的にレガシー化という現象にどのようにつきあっていくべきかを考えてみます。

シンプルにする

システムの開発生産性を落とす原因の一つとして、複雑さが挙げられます。ソースコードの複雑さは静的解析のツールなどである程度までは計測可能であり、一般にいくつかのツールが知られてます。一方で、システムをより大きな視野で見たとき、アーキテクチャレベルの複雑さや、業務知識など暗黙知による複雑さといった、計測が難しいものもあります。体感としてはむしろこちらのほうが生産性の低下につながりやすいです。この課題に対するひとつの対策として、隠蔽する方法が考えられます。複雑さを隠蔽しておけば、その他のメンバーがブラックボックス的に扱えるようになり、開発効率はよくなります。ある課題の解決方法を考えるとき、その自由度が高いのがソフトウェアの良いところでもあります。一方で、プロダクトと組織が成長してシステムに対する要件が複雑化した場合に、当初想定していたインターフェイスを拡張したり、ブラックボックスの中に手を入れる必要性が出てきます。このときの開発コストは非常に高くなります。このあたりの考え方は、以下の記事でわかりやすく言語化されています。

www.christopherspenn.com

この記事によると、複雑さの隠蔽は、開発に勢いをつけるmomentum builderとして価値を発揮し、一時期の開発生産性を高める上で有効です。一方で、組織が拡大し、ある程度の規模になるにつれて、利便さと自由さのバランスにおいて、適切な構成が徐々に変わっていきます。Sansanも先述のように組織の規模が大きくなってきたこと、またプロダクトが多機能化してきたことにより、easyな構成からsimpleな構成へと比重を移しつつあります。

開発プロセスに組み込む

ソフトウェアを開発していくかぎりレガシー化は避けられないとした場合、レガシー改善に終わりはないことになります。レガシー改善のプロジェクトを完遂し、一時的に単純な構成を手に入れたとしても、その後開発を続けていくかぎり、徐々に複雑さが蓄積されていきます。

また、ビジネスサイドあるいは日常的にコードを書かないマネージャにとって、レガシー改善に関する判断は負荷が大きいことが想像できます。ユーザの体験に関わる改善や、サーバコストのような目にみえる数値があればいいのですが、そうでないことのほうが多いです。アーキテクチャや開発環境など、エンジニアの人的コストに響くレガシーは、定性的にならざるを得ないのが実情だと思います。だからこそ「技術的負債」のような比喩が重宝されながら、現在でも議論の種になっています。意思決定者のコストが大きい以上、レガシー改善のプロジェクトはその価値を判断し、開始すること自体が難しくなってしまいます。

このような状況に対するひとつの解としては、「開発プロセスに組み込む」ことが考えられます。つまり、組織において、レガシー改善をイレギュラーなプロジェクトやタスクとしてひとつずつ取り組むのではなく、他の新規開発や機能改善のプロジェクトと同じく、システムの開発を進める上で必須であるという前提で取り組む、ということです。Sansanでは、これまで各チームでもっていたバックログを、ひとつに統一しました。この中で、レガシー改善も他の新規開発と同等の課題として、権限を持ったPM陣によって優先度が判断されます。バックログについては以下の記事を参照ください。

buildersbox.corp-sansan.com

もちろんこれでレガシー改善の難しさがすべて解決するわけではありません。事業要求に直結する他プロジェクトとの間での優先度の判断などは、依然として難しい課題です。とはいえ、この体制によって中長期的視点をもってレガシー改善を行うことができるとともに、意思決定者とのハンドシェイクのコストを抑えることが期待できます。

Sansanにおける事例

ここまでで、レガシー化という現象に対するつきあい方として、「シンプルにする」「開発プロセスに組み込む」という2点を挙げました。以下ではこれらを踏まえ、私が関わった事例を3つご紹介します。

事例1: ORMのレガシー改善

SansanではRDBMSにPostgreSQLを使っており、データプロバイダとしてNpgsqlを使っています。データアクセスは、初期のSansanでは自前で実装していました。2016年にアプリケーション全体の設計方針を再考し、その中でEntity FrameworkDapperを導入しました。

Entity Frameworkは.NET標準の高機能なORMであり、データを.NETオブジェクト(=エンティティ)として扱います。アプリケーションの開発においてはデータベースのことを意識せず、オブジェクトとして扱い、実装に集中できます。Dapperはより軽量なMicro-ORMであり、主にクエリの実装に使われていました。新規の画面や機能は新アーキテクチャで実装し、古いデータアクセスを用いる実装は、徐々に置き換えていく方針で進めてきました。

f:id:kyabatalian:20191012213537p:plain
レイヤ構造の概要図

課題

最近になり、Entity Frameworkを利用し続けることについて、課題が目立ってきました。Npgsqlは、Entity Frameworkに対するサポートとしてEntityFramework6.Npgsqlというライブラリを公開しています。ただし、このライブラリはPostgreSQLのある特殊な型をサポートしていませんでした。そのため、SansanではEntityFramework6.Npgsqlをクローンし、拡張して使っていました。PostgreSQLやNpgsqlのバージョンアップをする際、この拡張も併せてメンテナンスしていく必要がありました。

また、日々の開発業務においても、Entity Frameworkに適合させるためのコストがかかっていました。Entity Frameworkは高機能なORMであり、データベースのリレーションを意識することなくエンティティに対してプログラミングすることができます。一方で、凝ったことをやろうとすると手を入れづらいという課題がありました。例えば、Entity Frameworkが出力するSQLが期待どおりにならずチューニングが必要となった場合に、Dapperを使って実装しなおす、という場面もありました。

改善

Entity Frameworkを使っていた箇所を、全面的にDapperへ移行しました。Dapperは、単にコネクションクラスのインターフェイスを拡張してマッピングをサポートするMicro-ORMであり、Entity Frameworkと比べると最小限の機能しかありません。一方で、上記の課題にあるようなクエリチュー二ングなどはやりやすく、また複雑な要件でも比較的容易に実装できました。

Entity Frameworkはサービス全体にわたって利用されていたため、基盤チームのみでやりきることは難しい規模でした。長期的なプロジェクトとして開発部全体のリソースを使い、徐々に移行していきました。

f:id:kyabatalian:20191012214237p:plain
Entity Frameworkを脱した告知

現在、後続のプロジェクトとしてNpgsqlのバージョンアップに対応しているのですが、Entity Framework及びその拡張を考慮しなくてよくなったことにより、その中での改善コストが格段に少なくなりました。

考察

Entity Framework自体は.NET標準として問題なく使える状況でした。そのため、EntityFramework6.Npgsqlをさらにカスタムして使い続ける、あるいはデータベースの型を修正する、という選択肢もありました。実際に開発部内でも意見が割れるところでした。

当然、Entity Framework自体が悪いというわけではないですし、導入当時の判断に誤りがあったとも思えません。ビジネスの成長に伴い、新しくかつより複雑な要件が随時追加されてきたこと、また組織の急速な拡大により前提とすべき知識や学習コストも変容してきたことなど、プロダクトと組織が成長してきたことによりアーキテクチャに求められる要件が変わってきたと考えられます。

基盤チームのメンバーが主導して進めており、私は最後に少しお手伝いしただけですが、プロジェクトの性質上長期的に取り組む必要があったこと、また基盤チームだけでなく他チームのリソースも影響することから、管理が難しい部類のプロジェクトでした。途中で先述のバックログ体制に移行したことにより、チームだけでなく開発部全体、あるいはプロダクトの課題として明に合意でき、プロジェクトの完了に対して全メンバーが明にコミットできるようになりました。

事例2: パッケージ管理のレガシー改善

ソフトウェアの世界は技術の進化が早く、特にフロントエンド周辺は同時期に複数のライブラリが競合するなど、主流の移り変わりが比較的早いと言われています。Sansanでは2014年頃からパッケージマネージャを導入し、当時一般的であった、「フロントエンドをBower、バックエンドをnpm」という構成を採用しました。

課題

時を経てBowerが公式にメンテナンスされなくなりました。同時に、Bowerで管理していたパッケージ群もメンテナンスされなくなりました。

f:id:kyabatalian:20191012212739p:plain
2019年10月現在のBower公式サイト

メンテナンスされない環境を使い続けるのは、セキュリティの観点でも望ましくなく、別のツールに移行する必要がありました。移行先としてはnpmが有力でした。Bowerで管理していた既存パッケージを移行するにあたり、大きく2つのケースがありました。1つ目は、Bowerで管理していたパッケージがnpmにも対応しているケースです。つまり、公式レジストリに存在する、あるいはpackage.jsonファイルを含めたコードが公開されているケースです。両方とも利用するパッケージを一覧化された設定ファイルで管理するため、機械的に移行が可能です。2つ目は、Bowerで管理していたけどnpmに対応していないケースです。こちらは機械的に移行することができません。後者のケースにおいて、どのように移行するのかが課題となりました。

改善

「Bowerを利用しなくする」ことをゴールに据え、Bowerで管理していた各パッケージを一覧化し、それぞれどのように移行するかを分類しました。あとはどれくらい時間をかけるかを見積もり、計画して実施していきました。

f:id:kyabatalian:20191012213244p:plain
Bowerの対応リスト

先述した1つ目のケース、npmにも同様のパッケージがある場合は、基本的にはBowerの対象パッケージを管理する設定ファイル(bower.json)で一覧化されている各種パッケージを、npmの同設定ファイル(package.json)に移すだけです。あとはアプリケーション内の参照パスを張り替える必要がありましたが、SansanではもともとBowerで取得したパッケージを.NETのBundleConfigという機能を使ってバンドルしていたため、参照はほぼ集約されており、容易に移行できました。

2つ目のケース、npmに同じパッケージがない場合は、プロダクトの機能を変更しないことを前提とした場合、同様の機能を満たすパッケージをnpmから探し、利用箇所を修正・検証することが思いつきます。ただ、この方法だと修正箇所の調査や対応の見積もりの不確実性が高まり、また総合的に工数が膨らむことが予測されます。

今回はその方法ではなく、Bowerで取得したパッケージのコードをそのまま自社リポジトリの管理下におきました。これにより、アプリケーションのコードを変更することなく、bower.jsonから対象パッケージを削除することが可能になりました。

考察

事例1と異なり、外部の環境変化がプロジェクトのトリガとなっています。Bowerがメンテナンスされなくなった背景には様々な理由があります。従来はバックエンドを対象としていて地位を築いていたnpmが、その対象領域をフロントエンドにまで広がりました。また、Browserifywebpackといったバンドルツール、さらにYarnの登場などが追い風となりました。このように、過去の技術選定がある程度正しかったとしても、読みきれないことのほうが多いですし、極論をいえば永遠にメンテナンスされるライブラリはありません。適切な意思決定を重ねて開発を進めたとしても、レガシー化への対応は必要なことがわかります。

フロントエンドに関連するタスクがバックログに集約されることで、プロダクト全体からみた課題の大きさが概観でき、かつ相互の関連を勘案してマネージすることができました。実際、私とは別のチームで「フロントエンドのモジュール化」や「デザインガイドライン構築」のプロジェクトが進行しています。そういった意味で、「フロントエンドの環境改善」という巨大な課題を実施可能な単位に細分化できたのも、バックログ体制によるところが大きいです。

事例3: CI環境のレガシー改善

cloud beesのブログ記事では、CI(Continuous Integration)を以下のように説明しています。

CI(継続的インテグレーション)では、開発者が自分のコード変更を頻繁にセントラルリポジトリにマージし、その度に自動化されたビルドとテストを実行します。 小さなサイクルでインテグレーションを繰り返し行い、インテグレーションのエラーを素早く修正することによりチームは統合されたソフトウェアをより迅速に開発できるようになります。

コードを変更したあと、ビルドしてテストを実行し、変更が正しくされていることを確認します。CIは、この作業を自動で実行してくれることにより、品質を担保しながら開発をすすめることができる手法です。

Sansanでは、サービス開始初期からCIツールとしてJenkinsを利用してきました。JenkinsはCIのみでなく汎用的なジョブも作成できます。例えば、定期的にスクリプトを実行するcronのような役割もGUIで簡単に作成・管理することができます。セキュリティ検査やリリース時の作業など、CI以外にも様々なジョブを作成して利用しています。

課題

組織と開発速度がスケールしてきたことにより、サーバリソースが逼迫するようになってきました。また、歴史的経緯から直列実行せざるを得ないテストが多くあるため、複数のブランチで変更がプッシュされると、テストの実行待ちが発生する状態になっていました。また、ジョブ間の干渉により、テストがランダムに落ちる問題が発生していました。再実行すればいいのですが、実行待ちと同じく、開発生産性を落とす原因になっていました。

f:id:kyabatalian:20191012214659p:plain
テストガチャ

これらはすべて、「Jenkinsのジョブがブラックボックスになっている」ことがボトルネックになっていました。長年積み上げられてきたジョブが相互に依存していたり、またJenkinsが存在するサーバの環境に依存していました。なにか手を入れようとすると、予想しない不具合が発生するため、うかつに手が出せない状態でした。

解決方法としては、Jenkins自体のバージョンを上げる、サーバのスペックを上げる、クラウドCIサービスに移行する、などが考えられます。それをやればいいだけという話なのですが、実際には難しい状況でした。Jenkinsのジョブがブラックボックスになっており、メンテナンス性が著しく低かったためです。

改善

ブラックボックスになっているジョブを読み解き、コード化しました。これにより、何が行われているかを把握するコスト、また改善するコストが小さくすることができました。具体的には、UIで設定されたジョブを相互に接続して実現されていたCIのフローを、ひとつのスクリプトにまとめました。その過程で、複数のジョブで共通化されていた処理も分割し、ジョブを修正する場合にもなるべく他のジョブに影響を与えないようにしました。

f:id:kyabatalian:20191012215515p:plain
移行の告知

この過程で、既存のCIにおいて無駄なプロセスがいくつか発見され、改善されました。その結果、テストのジョブがつまることがほぼなくなりました。また、当初再実行するのみだったテストガチャも、ジョブ間の干渉やデータベース依存が原因となっていたことが判明し、テストコードを修正することにより解消しました。

考察

前述の2事例とは課題の性質が少し異なります。設計やコーディングを阻害するというよりは、CIというプロセスにおいて無駄な時間を食うことや、エンジニアの集中力やモチベーションといった、明には見えないですが貴重なリソースを奪うことが課題でした。いざ解決しようとしてもブラックボックス化されているが故に読み解くのが難しく、結果として長年じわじわとコストになってきた状態でした。ここで複雑さを積み上げるのではなく、シンプルにする一歩目を踏み出すことが、もっとも重要でした。

プロジェクトの要件として、「JenkinsからクラウドCIサービスに移行する」や「テストを速くする」など、複数の選択肢がありました。このプロジェクト開始当初は、まだバックログ体制になっておらず、チーム内の一タスクとして上記課題に取り組んでおり、明確にゴールを定めないまま進めてしまっていました。例えば、Jenkinsの2系ではスクリプトではなく宣言的にフローを記述できる機能があり、そちらに移行する方針で進めていましたが、最終的にはスクリプトで記述する方法に落ち着きました。

本プロジェクトの途中でバックログ体制に移行してからは、「シンプルにする」ことを第一目的として、それが達成されたといえる粒度までタスクを落とし込みました。これにより、具体的な計画や戦略を考えて実行し、理想的ではないにせよ、ブラックボックスになっていたJenkinsを読み解き、コード化することができました。この粒度でプロジェクトを完了できるのは、バックログ体制により、レガシー改善が継続的な取り組みとして組織的に合意できていることが大きいです。例えば、今回コード化したCIプロセスを別のCIサービスにのせたい、となった場合に、新規にプロジェクトを開始することになりますが、そのコストは相対的に小さくてすみます。

まとめ

システムのレガシー化とどうつきあっていくか、という観点で、「シンプルにする」「開発プロセスに組み込む」という2つの方針を挙げてみました。その上で、私が関わったレガシー改善のプロジェクトを3つご紹介しました。基盤チーム及びSansanプロダクト開発部では、新たな機能の開発や組織の拡大に対して開発生産性を最大化するべく、継続的にレガシー改善を進めています。

f:id:kyabatalian:20191012221642p:plain
社内向けリリースノートでのリリース報告

プロダクトと組織が成長していくかぎり、レガシー化は避けられません。そのような前提において、レガシーシステムを批判的に捉えたり撲滅しようと考えるよりも、どのようにつきあっていくかを考えるほうがより生産的です。今回ご紹介したことはほんの一部ですが、今後もよりよいプロダクトを作るため、ユーザに価値を提供していくために、レガシーシステムとうまくつきあっていければと思います。

© Sansan, Inc.