Stanby Tech Blog

求人検索エンジン「スタンバイ」を運営するスタンバイの開発組織やエンジニアリングについて発信するブログです。

モジュール分割による負債解消の取り組み

こんにちは、スタンバイで検索周りの開発を担当している鷹取です。
今回は検索関連についてではなく、スタンバイの技術負債解消についての取り組みについてご紹介します。

概要

Stanby Tech Blogのスタンバイ2+1年の軌跡の記事でも少しだけ触れられていますが、
スタンバイのシステムには複数チームでメンテしているstanby-apiと呼ばれるコンポーネントがあります。
stanby-apiはスタンバイのWeb画面を表示するための重要なコンポーネントですが、
歴史が長いこともあり、コードの複雑化や開発体制とのミスマッチにより、プロダクト全体の開発生産性の低下を招いていました。このstanby-apiの複雑化してしまったコードを、適切な粒度でモジュールに分割することにより、問題の解決を図ったというのが、本記事の内容になります。

stanby-apiとは

まず最初にstanby-apiについて説明します。
スタンバイはWeb上で求人検索を提供しているサイトですが、
その実現のためには、以下のような様々な機能が必要になります。

  • 求人の検索
  • 広告配信
  • 住所情報の解析
  • クエリの解析
  • クエリのサジェスト
  • 応募機能
  • ABテスト機能等

これらの機能は、スタンバイ内部の複数のコンポーネントがAPIとして提供しています。
stanby-apiはフロントエンドからのリクエストを受け、
バックエンドの各API群を適切な順で呼び出し、
それらのデータを統合してフロントエンドに返却する役割を担っています。

stanby-apiが抱えていた課題

上記で述べたように、stanby-apiはフロントエンドへのレスポンスを返す役割を果たすコンポーネントですが、最初からそのように設計されたわけではありませんでした。初期段階では、現在ほど多くの機能やコンポーネントが存在せず、バックエンドの機能が直接stanby-apiに実装されていました。時間が経過するにつれ、スタンバイの機能が増え、stanby-apiにも多くの実装が追加されました。しかし、これらの変更は全体のアーキテクチャを総合的に検討せずに行われ、将来の展望に基づいて設計されたものではなかったため、必要な機能が段階的に追加され、APIが無秩序に成長してしまいました。

さらに、組織の拡大と体制の変化に伴うチームの分割や統廃合、開発者の異動などもあり現在では、stanby-apiは複数のチームの開発者によって開発される状態になりました。しかしながら、チーム間の責任分担が明確でなかったため、各チームが自由に実装したコードがいたるところで入り混じり、stanby-apiの全体像を把握する開発者の不足に繋がりました。

結果として、stanby-apiの開発においてさまざまな問題が発生しました。必要な情報の把握には多くの時間がかかり、機能の開発に取り組むまでに遅延が生じました。一見するとささいな変更であっても、無関係のテストが失敗する事例も発生しました。何人もの開発者や複数のチームが同一のコードベースに変更を加えるため、コンフリクトが発生し、リリーススケジュールの管理が煩雑となりました。このような理由から、stanby-apiの開発はスタンバイの成長において大きなボトルネックとなっていました。

モジュール分割による解決

この問題を解決するために、stanby-apiに対するリファクタリングプロジェクトを立ち上げました。
このプロジェクトでは、以下の2つをゴールとして設定しました。

  • 絡み合ったコードを整理し、stanby-apiのコードを機能ごとにモジュールへ分割することで、依存関係を分離し、機能間の境界を明確にすること
  • 各モジュールに担当グループを割り当て、作業範囲を限定、責任を明確にすること

プロジェクトを進行する際、まず最初にモジュールの分割方針を策定しました。

モジュールを分割する際、最も検討が必要なポイントは、どの単位でモジュールを分けるかです。分割単位が小さすぎると保守性が低下し、大きすぎるとコードの複雑性の増加につながる可能性があります。しかし、モジュールの分割単位については絶対的な正解はありません。このプロジェクトを進める際、各チームと実際のコードを検討しながら、モジュールの分割単位を合意するためにいくつかの方針を設けました。

  • モジュールの分割単位は、担当チームが単独で開発・運用できる単位とする。
  • 既にAPIとして独立した機能が切り出されている場合、外部API呼び出しを1つのモジュールとして分割する。

