はじめに
こんにちは。基幹システム本部・物流開発部の岡本です。普段はZOZO基幹システムのリプレイスを担当しています。
ZOZOではさらなる成長のため、様々なリプレイスプロジェクトが進行しており、これまでにZOZOTOWNやWEARなどのプロダクトにおける多くのリプレイス事例を公開してきました。本記事では、2022年8月より本格始動したZOZO基幹システムリプレイスの第一弾であるZOZOの物流拠点「ZOZOBASE」を支える「発送システムリプレイス」を紹介します。「発送システムリプレイス」は設計を終えた開発段階で、リリースに向けて進行中です。本記事を皮切りに今後も継続的に発信を続けていくので、是非ご注目ください。
現状の「発送システム」は、Classic ASPのトランザクションスクリプトで実装された大規模なモノリス構成のシステムの一部であり、「障害リスク」と「開発速度の低下」に課題を抱えています。本リプレイスでは「障害リスク」の課題解決のために既存のモノリスから発送機能のみを切り出して、既存のモノリスと疎結合な発送マイクロサービスへと移行します。また「開発速度の低下」の課題解決のために発送マイクロサービスはJavaのドメインモデルパターンで実装します。
本記事では、「発送システムリプレイス」での様々な工夫のうち、「データベース分割の工夫」と「ドメインモデルがビジネス上の関心事の表現に専念するための工夫」の2つを中心に紹介します。この事例が我々と同じく大規模なモノリス構成の基幹システムの開発・運用を課題に感じており、リプレイスを検討している方々の一助となれば幸いです。
目次
ZOZOBASE
「ZOZOBASE」は、ZOZOの保有する物流拠点の名称です。現時点で4つの「ZOZOBASE」が稼働しており、2023年8月には自動化を推進した「ZOZOBASEつくば3」の稼働開始も控えています。
また「ZOZOBASE」では、荷受け・検品・採寸・撮影・入出庫・発送・返品対応などの業務が内製したClassic ASP製のWebアプリケーションで行われており、現在も機能追加が頻繁に行われています。
発送システム
本記事では、発送機能を提供するシステムを「発送システム」と呼びます。具体的に発送機能とは「ZOZOBASE内の商品をピッキングし、注文通りに梱包してお客様宛てに発送する」機能を指します。また、ZOZO基幹システムが提供する機能のうち、発送機能を除いたその他の機能をまとめて基幹機能と呼びます。下図は「発送システム」を含めた現状のZOZO基幹システムの全体を簡易的に表現したものです。
ZOZO基幹システムは、BO(Back Office)とBO2(Back Office 2)というClassic ASPのWebアプリケーション(以降、BO・BO2と表記する)で構成されます。BOはPC用Webアプリケーション、BO2はZOZOBASE内のハンディ端末用のWebアプリケーションで、どちらも基幹DBと通信します。それぞれが提供する主な機能は以下の通りです。
- BO
- ZOZOTOWNへの出品やセールの設定などを管理するためのEC管理機能
- ブランド様への月次の精算書発行やZOZO経理部へ売上や手数料などを連携するための経理機能
- お客様対応のためのカスタマーサポート機能
- ブランド様から送られてきた商品を保管するための入荷機能
- お客様へと商品を発送するための発送機能
- etc
- BO2
- 入荷業務のひとつである棚入れのための機能
- 発送業務のひとつであるピッキングのための機能
- etc
発送システムはBOとBO2の一部であり、発送システムの開発および運用は専任のチームが担当しています。基幹機能とも一部結合していますが、基本的には発送システムは基幹機能とは疎結合なモジュールとして実装されています。
発送システムの課題
ここからは、発送システムの抱える2つの大きな課題について詳しく説明します。
システム障害リスクの増大
1つめの課題は発送システムの障害リスクの増大です。先に示した通り、発送システムはBO・BO2という巨大でモノリシックなアプリケーションの一部です。アーキテクチャ上、様々な機能と密結合しています。そのため、下図のようにDB障害やWebサーバーの障害など、発送システムとは直接関係がなくとも、基幹機能を提供するシステム起因の障害に巻き込まれる可能性を否定できません。例えば、経理機能の利用が月初に急増することが原因で、発送機能のパフォーマンスが著しく低下するなどの事象が発生する可能性もあります。
ZOZOTOWNでは、注文が入ったその日のうちに商品を発送する即日配送などのサービスをエンドユーザーに提供しています。発送機能の障害は社内ユーザーだけでなく、ZOZOTOWNのエンドユーザーのサービス体験を損なう大きなリスクとなります。そのため、基幹機能を提供するシステムに起因する障害の発送システムへの影響を避け、発送業務が停止しないようにする必要があります。したがって、下図のように発送機能の責務を持つ発送マイクロサービスを既存のモノリスと疎結合な形で実装し障害を分離する必要があると判断しました。
ビジネスロジックの複雑化に伴う機能追加の労力の増大
2つめの課題はビジネスロジックの複雑化に伴う機能追加の労力の増大です。 「ZOZOBASEつくば3」の新規稼働などを控えるZOZOにおいて、発送システムの重要性と寄せられる期待がますます高まっており、このギャップが大きな課題となっています。
トランザクションスクリプト
発送システムの含まれるBOとBO2はトランザクションスクリプトで実装されています。具体的には、下図のように、WebアプリケーションのUIから要求を受けたコントローラー内でSQL文字列を動的に組み立てビジネスロジックを構築しています1。
以下は、Patterns of Enterprise Application Architecture(以降、PofEAAと表記する)でのトランザクションスクリプトについての記述です。
ビジネスロジックが複雑になるにつれて、優れた設計を維持するのは大変になる。特に注意すべきことは、トランザクション間での重複の問題である
この記述と同様の問題が発送システムでも発生しています。具体的にはユースケース間で共通のロジックを持つことができないために、同様のロジックが複数のユースケースに書かれています。新たな機能を追加する際には、既存機能への全ての影響を把握したうえで適切な変更を加える必要があります。しかし、Classic ASPをサポートする高機能なIDEも存在しません。そのため、既存機能への影響調査はgrepコマンドと目視で行われており、これが業務時間の多くを占めてしまっている現状があります。
ドメインモデル
これまで述べたような課題を解決するための方法として、PofEAAでは、データとプロセスが一体化したドメインモデルを用いてビジネスロジックを構築するパターンがあげられています。ドメインモデルパターンでは、下図のようにドメインモデルでビジネスロジックをカプセル化して凝集度を高めることができます。ユースケースはドメインモデルを使い処理を組み立てる役割を担い、トランザクションスクリプトの時のように、ユースケース間でのロジックの重複を避けることができます。
また、下図はPofEAA内でトランザクションスクリプトとドメインモデルについて比較している図を抜粋したものです。ここでは両者の比較に焦点を当てるため、テーブルモジュールとの比較に関する表現を省略しています。
この図はドメインロジックの複雑性が赤丸で示した臨界点を超えた場合、ドメインモデルを用いるとトランザクションスクリプトよりも機能追加の労力が小さくなるとことを表しています。発送システムは10年以上改修を続けて成長し続けてきたシステムであり、この臨界点をゆうに超えているため、本リプレイスではドメインモデルへの移行が必要と判断しました。
発送システムリプレイス
これまでに説明した課題の解決のために、本リプレイスでは以下のアーキテクチャを採用します。
左の緑の部分はオンプレミスで稼働している既存のモノリスで、橙の線で囲った部分が本リプレイスで新たにクラウドに構築するシステムです。
クラウドに構築する新システムのうち、右の赤の部分は発送マイクロサービスです。Javaによるドメインモデルパターンでモノリスとは疎結合に実装します。中央の青の部分はモノリスのDBに接続する新たなシステムです。これはレガシーである既存のモノリスに大きく手を加えることなくリプレイスを進めるため、既存のモノリスと発送マイクロサービスを接続するためのモジュールとしてJavaで実装します。この部分は既存のモノリスとDBを共有しているためモノリスの一部と捉えます。
以降、具体的な課題の解決方法と設計時に考慮した点を踏まえてアーキテクチャについて詳細に説明します。
障害の分離
まず、1つめの課題であげた障害の分離の実現について説明します。課題の説明の中で述べた通り、本リプレイスでは、発送システムをモノリスと疎結合なマイクロサービスに移行することで障害を分離します。
上記のリンク内で説明されているように、マイクロサービスアーキテクチャにはさまざまなメリットとデメリットがあります。本リプレイスでは、特に障害の分離とデータの分離に焦点を当て、「既存のモノリスに障害が発生しても発送マイクロサービスが問題なく稼働し発送業務を遂行できる状態」の実現を主な目的として設計します。
結果整合性を用いたデータベースの分割
目的を達成するためにはデータベースの分割が不可欠です。ここでは、モノリスと疎結合な発送マイクロサービスを実現するために行ったデータベースの分割について説明します。下図は基幹DBのテーブルを使用(参照・更新)している機能に着目して整理したものです。
全てのテーブルは以下のいずれかに分類できます。
- 基幹機能のみが使用するテーブル
- 基幹機能と発送機能どちらも使用するテーブル
- 発送機能のみが使用するテーブル
モノリスは基幹機能の責務を持ち、発送マイクロサービスは発送機能の責務を持つため、1
はモノリスに残して、3
は発送マイクロサービスへとテーブルを完全移行することが比較的容易です。しかし2
は双方で使用するためどちらか一方に所有権を持たせつつ、他方でも利用可能にする必要があります。
本リプレイスでは、「既存のモノリスに障害が発生しても発送マイクロサービスが問題なく稼働し発送業務を遂行できる状態」を目的とするため同期的なシステム通信を無くすことが重要です。そこで、「常に非同期通信による結果整合性を許容できるか?」「非常時にも同期通信による整合性は必要か?」という2つの観点で改めて整理しました。
結果整合性を許容できない | 結果整合性を許容できる | |
---|---|---|
非常時に整合性が必要である | 2-A | 2-C |
非常時には整合性が必要ない | 2-B |
2-A
のようなテーブルが存在する場合には、既存のモノリスシステムが停止すると発送マイクロサービスも停止せざるを得ず障害の分離は実現できません。結果として本リプレイスにおいてはそのようなデータは存在せず、障害の分離が可能である確信を得ることができました。以降では、2-B
および2-C
のデータを双方で利用するために、どのようにシステム間通信するか具体的に説明します。
システム間の同期通信
下図のような同期的なシステム間通信をすることで、2-B
のテーブルをモノリスと発送サービスの双方で利用します。データはモノリスに残して、発送マイクロサービスはモノリスの提供するREST APIを同期通信で利用します。ただし、非常時には整合性を保つ必要はないため、通信を諦めて発送マイクロサービス側のデータのみで発送機能を進行します。
2-B
のテーブルの具体例として「搬送備品2管理テーブル」があげられます。モノリスの基幹機能と発送マイクロサービスの発送機能は物理的に同じ搬送備品を使用します。そのため、基本的には結果整合性ではなく同期的な整合性が求められます。しかし、モノリスでの大規模な障害発生などの非常時では、搬送備品の管理より発送業務の進行を優先し、発送マイクロサービスはモノリスがレスポンス不能な場合にも処理を続行します。
システム間の非同期通信
2-C
のテーブルは所有権(書き込みの権限)をどちらが持つべきか検討した上で、モノリスおよび発送マイクロサービスいずれか一方に所有権を移行します。そして、下図のようにコンシューマとプロデューサーを用意して、非同期通信することで結果整合性を担保して双方で利用します。
2-C
のテーブルのうち、モノリスが所有権を持つものの具体例としては商品や配送先などの情報を持つ「発送依頼テーブル」などがあげられます。また、発送マイクロサービスが所有権を持つものの具体例としては発送マイクロサービス内で梱包作業が完了したことを示すデータなどを持つ「梱包完了テーブル」などがあげられます。「発送依頼」はモノリスで更新され、発送マイクロサービスへ非同期的なシステム間通信で反映されます。反対に「梱包完了」は発送マイクロサービスで更新され、モノリスへ非同期的なシステム間通信で反映されます。このとき、モノリスと発送マイクロサービスで同じ構造のテーブルを持つ必要はなく、各々が適切な形でデータを保有できます。
複雑なビジネスロジックの整理
次に、2つ目の課題にあげたドメインモデルパターンへの移行について紹介します。これにより、開発速度を落とすことなく、複雑なビジネスロジックを持つシステムを進化させていくことを目指します。
2つ目の課題の説明で述べた通り、ドメインモデルパターンではドメインモデルに重要なビジネスロジックを凝集することが求められます。しかし、明確な指針を設定せずにコードを書いていくとドメインモデルに様々な関心事が紛れ込んでしまい焦点のぼやけたドメインモデルとなり、その真価を発揮できません。以降では発送マイクロサービスで実施した、ドメインモデルがビジネス上の関心事の表現に専念するための工夫を紹介します。
コマンドとクエリの分離
一般的にコマンド(書き込み側)とクエリ(読み取り側)ではモデルに求められる性質が大きく異なります。
- コマンド - ユースケースに依存せず、ドメインモデルの持つビジネスルールで一貫性をもちデータを更新したい。
- クエリ - ユースケースに特化した柔軟なモデルでデータを取得したい。
性質の異なる両者を同じモデルの関心事として混在させると、モデルが複雑化してしまいます。そこで発送マイクロサービスではドメインモデルをシンプルに保つために、下図のようにコマンドとクエリでモデルを分離します。
これにより、単一のモデルの関心事を絞ることができ、最も重要であるコマンドのビジネスルールをそのままコードで表現できます。具体的には、コマンド側ではドメイン駆動設計の集約の単位で更新し、クエリ側ではコマンドモデルで更新したDBのデータに対してクエリモデルを利用して比較的自由に取得します。また、クエリモデルとしてOpenAPI定義をもとにOpenAPI Generatorで自動生成したモデルを利用しています。
さらに、用途に合わせてDBも分離するCQRSパターンを採用することで、柔軟なクエリの実現や処理効率の向上が期待できます。ただし、今回は以下の2つの理由からDBの分離までは行いません。
- コマンドで更新したデータをクエリ側に即時反映したい
- 全体のアーキテクチャ構成をできるだけシンプルに保ちたい
ZOZOの店舗在庫連携サービスではAWSのマネージドサービスを活用してDBの分離まで行っています。発送システムリプレイスにおいても参考になる点が多かったのでぜひご覧下さい。
境界づけられたコンテキストの分離
発送マイクロサービスでは、ドメイン駆動設計の境界づけられたコンテキストという考え方を取り入れて下図のようにドメインモデルをさらに分離します。
具体的には、発送マイクロサービスを「ピッキング」「梱包」という境界づけられたコンテキストで分離します。それにより、コンテキストに依存して異なる意味を持ちうる概念も別々のモデルとして簡潔に表現できます。たとえば、同じ「商品」でもピッキングコンテキストでは「A階の棚BのC段目に保管されている商品」、梱包コンテキストでは「割れ物包装が必要な商品」として別々の道3を選択します。
もちろん、1つのユースケースで複数の境界づけられたコンテキストのデータを更新したい場合もあります。発送マイクロサービスでは境界づけられたコンテキスト間を疎結合にするため、1つのユースケースでは1つのコンテキストのデータのみを更新します。そして、別コンテキストのデータはイベント駆動の結果整合性で更新します。具体的な更新イメージは下図の通りです。
任意のユースケースにおいては(1)のように、1つのコンテキストに対してのみデータ更新します。すると、(2)のように発送DBの特定のテーブルをChange Data Capture(以降、CDCと表記する)するイベントプロデューサーが、変更を検知してブローカーにイベントを投入します。次に、ブローカーに対してポーリングを行う、別のコンテキストのイベントコンシューマーが(3)・(4)のようにデータを更新します。
モジュラーモノリス
境界づけられたコンテキストという考え方は、マイクロサービスの境界を考える上で用いられることも多いです。しかし本リプレイスでは、以下の2点を考慮し、発送機能に関連する複数の境界づけられたコンテキストに関するモジュールが同居するモジュラーモノリスのような形で発送マイクロサービスを構成します。
- 複数のマイクロサービスを作るコストが高い
- 現時点では発送マイクロサービス内のコンテキスト境界の完璧な見極めが難しい
また、モジュラーモノリスとすることで、どうしてもコンテキスト間での結果整合性を許容できないケースではRDBのトランザクションを利用するという選択肢を残すことができます。コンテキスト毎にマイクロサービス化した場合、同様のケースは分散トランザクションの問題となり、sagaパターンなどの実装が必要となります。両者では実装や運用にかかるコストが大きく異なるため、現時点では選択肢を残しておけることが大きなメリットであると感じています。ただしコンテキストをまたいだRDBトランザクションの利用は、将来的なDBの分離の難易度を大きく上げる決断であるため、そのようなケースに直面した場合にはトレードオフを見極めて慎重に決断する予定です。
レイヤードアーキテクチャの導入
これまで説明したように、コマンドとクエリを分離し、さらに境界づけられたコンテキストでモデルを分離することでモデルの関心事を絞ることができます。次に、ビジネスロジックを表現するドメインモデルの息づく場所であるドメイン層が他の層の関心事や技術的な関心事と混同することを避けるためにレイヤードアーキテクチャを導入します。
赤い矢印は依存性逆転の原則(Dependency Inversion Principle)を表します。具体的には、ドメイン層が定義したインタフェースをインフラストラクチャ層で実装します。すると「ドメイン層→インフラストラクチャ層」の依存関係を「インフラストラクチャ層→ドメイン層」に逆転でき、ドメイン層から他の層への依存を無くせます。これにより、UIなどのプレゼンテーション層の関心事や、DBなどのインフラストラクチャ層の関心事に振り回されることなく、開発者はドメイン層でビジネスロジックを表現することに専念できます。
パッケージ構成
これまで紹介したような様々な工夫をした結果、発送マイクロサービスのリポジトリのパッケージ構成は以下のようになっています。
. ├── command/ │ ├── picking/ │ │ ├── application/ │ │ │ └── usecase │ │ ├── domain/ │ │ │ └── model │ │ ├── infrastructure │ │ └── presentation/ │ │ └── openapi/ │ │ └── generated/ │ │ └── model │ └── packing/ │ ├── application/ │ │ └── usecase │ ├── domain/ │ │ └── model │ ├── infrastructure │ └── presentation/ │ └── openapi/ │ └── generated/ │ └── model └── query/ ├── picking/ │ ├── application/ │ │ └── usecase │ ├── infrastructure │ ├── openapi/ │ │ └── generated/ │ │ └── model │ └── presentation └── packing/ ├── application/ │ └── usecase ├── infrastructure ├── openapi/ │ └── generated/ │ └── model └── presentation
発送マイクロサービスのリポジトリでは、Gradleのマルチプロジェクト機能を活用してパッケージ間の依存関係を厳密に制御しており、開発メンバーが増えても依存関係をクリーンに保つことができています。
発送システムリプレイスの進め方
これまで本リプレイスにおける、リプレイス前後のアーキテクチャを詳細に説明してきました。ここでは具体的なリプレイスの進め方を説明します。
これまでのZOZOTOWNのリプレイスでは、ストラングラーパターンが多く採用されています。具体的には、ストラングラーファサードとして自社開発したAPI Gatewayを用いて同一のURLに対するリクエストをレガシーとモダンに振り分けることで段階的なリプレイスを行っています。以下は本リプレイスで行う段階的なリプレイスを示した図です。
本リプレイスでは、ユーザーがアクセスするURLも異なる別のアプリケーションを作り、レガシーとモダンの2つのアプリケーションが並行稼働し同一の業務を双方で実行可能な状態にします。そのうえで、上図のように現場作業者の多大なる協力のもと運用をコントロールすることでストラングラーファサードのような役割を担います。そして、1日の発送業務のうちの1%の作業者はモダンシステムで作業をしてもらい、動作の確認ができたら次の日は5%とする。といったように比率を徐々に変えていくことで段階的にリプレイスを進めます。また、モダンシステムで作業エラーやバグが発生した場合でも、現場作業者の協力のもとレガシーシステムに切り替えての作業の続行が可能な状態としています。
本リプレイスは移行が完全に完了して初めて受けられる恩恵が多く、移行期間は負担が大きいです。そのため、移行の検証期間での多少のバグは許容しつつも、完全移行までの期間を最小化することを目指します。これは本リプレイスの対象が社内向けのアプリケーションであり、社内のユーザーがリプレイスに対して理解し協力が得られているという条件が揃ったからこそ採用できる手法だと考えています。リプレイスを無事に成功させ障害の分離と開発速度の向上を実現することでより、現場やビジネスに貢献できるように開発メンバー一同で真摯に取り組んでいます。
現在は、一点のみのご注文商品を発送する「単数発送」機能を段階的にリプレイスすることを目指して開発に取り組んでいます。続けて、「単数発送」以外の「複数発送」などの残りの発送機能もリプレイスしていく予定です。
まとめ
ZOZOの「発送システム」は、大規模なモノリス構成のシステムの一部であり、「障害リスク」と「開発速度の低下」に課題を抱えています。そこで、これらの課題を解決するため、以下を目的としてリプレイスを進めています。
- マイクロサービス化による障害分離
- 開発速度の向上のためのドメインモデルパターンへの移行
本リプレイスは進行段階であり、これからリリースを控えています。リリース後に実際に得られた成果や発生した問題についても別の記事で改めて紹介する予定です。また、本記事ではCDCやイベント駆動の分散アーキテクチャを実現するための技術選定や実装の詳細について触れることができなかったため、そちらも今後別の記事で紹介しようと考えています。
おわりに
ZOZOではZOZOTOWNやWEARのようなエンドユーザーが直接触れるシステムだけでなく、本記事で紹介したような基幹システムの開発・運用・リプレイスも行っています。基幹システムのリプレイスを進めていく仲間や、既存のシステムの開発・運用を担ってくれる仲間を随時募集しています。また、ZOZOの基幹システムのリプレイスプロジェクトについてはZOZO DEVELOPERS BLOG内のこちらのインタビュー記事でも紹介しています。
ZOZOの基幹システムの開発・リプレイスに興味を持ってくださった方は、以下のリンクからぜひご応募して下さい。