yshnb’s engineering blog

Webエンジニアです。Fundsというサービスを開発・運用しています。

新規開発における技術的負債への向き合い方

僕は現在、Funds(ファンズ)というサービスを開発・運用しています。

Fundsは2019年1月にローンチしたばかりのサービスであり、これから本格的にサービスを伸ばしていくというフェーズにあります。

スタートアップである以上、ビジネスニーズに対応するためのスピードが必要である一方、長期的にサービスを伸ばしていくという観点からは、初期の段階から技術的負債と向き合っていくことが重要だと考えています。

この記事では、プロダクトの新規開発時における自分なりの技術的負債への向き合い方を整理してみました。

技術的負債とは

Wikipediaによると、

行き当たりばったりなソフトウェアアーキテクチャと、余裕のないソフトウェア開発が引き起こす結果のことを指す新しい比喩である

と書かれています。

Wikipediaの定義を採用するのがよいかはさておき、技術的負債は一般的にもこのような意味で認識されていると思います。

技術的負債はどのような問題を生み出すか

プロダクトの改善スピードが遅くなる

僕が手がけているのはWebサービスですが、成長を見越したWebサービスにおいて、プロダクトの機能がずっと同じということはあまりありません。すでにある機能を改善したり、追加したりするのが普通です。

技術的負債があると、本来必要な実装と本質的には関係のない変更を加える必要が出てきます。結果、プロダクトの改善速度は徐々に落ちていくことになります。

一度限りのことであれば許容できるかもしれませんが、負債の厄介なところは、複利で利息が膨らむかのごとく、実装を重ねるほど負債も重くなっていくことです。

BizDev上の要請からくる機能の追加、あるいはUXの改善などを目的として、積極的に改善を進めたプロダクトほど負債が積み重なっていくというのは少し辛いものですが、技術的負債について考慮されていないサービスの場合、多かれ少なかれこうした状況が見られるのではないかと思います。

プロダクトの品質低下・バグを生み出しやすくなる

技術的負債がある状態で開発を続けると、小さな機能の追加や修正でもバグを生みやすい状態となります。

また、開発は続けているのにプロダクトの改善が思うように進まなくなることで、開発に携わるエンジニアのモチベーションも低くなってしまいます。

バグだけでなく、依存しているパッケージやライブラリのアップデートが行われず、セキュリティ上の問題を抱えたまま運用することになるというのも想定されうるリスクです。

担当できるエンジニアが固定化する

技術的負債は、そもそも負債となっている問題自体の説明が難しくなっている場合もあります。

技術的負債が積み重なった結果、組織としては、暗黙知と化した問題を理解している人しか対応できなくなり、チーム内での役割を変えたり引き継ぎを行うのが難しくなります。

この状態で無理に引き継ぎを行うと、引き継いだ先の担当者が暗黙知となっている地雷の避け方を知らず、不具合を生み出してしまうといったことも想像できます。

技術的負債の種は設計・開発の初期に生まれる

こうした技術的負債が生じやすいかどうかは、プロジェクト初期段階の設計や、実装ルールの統一などがどれだけ十分できているかによると思っています。

そこで以下では、プロダクトの初期段階での考慮が必要な要素の具体例を挙げてみます。

採用する実装技術・データストア

採用技術の選択は重要な課題です。言語やフレームワークは、一度採用すると、作り直さない限りあとから変更することはできません。

ライブラリも、例えばReactやAkkaなど、その選択自体が基本的な実装手法を決めるようなライブラリであれば、後から変更することは難しいでしょう。

またデータベースやデータストアなどの選択も同様です。データベースやデータストアの変更は、記録されているデータの移行を伴うため、簡単に変更することはできません。

もちろんこれらは一般論の話で、あとから変更可能な場合もあります。例えば、多数のマイクロサービスから構成するマイクロサービスアーキテクチャを採用した場合、サービス間のインターフェースや管理手段が整備できていれば、個々のマイクロサービス単位で言語を変更することも可能だと思います(一方で、マイクロサービスで構成するシステムの立ち上げには、かなり大きな初期投資を必要とすることも考慮すべきだと思います。)

実装コードの保護