これらの方針を定めることで、モジュールの分割に関する議論を具体的に進め、合意を得る際の指針としました。

方針が固まった後、具体的なモジュール分割方法を決めました。
モジュールのインタフェース定義には、Protol Buffersを採用しました。

これまでは、特定の機能でのみ使用される想定のクラスやメソッドが、stanby-apiのどこからでも呼び出し可能になっていたため、本来関係のない箇所で使用されていることがありました。
例えば、求人情報を表すJobクラスに実装されている求人のタイトルを表示を整えるために加工するメソッドが、検索APIを呼び出しているメソッド内で使われているといったことがありました。このような状況では、変更の影響範囲が特定できず、小さな変更に対しても調査に時間がかかってしまいます。ソースコードを単にモジュールに分割しただけでは、この問題は解決できません。今いるエンジニアが注意を払って開発していても、いずれ新たに入ってきたエンジニアが、意図せずにモジュール外からモジュール内部のロジックを呼び出してしまう可能性があります。

そこで、Protocol Buffersを使用して、モジュール間でビジネスロジックが漏れ出ることを防ぐことにしました。Protocol Buffersでスキーマを定義し、ツールを用いてモジュール呼び出しのインプットとアウトプットを表すクラスのコードを生成します。このクラスには、数値や、文字列、配列、オブジェクト型のメソッドを持たないデータのみが含まれます。モジュールの実装側と呼び出し側は、このクラスにだけ依存するようにすることで、ロジックがモジュールの外に漏れ出てしまうことを防ぐことができます。

モジュールの分割の進め方として、モジュールを分割する際には、一括で全てのモジュールを切り出すのではなく、段階的に進めることにしました。最初に影響が比較的小さいモジュールをトライアルで切り出し、モジュールの分割における実績を積むことに重点を置きました。
そこで得た経験を活かしながら、他の開発に対する影響を最小限に抑えつつ、順次モジュールを切り出していきました。

モジュール分割によって得られた効果

上記の方針に従ってプロジェクトを進め、モジュール分割は約半年で完了しました。
モジュール分割により得られた効果として、複数チーム間での開発が非常にやりやすくなりました。
すべてのコードの担当グループが明確になったため、ある機能を追加しようとした際に、どこのチームに相談をすればよいかが一目でわかるようになりました。
また、スキーマ定義の変更箇所が決まってしまえば、モジュール呼び出し側とモジュールの実装側で並行作業ができるようになり、開発期間が短縮されました。
同じコードを触ることがなくなったので、コンフリクトも減少しリリースの管理も容易になりました。おまけに、誰の持ち物かわからず削除できていなかった大量のデッドコードを削除でき、コードがクリーンになりました。

今後について

今回はstanby-apiというコンポーネントのリファクタリングとして、ボトムアップのアプローチでモジュール分割を進行しました。しかしながら、このような課題は1つのコンポーネントに限らず、プロダクト全体で発生する可能性があります。このような問題を未然に防ぐため、全社的なアーキテクチャの確立が必要と考えられます。

近年、マイクロサービスやモジュラモノリスなどのアーキテクチャが広く採用されつつあります。今回の改修において、stanby-apiはモジュラモノリスとマイクロサービスの中間的なアーキテクチャとなりました。将来的に全社的なアーキテクチャがどのようになるかに関わらず、今回の改修によりスムーズな移行が可能となっています。

まとめ

本記事では、stanby-apiのモジュール分割によるリファクタリングについてご紹介しました。
Stanbyの重要なコンポーネントであるstanby-apiは、長い歴史と複雑性から生産性低下の課題を抱えていました。モジュール分割プロジェクトにより、モジュールの明確な分離と管理体制の整備が行われ、開発生産性が向上しました。将来のアーキテクチャ変更にも柔軟に対応可能な状態となりました。

スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。
www.wantedly.com