メルカリの取引ドメインにおけるモジュラーモノリス化の取り組み

この記事は「連載:連載:技術基盤強化プロジェクト「RFS」の現在と未来」として書かれたものです。


メルカリのトランザクションチームでバックエンドエンジニアをしている @adachang です。今回は前回の記事に引き続き、メルカリの取引ドメインに属するコードベースのメンテナンス性・拡張性を改善するための取り組みを紹介します。

前回の記事では、この取り組みの背景や課題、ドメイン分析や依存関係をデカップリングするアプローチについて説明しました。その中でメルカリの “取引” を扱う大きなドメインを、商品代金の支払いとそれ以降のプロセスに分割し、前者を「Checkout ドメイン」、後者を 「Transaction ドメイン」と呼ぶことにしました。この記事では、Transactionのドメインを取り上げ、PHPのモノリスをモジュラーモノリスに移行するための現在進行中の取り組みをご紹介します。私たちが行っているモジュール間の境界を定義する方法や、既存の実装を新しい実装に安全に切り替える方法が、運用中のアプリケーションのモジュール化を検討する上での参考になればと思います。

モノリスからモジュラーモノリスへ

Transaction ドメインはメルカリの創業当時から開発・運用され続けてきた PHPの巨大でモノリシックなアプリケーションの中に実装されています。そして私たちは前回の記事で紹介したようなドメイン分析を経て、Transaction ドメインを複数のサブコンポーネントにマッピングしました。


図. Transaction と Checkout のサブコンポーネント

TransactionドメインにはPurchase HistoryやShippingなど6つのサブコンポーネントが、CheckoutドメインにはFee CalculationやPaymentなど4つのサブコンポーネントが存在します。これらのサブコンポーネントに含まれる機能やこれから開発する機能の中には、マイクロサービスアーキテクチャを採用する方が適切なケースもあります。幸いメルカリでは、これまでたくさんのマイクロサービスを立ち上げ、運用してきた実績や仕組み、そしてサポート体制があります。しかしながら、取引に関する多くの既存のワークフローは長大かつ複雑で、マイクロサービスを使って構築し直すことが容易ではありません。例えば、モノリスではリレーショナルデータベースをアプリケーション全体で共有し、トランザクションを利用することでデータの整合性を保てます。しかし、マイクロサービスアーキテクチャでは原則的にサービスごとに独立したデータベースを所有することになるため、サービス間でデータベースを直接参照することはできません。リレーショナルデータベースのトランザクションの代わりに、別の整合性モデルに置き換えることを検討する必要があります。

またモジュラーモノリスに移行した後でも、各サブコンポーネントをマイクロサービスに切り出していくことは再検討できます。サブコンポーネント化した時点でインタフェースの設計だけは終わっているため、マイクロサービスに切り出すことはモノリスから直接マイクロサービスに移行するよりは成功する確率が高いと考え、私たちはモジュラーモノリスを選びました。

サブコンポーネントのAPI設計

全体設計

Transaction ドメインのサブコンポーネント間のやり取りや他ドメインとの接続はAPIを介して行うことにしました。既存の処理を新しく実装するAPIに切り替えていくことで、サブコンポーネント間の境界を形成します。このAPIはPHPのメソッドとして実装されていて、データベースのトランザクションなどをコンテキスト(引数)として引き渡すことができます。

最初のステップとして Transaction ドメインの各サブコンポーネントとそのAPIを配置するための名前空間を設計しました。さらに私たちはAPIのうち書き込み処理など副作用が発生するものをCommand、副作用のないものをQueryと呼び、APIの名前空間もそれに合わせて分けています。例えば、配送に関する書き込み系のAPIは「\Mercari\Transaction\Shipping\Api\Command」に配置します。


図. サブコンポーネントのAPIの名前空間

