はじめに
こんにちは、検索基盤部の倉澤です。検索基盤部では、検索機能に必要なデータを生成するバッチシステムの開発や運用を担当しています。また、ユーザーのニーズやサービスの成長に合わせてリアーキテクチャを行うこともあります。今回は、リアーキテクチャを繰り返し行う中で見えてきたバッチシステムの内部設計の品質を高める・標準化するためのポイントを紹介します。
今回、バッチシステムの内部設計をソフトウェアのアーキテクチャ特性(品質特性とも呼ばれる)に基づいて説明します。
ソフトウェアのアーキテクチャ特性とは、非機能要件や品質特性と同じ意味を指しますが、「ソフトウェアアーキテクトの基礎 (Fundamentals of Software Architecture)」ではシステムが成功するために必要な運用と設計の基準として定義されています。アーキテクト特性には拡張性(Scalability)やメンテナンス容易性(Maintainability)、再利用性(Reusability)などがあり、イリティ(ility)とも呼ばれます。
リアーキテクチャによって得たバッチシステムの内部設計のポイント以下3点を紹介します。
- ビジネスロジックはなるべく1つのコンポーネントに集約する
- データの抽出、変換、統合、削除などの各処理をなるべく小さなコンポーネントに分割する
- データリソースが増えた場合を考えデータ抽出処理と変換処理を独立させる
検索基盤部では、内部設計の品質を高めるためにDesign Docを活用しています。どのような項目に何を意識して記載しているかを最後に紹介したいと思います。
目次
- はじめに
- 目次
- 背景
- リアーキテクチャ前のバッチシステムの課題
- Design Docの活用
- まとめ
- おわりに
背景
ZOZOTOWNの検索機能の1つにユーザーが入力したクエリの意図を解釈する機能があります。この機能は、ユーザーが入力したクエリを解釈し、検索結果の品質を向上させることを目的としています。例えば、ユーザーが「ジャケット」というクエリで検索した場合、音楽に関連した情報ではなく洋服のカテゴリーであることを解釈し、検索結果の品質を向上させます。
この機能を実現させるために、ファッション用語を定義した辞書を作成するバッチシステムを開発しました。検索機能はバッチシステムが作成した辞書を元にルールベースでクエリの意図解釈を行います。この意図解釈を行う辞書は、社内外にある様々なデータソースからタームとその意味を抽出し、検索機能が利用できる形式に変換したり、ZOZOTOWN独自のビジネスロジックを適用したりすることで生成されます。
本文中の「ビジネスロジック」とは、辞書で定義しているタームの意味を恣意的に変更することを指しています。例えば、ZOZOTOWNで扱っているブランド名には一般名詞に該当するものがあり、ZOZOTOWNで扱う辞書を生成する際には該当のブランド名を一般名詞ではなく固有名詞として扱うことがあります。
今回紹介するバッチシステムは上記のようなドメイン特性がありますが、データ抽出・変換・統合・削除など一般的なバッチシステムの特性を持っています。そのため、本記事で紹介する内容は今後のバッチシステムの開発・運用においても参考になると考えています。
以下がリアーキテクチャ後のバッチシステムの全体像です。
上記のバッチシステムは主に2つに分かれています。
- ビジネスロジックが介入していない辞書を作成するバッチシステム
- 各サービスのビジネスロジックが介入している辞書を作成するバッチシステム
ビジネスロジックが介入していない共通処理をまとめたバッチシステムが前段にあることで、各サービスのバッチシステムはビジネスロジックの実装に集中できます。また、辞書を活用するサービスが増えた場合でも拡張しやすい構成となっています。
なぜ、上記のようなバッチシステムのアーキテクチャを採用したかを、次章のリアーキテクチャ前に存在していた課題を紹介することで説明します。
リアーキテクチャ前のバッチシステムの課題
リアーキテクチャのきっかけは、バッチシステムが生成する辞書をZOZOTOWN以外のサービスで使用したいという要望が発生したことでした。リアーキテクチャ前は、ZOZOTOWNの検索機能で利用することだけを考えた設計になっており他サービスはZOZOTOWNのビジネスロジックが介入した辞書を使うしかありませんでした。
以下がリアーキテクチャ前のバッチシステムの全体像です。
上記の構成図からわかるようにコンポーネントから出力される成果物は全てビジネスロジックが介入しています。ZOZOTOWNのビジネスロジックが介入している成果物を他のサービスで展開することは適していません。
これから紹介する課題はリアーキテクチャ前のバッチシステムが良くない設計だったというわけではなく、要求に応えるためのリアーキテクチャによって発生した課題です。当時の状況を考えるとリアーキテクチャ前のバッチシステムの構成はシンプルな設計だったと言えます。
良くない点1: ビジネスロジックが複数のコンポーネントに点在している
なぜ良くないのか
- 課題1. ビジネスロジックの変更がしにくい・仕様の把握がしにくい
- 課題2. 他のサービスに成果物を展開できない
課題1: メンテナンス容易性(Maintainability)
「メンテナンス容易性(Maintainability)」とは、システムの変更や拡張がどれだけ簡単に行えるかを示す特性です。
コンポーネントのビジネスロジックに変更を加える際、他のコンポーネントのビジネスロジックに影響がないかを常に気にしないといけないため変更が難しくなります。また、各コンポーネントにビジネスロジックが点在していることで仕様を把握することが難しくなります。
課題2: 拡張性(Scalability)
「拡張性(Scalability)」とは、システムがどれだけ成長に対応できるかを示す特性です。
各コンポーネントにビジネスロジックが点在していることで、他のサービスに成果物を展開することが難しくなり、拡張性が損なわれることを示しています。
どう解決したのか
- ビジネスロジックをなるべく1つのコンポーネントに集約することで、仕様の把握から変更をしやすくし、メンテナンス容易性(Maintainability)を高める
- 各サービスで共通となる成果物を生成するバッチシステムを起点にして、そこから他のサービスに成果物を展開できる設計にすることで、拡張性(Scalability)を高める
良くない点2: 1つのコンポーネントに複数の責務を持つ処理が混在していた
なぜ良くないのか
- 課題1. コンポーネントを再利用することが難しい
- 課題2. インシデントの原因究明に時間がかかる
- 課題3. コンポーネントのテストが難しい
- 課題4. コンポーネントの凝集度が低くなる
課題1: 再利用性(Reusability)
「再利用性(Reusability)」とは、システムの一部を他のシステムや他の部分で再利用できるかを示す特性です。
1つのコンポーネントにデータ抽出から変換処理など様々な責務を持つ処理が混在していると、特定のバッチシステムに特化したコンポーネントとなり、再利用性が損なわれます。
課題2: サポート容易性(Supportability)
「サポート容易性(Supportability)」とは、システムのエラー時に必要となる情報のログを整えられているかを示す特性です。
私たちはコンポーネント内の各処理で都度、成果物の状態を全てログとして残すのは現実的でないと判断したため、コンポーネントの出力結果をデータストレージに保存しています。そのため、コンポーネント内のどの処理でどのようなデータが生成されたのかをトレースすることが難しくなり、インシデントの原因究明に時間を要することがありました。
課題3: テスト容易性(Testability)
「テスト容易性(Testability)」とは、システムのテストがどれだけ簡単に行えるかを示す特性です。
テスト容易性は複数の要素から構成されます。課題3は「単純性(Simplicity)」と「理解容易性(Understandability)」に関わります。複数の責務を持つコンポーネントは、テスト対象となるコードの増加により単純性が損なわれます。さらに、テスト対象が増えることでテストケースの数も増え、テストの目的が理解しにくくなります。
課題4: 凝集度
課題4はコンポーネントの処理がどのくらい関連度のある塊になっているかを示す凝集度に関わる課題です。コンポーネント内に複数の処理があること自体は問題ありませんが、まとまり具合が問題となります。
リアーキテクチャ前の実装では、時間的凝集になっている箇所がありました。時間的凝集とは、同じようなタイミングで実行される処理をまとめ、実行順序を入れ替えても問題ない特徴があります。ここでは、ビジネスロジックと正規化処理を入れ替えても問題ないということがわかっていました。凝集度が低いコンポーネントは、課題3で説明したテスト容易性が損なわれることにも繋がります。
また、コンポーネント内の処理間に新しく追加したい処理が発生した場合、既存のコンポーネントへ実装することになり、さらに凝集度は低くなります。
どう解決したのか
- データの抽出、変換、統合、削除などの各処理をなるべく小さな単位でコンポーネント化し、再利用性(Reusability)やテスト容易性(Testability)、凝集度を高める
- 正規化処理の具体的な処理内容は外からコントロールできるようにし、再利用性(Reusability)を高める
- 小さなコンポーネント毎に成果物を出力することで状態をトレースしやすくし、サポート容易性(Supportability)を高める
- 削除処理を1つのコンポーネントに集約し、最後に配置することでインシデント対応時の情報源となるデータの欠損を防ぎ、サポート容易性(Supportability)を高める
良くない点3: データ抽出処理が独立していない
なぜ良くないのか
- 課題1. 新しくデータリソースが増えた場合に対応しづらい
課題1: 拡張性(Scalability)
バッチシステムのデータリソースは将来増えることが予想されていました。そのため、データ抽出処理とシステムの拡張性は密接に関連しており、データ抽出処理が独立していないとデータリソースが増えた場合に対応しづらく拡張性(Scalability)が損なわれます。
成果物のデータ形式にJSON Linesを採用しているため、各コンポーネントにはJSON Lines形式に変換する処理が含まれていました。そのため、データリソースが増えた場合はデータ抽出処理に加えて変換処理を都度実装する必要もありました。
どう解決したのか
- データ抽出処理を独立させることで、データリソースが増えた場合にも対応できるように拡張性(Scalability)を高める
- データ抽出以外のコンポーネントの入力形式をJSON Lines形式に統一し、変換処理の責務をデータ抽出処理に集約することで、拡張性(Scalability)を高める
Design Docの活用
Design Docとは、システムの設計に関するドキュメントのことです。Design Docを用いて議論することで設計の品質を高めることを目的としています。今回のリアーキテクチャにおいても、Design Docを活用しました。
以下にDesign Docで共有した情報を紹介します。
なにを書くか
検索基盤部では、Design Docの雛形を各プロジェクトの要件に合わせてカスタマイズしています。今回のリアーキテクチャで共有した情報をいくつか紹介します。
Goal / Non-Goal
プロジェクトの目的(Goal)と意図的に目指さないこと(Non-Goal)を記述します。リアーキテクチャの場合は、なぜ目的としているのかの背景や歴史的な経緯を記述し、途中から加わったメンバーにも目的を共有できるように意識しています。
用語集
プロジェクト内で用いる単語の定義を記述し、コミュニケーションの精度を高めることを目指しています。
全体のシステム構成図
システム全体が複数のコンポーネントから構成される場合、コンポーネント間の関係を示す図を作成します。この図によって、プロジェクトメンバーが全体像を把握しやすくなります。
議論段階では、考えられる設計案を複数提示しそれぞれのメリット・デメリットを記述するようにしています。リアーキテクチャの場合は現在のシステム構成図とリアーキテクチャ後に予定しているシステム構成図を比較しやすいようにしています。
コンポーネントの定義
システム全体を構成するコンポーネントの処理内容とインプットとアウトプットを記述します。さらに、インプットとアウトプットの例を記述し、処理のイメージを理解しやすくするように意識しています。
効果
今回Design Docを活用したことで、以下のような効果がありました。
- リアーキテクチャ前のバッチシステムの課題を共有し、解決策を共有できた
- バッチシステムのドメイン知識やビジネスロジックを共有し関係者の理解を深めることができた
- Design Docの設計から実装がズレていないかをチェックできた
- 内部設計に関する知識や情報を共有できた
まとめ
本記事では、バッチシステムの内部設計の品質を高めるためのポイントをリアーキテクチャの事例をもとに紹介しました。また、アーキテクチャ特性の観点で課題を整理し、それらの課題を解決するためのアプローチを紹介しました。
バッチシステムの外部設計に関するテックブログなどは散見されますが、内部設計に関する情報は多くありません。そこで、本記事がバッチシステムの開発や運用に携わるエンジニアの方々の参考になれば幸いです。
おわりに
ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!