それなりに成熟した言語であれば、成熟度に差こそあれlintツールやユニットテストを実行するためのライブラリが提供されています。

テストや基本的なコーディングルールをチェックできるようにし、CIで自動実行するといったことは、現代的なサービス開発では一般的です。

こうした実装を保護するアプローチをしていないと、品質の担保がされないコードが生まれ、将来の負債として積み重なっていきます。

ソフトウェアアーキテクチャ

アーキテクチャの設計が不明瞭なままだと、行き当たりばったりの実装が生まれていきます。

簡単なCRUDだけで成立するようなアプリケーションであれば別ですが、一般的な業務アプリケーションやWebサービスでは、多かれ少なかれCRUDだけでは完結しないようなビジネスロジックを要求されるのが普通だと思います。

どのレイヤー・パッケージにはどういったものを扱うのか・どのレイヤーで副作用の取り扱いを認めるのか、などを予め明確にしていないと、全体として一貫性のない実装となってしまいます。

取り扱うデータモデルの設計

RDBMSをはじめとするデータベースや、KVSなどのデータストア上のデータモデルの設計に関する負債も、返済が難しい負債の1つです。テーブル設計・データモデルの設計に失敗していたり、データストアの特性を十分考慮せずにデータストアを利用する実装を採用しまっているケースです。

データモデルに関する負債の解消は、データの変更を伴うため、一般的にはアプリケーションの実装に比べて運用開始後の変更が難しいものだと思います。データのサイズが大きく、サービスの停止が困難なサービスであればあるほど変更は難しくなります。

ドメインモデルとデータモデルを明確に分離しておくことで、データモデルだけを修正するようなアプローチも可能ですが、それでもなお気軽なリファクタリングをすることは難しいと思います。

クラウドプラットフォーム上のリソース

AWSGCPなどクラウドプラットフォーム上のリソース構築においても、特に最初の段階で注意深く設計すべき点があります。

例えば、ネットワークの構成を変更するには、事実上VPC内で運用されるリソースを再構築することになるため、可能な限り最初の時点で考慮しておきたい問題です。

またIAMなどを利用した権限管理においては、強すぎる権限の付与やクレデンシャルを直接埋め込むような安易な運用が問題になります。

こうした運用は単純にセキュリティ上の観点から避けるべきというのもありますが、運用開始後にこうしたポリシーの変更をする場合、アプリケーションが正しく動作することを保証しつつ設定や実装を加えていく必要があり、厄介な作業です。

技術的負債を少なくするために考えたいこと

ここまで挙げてきたような技術的負債の種を避けるため、初期段階では、次のようなことを考えるとよいのではないかと思っています。

プロダクトの寿命を考える

技術的負債について考えるにあたりまず重要なのが、設計・開発するプロダクトの寿命を考えることだと思っています。

プロトタイプのような、開発後3ヶ月しか使用しないようなプロダクトであれば、そもそも技術的負債について深く考察することが無意味だと思います。 (ただし当初はプロトタイプと考えていたものが、そのまま運用し続けられることも往々にしてあるので、然るべきタイミングで潔く捨てられることが必要です。)

逆に、長い期間で開発・運用し続けていくことを前提とする場合、そもそも採用したライブラリがメンテナンスされ続けるのかといった、外部のエコシステムの充実度も含めて検討する必要があります。

将来必要となる拡張をできる限り予想する

想定しているプロダクト運用期間の中で、将来どのような拡張の可能性があるのかを考えることも重要です。予想されうる機能をできる限り洗い出し、その実装や拡張に対して開いているかといったことを考えることで、将来的に負債化するリスクを抑えることができます。

ここで洗い出したことについて、最初からすべて実現可能な一般性を持たせる必要はないと思っています。

重要なのは将来の変更を可能にしておくという点で、バックアッププランだけでも事前に考えておければ、いざ改修が必要となった時の負担もそれほど高くないと思います。

変更に強い設計コンセプトを知る

ソフトウェアの開発においては、先人たちによって蓄積された様々なノウハウがあります。例えば次のようなものです。

