はじめに
こんにちは! Yahoo!知恵袋の津村です。去年の11月からYahoo!知恵袋のフロントエンドシステムのリアーキテクトに取り組んでいます。この記事では、これまで抱えていた技術 的な問題と、それらをどう解決したかについて説明します。この結果、開発効率向上やレビュー時間短縮などの効果がありました。
Yahoo!知恵袋は利用者登録者数5,200万人、質問総数2億8,000万件、回答総数は6億5,000万件以上(2024年4月3日現在)ある、日本最大級のQ&Aサイトです。2004年からサービスを開始し、今年20周年を迎えます。
Yahoo!知恵袋はモバイルクライアント、バックエンド、フロントエンドの3つのシステムで構成されています。今回対象とするフロントエンドシステムは、ブラウザやYahoo! JAPANアプリからアクセスされる際のWebページを提供するシステムです。
Yahoo!知恵袋のフロントエンドシステム
現在、Yahoo!知恵袋のフロントエンドシステムは以下のような構成になっています。約6年前にPHPからJavaScriptに移行し、Express.jsやReact.jsを利用してシステムを構築しています。Next.jsは現在利用せず、BFF(Backends For Frontends)は存在していません。フロントエンドシステムから直接(サービス内外の)APIをたたいてデータを取得しています。
次にYahoo!知恵袋のページが表示されるまでにフロントエンドシステムで行われる一連の処理について説明します。
- ユーザーがYahoo!知恵袋のページを訪問すると、controllerが呼び出される
- controllerからservice(APIごとに存在する)を呼び、serviceからAPIに対してリクエストし、データを取得する
- serviceから受け取ったデータをcontrollerに返却する
- controllerは取得したデータを元に、viewmodelを利用して、Webページの表示に必要なデータを生成する
- そのデータをReact Componentに渡して、React Componentを元にWebページの表示に必要なHTMLを作成し返却する
今回のリアーキテクトでは上図の赤線で囲われているcontrollerのみを対象としています。理由としては、controllerには、ルーティング、各層との連携、静的なバリデーション、データの加工、主要なビジネスロジックのほぼ全部が存在するため、処理が複雑であり、事故の原因となることが多く、開発効率・品質・保守性への影響が大きかったからです。
将来的にはcontrollerで行う処理を減らし、代わりにapplicationService層などの層を新たに作成することで、controllerにあるルーティング・静的なバリデーション・レスポンスの返却以外の処理を適切な層に移行することを目指しています。
対象範囲
全体のリアーキテクトを行う上で、まず他のメンバーがまねできるようなモデルケースとなるような実装が必要であったため、1つのcontrollerに絞ってリアーキテクトを行いました。今後、他の箇所にも同様のアーキテクチャを適用していく予定です。
今回は、そのモデルケースとして質問詳細ページのcontroller(detail controller)をリアーキテクトしました。質問詳細ページは質問に対する回答やリアクション、ランキング、回答への返信等様さまざまな情報が存在するページです。detail controllerを選んだ理由は、機能が一番多く、それに伴いコード量も多いので、モデルケースとして最も適していると考えたためです。他には、一番リアーキテクトを行った結果 が実感やすいという理由もあります。結果が実感できれば、他の箇所にも同様のアーキテクチャを適用していく話にもつながりやすいと考えました。
Yahoo!知恵袋のフロントエンドシステムが抱える問題
将来的には、applicationService層に処理を移行することも踏まえつつ、今回のリアーキテクトでは以下の問題の解決に取り組みました。
問題1: ロジックの集中と重複が多く発生していて、保守性が低くバグが起きやすい
controllerには、ルーティング、静的なバリデーション以外に、各層との連携やデータの加工、主要なビジネスロジックのほぼ全部が存在しています。そのため、追加修正を行いづらく、いろいろなケースのユニットテストを書くことも難しいため、保守性が低くバグが起きやすい状況になっていました。また、微妙に異なるロジックがcontrollerごとで存在しているため、意図せぬバグが発生していました。
問題2: 型がないため、実行時になって初めて発覚するエラーが多い
JavaScriptを利用しているため、実行時になって初めて型エラーが多々発生していました。例えば、mapメソッドをarrayに対して呼んでいるつもりが、実際にはundefinedに対して呼んでしまっていて、実行時になってエラーが発生していました。また、型がないため、関数の引数や返り値の型が分かりづらく、コードの理解が難しい状況になっていました。
解決策
問題1への対処: 重複したロジックを排除しつつ、ロジックの集中を緩和
以下のようなアーキテクチャを導入することで、controllerはルーティング・静的なバリデーション・レスポンスの返却のみを行うようにし、controllerへのロジックの集中を緩和することを目指しました。それを実現するために、以下の2つの方法を考え、1を選択しました。
- controllerからmodelとutilityに処理を移動させてからapplicationServiceを作成し、applicationServiceからmodelとutilityを呼び出す
- controllerからapplicationServiceに処理を移動させてからmodelとutilityを作成し、applicationServiceからmodelとutilityを呼び出す
理由としては、1を選択した場合、controllerに処理が存在する期間が2と比べて長いため、既存のコード(問題2の解決策で説明しますが、detail.js(既存)とdetail.ts(新規)が存在しました)と見比べやすく、追いつき作業が行いやすいからです。今回は1ヶ月という短い期間でリアーキテクトとTypeScript化を行う必要があったため、1を選択しました。1を行うことで、controllerから処理を逃がし、これから追加する予定のapplicationService層への移行を行いやすくしました。これにより、重複したロジックを排除しつつ、ロジックの集中を緩和させました。