ちなみにAPIを使う以外の方法で、他のサブコンポーネントとやりとり出来ないようにしています。これは先日の記事で紹介したBoundary Violation Detectionツールを利用していて、CIのタイミングでチェックを行なっています。

サブコンポーネントごとのAPI設計

APIはサブコンポーネント間の境界を定義するものとなるため、各サブコンポーネントの責務を確認しながら設計することが重要です。まずチームでサブコンポーネントの責務に関して共通認識を持つため、前回の記事で紹介した輪読会の録画を再確認したり、追加のコードリーディングを行いました。例えばTransactionには "Shipping" というサブコンポーネントがあります。これは購入された商品の配送を担当するロジスティクスのドメインとの橋渡し役となるサブコンポーネントです。このドメイン自体は別のチームが担当していますが、とても複雑かつ深い知識が求められます。そこで私たちのチームでも、ある程度その詳細を知っておく方が良いと考え、配送業者ごとのデータ連携の方法やタイミングについての理解を深めるため、イベントストーミングのワークショップを行いました。イベントストーミングとは、あるドメインで発生する出来事を洗い出し、時系列に並び替えながらドメイン知識を整理していくためのワークショップです。

このようにして深めた知識をベースにAPIを洗い出し、APIごとに仕様をまとめたDesign Docを書き起こしました。このDesign Docには下記のような内容が記述されています。

  • 名前
  • 概要 / 役割
  • ユースケース
  • 呼び出し方
  • 前提条件 / 事後条件
  • シーケンス図
     - ユースケースごとに As-Is / To-Be の図を用意


図. シーケンス図の例

サブコンポーネントのAPI実装

新旧実装の差分チェック

設計が終わったAPIを実装するとき、既存の実装にユニットテストや結合テストが十分に充実していないことがあります。私たちはテストケースを追加したり、テストカバレッジを逐一確認しながら新しく用意したAPIに少しずつ既存のロジックを移植しました。また取引に関する不具合や障害はビジネス的な損失が大きくなるため、回帰テストも慎重に行なっていました。特に取引画面を表示するために使用されるAPIは慎重に変更を加えていく必要がありました。

ここで使用される API はロジックが複雑なうえ、レスポンスに含めるデータが多かったのですが、幸いなことに読み取りだけの副作用のないAPIでした。そのため、移植する際には新旧の実装を両方同時に動かし、結果に差分がないかチェックできました。これを開発環境で行うだけでなく、実際に本番環境にリリースして行いました。もし差分が出てきた場合は、JSONのキー名だけをログに残し、DataDog Logsで確認できるようにしていました。


図. 差分チェックの処理フローのイメージ

この方法は新旧の両方のロジックを動かすためレスポンスタイムが遅くなりますが、あらかじめ決めていた切り戻し判断の閾値よりも遅くなることはありませんでした。この差分チェックを実施した結果として、新しい実装の不具合だけでなく、既存実装の不具合まで発見し、修正できました。

おわりに

この記事では、私たちが PHP のモノリスの中に取引ドメインをモジュール化していく取り組みについて紹介しました。これまでにいくつかのサブコンポーネントを設計し、そのうちの1つを既に実装できました。これまでの間に、私たちのチームは発足当初(2021年10月)と比べてとても詳しくなり、またモジュラー化のアプローチが大きく間違っていないことを確認できました。加えて、この取り組み自体の重要性、必要性も再認識しました。まだまだ道半ばですが、モジュール化をさらに前進させ、より丈夫で柔軟なシステムを目指します。

この記事で紹介した内容は、「Robust Foundation for Speed」というメルカリのビジネス共通基盤の技術的な課題を解決していく全社横断的なプロジェクトにおける、トランザクションチームの取り組みです。そしてメルカリではこのRFSという大きなプロジェクトに一緒に立ち向かっていく仲間を募集しています!ご興味が湧いた方は、以下のリンクをご覧ください。
Software Engineer, Backend Foundation (PHP/MySQL) – Mercari

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加