ソフトウェアアーキテクチャ的なアプローチは、実装から一歩引いた視点で解説されることもあり、理論を実装へ適用することは少し難しいと感じることもあります。しかし裏を返せば、具体的な言語に依らず利用可能な手法であり、幅広い場面で使用できます。

またTwelve-Factor Appで提唱されている方法論は、現代的なアプリケーションに要求される性質がまとまった、優れた指針だと思っています。インスタンスベースの環境で運用しているアプリケーションをコンテナ主体の運用環境に載せ換えるような場合なども、この方針に沿っていれば、アプリケーションの変更なく移行が可能になるはずです。

こうした普遍的に適用可能なアプローチを知り、適用していくことで、明らかに失敗となるような問題を防ぐことができます。

基本的な設計だけは外部のエキスパートに依頼するのも1つの方法

設計に自信がない場合、基本的な設計については、外部のエキスパートに設計や基本的な土台を作ってもらうのも1つの方法だと思います。

通常、すでになんらかの実装があったとき、普通のエンジニアであれば既存の実装に合わせた形で実装していくはずです。

そのため、初期段階の設計やベースの実装が非常に重要となります。

最初の設計だけをエキスパートに依頼しつつ、よりドメインに特化した課題については、ドメインの詳細を理解している社内で取り組むというのも、現実的な解決策になると思っています。

許容できる負債を判断する

ビジネスニーズを叶えるためには妥協も必要

技術的負債の問題点や対処方法について書いてきましたが、実際には技術的負債が生まれることを許容せざるを得ない場面もあると思っています。

趣味で作っているプロダクトでもない限り、大抵のプロダクト開発には何らかの目的があります。できるだけ技術的負債は少ない方がよいとはいえ、技術的負債を減らす究極の目的は、長期的なビジネス上の成果を最大化させることであるはずです。

技術的負債を無くすことにこだわった結果として、ビジネス上の成果があげられないと意味がなく、なんらかの妥協が必要だと思っています。

妥協点は変更可能性が担保できること

では、どこで妥協すればよいのでしょうか?

現実的な工数で後から修正可能であるか否かということが、技術的負債を許容するかどうかの判断基準になると考えています。

一括りで技術的負債と呼ばれるものの中にも、後からも変更が可能なもの、変更がしづらいものなどさまざまです。

あらかじめ負債を返済する方法を考えておく

あらかじめ問題点を理解していれば、それに対する対処の方法を考えておくことも可能です。例えば以下のようなことが考えられます。

(例)コーディングスタイルの問題

僕は普段、ScalaJavaScriptなどの言語を使用しています。

Scalaであればscalariform、JavaScriptであればeslintの--fix オプションを使用するなど、ある程度自動的にコードの修正な可能なアプローチは利用できます。

(例)read主体のデータアクセスのパフォーマンス

パフォーマンスの問題は、アーキテクチャを正しく設計できてさえいれば、ある程度あとからでも調整できると考えています。

例えばread主体のデータアクセスだと、副作用を取り扱うレイヤーが分離されていれば、そのレイヤー内で実装を変更することができます。抽象的なデータアクセス手段を定義したインターフェースに対して、既存のデータアクセス実装の代わりに具体的なキャッシュ実装を組み込むなどしてパフォーマンスを改善することが可能です。

(例)AWSGCP上のリソース管理

クラウドプラットフォーム上のリソースの構築において、僕は普段Terraformを使用しています。

ただ、時にはアドホックにリソースを作ってしまいたいような場合もあります。

その場合もリソース作成時の情報をメモとして残しておくことができていれば、 terraform import でリソースを取り込むことができます。(その後HCLを調整していくのは少し面倒ですけどね。)

まとめ

長々と書きましたが、重要なのは技術的負債を認識し管理することであると思っています。

技術的負債があるから全て悪ということはなく、現在どのような負債があるのかという認識や、負債に対する返済計画を持っておくことが重要です。この見通しさえあれば、負債による成長の妨げも限定的な範囲に収められると思っています。

また逆に、0→1のフェーズであるから盲目的に技術的負債が許されるということはなく、むしろ0→1フェーズの負債は将来に渡って影響することを考えて、技術的負債を管理するのがよいのではないかと思います。