持続可能なシステムを目指してプロダクトをリアーキテクトしました〜ドメインモデリング導入編〜

サムネイル画像

はじめに

こんにちは。プラットフォーム開発本部 カスタマーサポートプラットフォームグループ(CSPグループ)でバックエンド開発を担当している坂本です。

CSPグループでは「カスタマーサポートを行う人たちと連携してDMMユーザーの顧客満足度をあげること」を目指し、DMMヘルプセンターをはじめとするカスタマーサポート業務に関するプロダクトと機能を開発しています。フロントエンド/バックエンド双方の機能改善によるユーザビリティ向上やAIを活用した業務改善などを進めてきましたが、プロダクトが大きくなるにつれてバックエンドAPIのコードが複雑になり機能追加や改修時にバグが頻発するようになりました。

「改修のたびにプロダクトが壊れる…このままでは安心して開発できない」

こうした理由から、より安全かつ迅速に価値を提供していくためにバックエンドAPIの設計を見直し、プロダクトをリアーキテクトしました。その過程で得られた知見と学び、現場で実践している設計手法(ドメインモデリング)と具体的な実装方法について、2回に分けて共有します。この記事では戦略的DDDと呼ばれている部分、ドメインモデリングの導入と実践についてお話しします。

リアーキテクトとドメインモデリングの導入に至った背景

今回リアーキテクトしたプロダクト(バックエンドAPI)はもともと下記のようなMVCパターンを採用していました(APIなのでViewに相当するレイヤーはありません)。

               クライアント
               ↓↑
response層 ←→  handler層...クライアントからのリクエストを受け取り、レスポンスを返す。
               ↓↑
               service層...受け取ったリクエストに応じて処理を行うビジネスロジックを置く。
               ↓↑
               dao層...DBや外部サービスとやりとりし、データの永続化を行うための処理を置く。
               ↓↑
               DB/AWS/外部APIなど

モデルに該当する部分をservice層とdao層に、コントローラーに該当する部分をhandler層とresponse層にそれぞれ分離する構成になっています。

当時の開発メンバーはMVCベースのアプリケーション開発経験があったため、初期の頃はスムーズに開発を進めていくことができました。しかしプロダクトが成長するにつれてコードが複雑になり、変更を加えるたびにバグが発生するようになりました。

発生した不具合の多くは次のような複雑な処理で発生していました。

// UpdateHogehoge 長くて可読性の低いメソッド
func (a *hogehoge) UpdateHogehoge(p *Params) (*Mogumogu, error) {

    // パターンA
    if p.Hoge != nil {

        // AのなかのパターンB
        if B {
            // ...省略

            // A-BのなかのパターンC
            if C {
                // ...省略
            }

            // A-BのなかのパターンD
            if D {
                // ...省略
            }

            // 処理が続く

            // A-BのなかのパターンE
            if E {
                // その他、延々と続く悲観的排他処理
                // ...省略
            }
        }
    }

    return mogumogu, nil
}

上記はservice層に実装されているメソッドの一つです。ビジネスロジックの大部分がservice層に集中しており、複雑なユースケースを有する一部のAPIでservice層のメソッドが肥大化しました。その結果、開発効率と可読性が低下し、さらにテストコードの実装負荷が上がりました。

そのほか、私を含む当時の開発メンバーの設計力不足もあり、枚挙にいとまがないほど大量の改修困難なコードが実装されてしまいました。

例:

  • ドメインロジックとインフラ部分のデータクラスが層ごとに共有されており修正が困難(密結合)
  • 構造体のカプセル化が不十分で、バリデーションなどの関連する処理がバラバラに実装されており改修が困難(低凝集)
  • 同じくカプセル化が不十分で、構造体の各フィールドの読み書きが層をまたいで可能な状態になっており、データが壊れやすい
  • 関心の分離ができておらず、様々な目的で利用される神クラス的な構造体が存在している(密結合)
  • etc...

これらの問題を根本解決するにはデザインパターンを再考する必要があると判断し、リアーキテクトを決心しました。また、service層の肥大化はドメインの分析不足が原因の一つとして考えられたため、リアーキテクトのタイミングでDDD(ドメイン駆動設計)を導入しました。

リアーキテクトの体制

リアーキテクトを進めるにあたりいくつかの方針を決めました。

  • まずは一人が実装を進め、実装が固まってきたら他のメンバーにコードベースを共有して複数人でリアーキテクトを進めていく(支援内容は主にコードのレビュー。実装に困った際やレビューの指摘などで理解できない時などにペアプロで相談)
  • CSPグループではDDDについて詳しいメンバーがいないため、経験と実績のあるチームの支援を受けながら導入を進める(DMMでは様々な領域のスペシャリストが揃っており、開発を進めるうえで他部署の協力や支援を受けたり学んだりできます)

リアーキテクトの過程で発生した課題

ある程度デザインパターンと実装方針が固まってきたところでコードベースを他メンバーにも共有し、本格的にリアーキテクトを進めていく過程で問題が発生しました。domain層の実装が上手くいかず、何度も手戻りが発生してしまったのです。

テンプレートに当てはめてそれらしい実装を真似するだけでは上手くいかず、DDDで実装を進めていくためにはドメインモデルをしっかりと作り込む必要があり、ドメインモデルの設計にはドメインモデリングが重要であることに気づきました。そこでレクチャーを受けながら基本的な設計手法とドメインモデリングを学ぶことになります。学習の様子と成果についてはこちらの記事で紹介されていますので、ぜひご覧ください。

以下、学んだ内容を元に私たちがどのようにドメインモデリングしているのか解説していきます。

ドメインモデリングの導入と実践

ドメインモデリング参加者

ドメインモデリングは「ドメインエキスパートやプロダクトの利用者なども交えて話し合い、ドメイン知識をコードに落とし込む」ための手段として用いられます。

しかし現実的にドメインエキスパートやプロダクトの利用者と話し合う時間が取れないなどの理由から、CSPグループではバックエンドのメンバーが中心となってドメインモデリングを行っています。

バックエンドのメンバーはドメインエキスパートではありませんが、利用者からの要望と実現したい内容をベースにPRDとDesign Docを作成し、ドメイン知識を理解するための情報を収集した状態でドメインモデリングを行っています。

ドメインモデリングの流れ

私たちはドメインモデリングを以下の流れで行っています。

  1. 要件の洗い出しとドメイン知識の整理
  2. ユースケースの洗い出し
  3. イベントストーミング
  4. ドメインモデルの抽出
  5. 制約の洗い出し
  6. 実装メモの作成
  7. ドメインモデリングの見直し

今回はCSPグループで管理している告知機能を題材に、順に解説します。

告知機能について

告知機能はDMMのサービスのメンテナンスや障害などの関連情報をユーザーにお知らせするための機能です。エンドユーザー側では告知の一覧を表示したり、単体の告知の詳細を確認したりできます。また、告知の作成や編集、削除などの操作は管理画面から行うことができます。

要件の洗い出しとドメイン知識の整理

ドメインモデリングはドメインエキスパートやプロダクトの利用者と話し合いながら行うのが理想ですが、現実的にはバックエンドメンバーが中心となっておこなっています。

そのため、ドメイン知識を補うためにPRDとDesign Docまたはそれに準じた資料を用意し、必要な情報を収集した状態でドメインモデリングを行っています。

  • PRD(Product Requirements Document)...目的や実現したい内容をまとめたドキュメント
  • Design Doc...技術的にどう実現するかをまとめたドキュメント

今回のリアーキテクトのように修正対象のコードがすでに存在する場合は既存の資料とコードを元にドメイン知識を収集できますし、実装の規模によっては詳細なPRD/Design Docが必要ない場合もあります。

ドメインモデリングを進める過程で目的があやふやになったり、要件がわからなくなった場合はドメインエキスパートに確認してフィードバックしてもらいます。

ユースケースの洗い出し

達成したい目的と要件、達成方法について整理したのち、それらの情報を元にユースーケースを洗い出します。

ユースケースは「誰が」「何を」行うかを整理したものです。告知機能のユースケースは以下のように整理しました。

洗い出したユースケース

「ユーザーが告知を閲覧する」という操作もユースケースの一つですが、ドメインモデリングの際には参照系の洗い出しを省略しています。というのも、参照系は整合性が保証されたデータを取得する前提のため、制約やバリデーションは基本的に必要がないためです。 一方で更新系はデータの永続化を行うため、データの整合性を保つための制約やバリデーションが必要です。

例外として、取得したデータの加工やフィルタリング等の処理を行う場合は参照系であっても複雑になるため、更新系のようにドメインモデリングすることがあります。

イベントストーミング

ユースケースを洗い出したら、次はイベントストーミングを行います。

イベントストーミングはモデリングの手法の一つです。「誰」が「xxをする」という操作の結果として「yyが起こる」というように、ユースケースごとに一連のイベントを可視化します。この作業を通して一連の出来事がどのように関連しているのか俯瞰して確認でき、後工程のドメインモデルと制約を洗い出す助けになります。

私のチームではイベントストーミングを行う際、下記ルールを設けています。

  • イベントストーミングに不慣れなメンバーの場合はチーム全員で作業する
  • 個人で行う場合は作業後にチーム全員でレビューしてフィードバックする(私たちのチームではレビューが1回で終わることは稀で、全員が納得するまで繰り返しブラッシュアップしています)
  • draw.ioなど共同で作業可能なツールを使う

以下、イベントストーミングの流れを示します。

イベントを洗い出す

ユースケースを元にイベントを洗い出し、付箋に書き出していきます。

イベントを洗い出した様子

イベントを時系列に並べる

イベントを時系列に並べます。

例えば、リソースを更新する場合は更新対象のリソースが存在していないと更新できません。そのため時系列的にリソースの作成が先で、更新のイベントは後になります。

このようにイベントの前後関係を考慮して時系列に並べていきます。

イベントを時系列に並べた様子

コマンドを洗い出す

コマンドはイベントを発生させるための操作です。「告知を作成した」というイベントを発生させるには、例えば画面上で「告知を作成する」というボタンを押す作業がコマンドに該当します。

コマンドもイベントと同じ時系列に整理します。コマンドが実行された結果としてイベントが発生するので、コマンドはイベントの前に配置します。

コマンドを洗い出した様子

アクターを洗い出す

アクターはコマンドを実行する人です。今回のようにアクターが一人しかいない場合は省略することもあります。

リードモデルを洗い出す

リードモデルはコマンドを実行するために必要な情報です。「告知を更新する」というコマンドを実行するためには、更新対象の告知のIDや更新内容などの情報を事前に取得しておく必要があり、それがリードモデルにあたります。

リードモデルが不要なパターンも存在します。例えば「告知を作成する」というコマンドの場合はリードモデルがなくても実行できるため、リードモデルは省略します。

最終的に下図のようなイベントストーミング図が完成しました。

リードモデルを洗い出した様子

ドメインモデルの抽出

イベントストーミングを元に、アクターによる操作(コマンド)によってどのようなデータが更新されるのかを見極め、ドメインモデルを抽出します。

下図は「告知記事を作成する」イベントに対して抽出したドメインモデルです。

ドメインモデル

AnnouncementToCreateモデル(Aggregate)

「告知記事を作成する」コマンドでは集約モデルである「告知」のデータが作成されます。なのでデータを作成するためのドメインモデルが一つ必要と判断し、AnnouncementToCreateという命名で用意しました。

DraftArticleToCreateモデル(Aggregate)

告知は「告知のタイトル」や「告知の内容」などの情報を持つことになります。AnnouncementToCreateモデルに「title」や「content」という命名のフィールドを持たせても良いかもしれませんが、今回はそうしませんでした。

なぜなら、告知は作成された時点では管理画面でのみ参照でき、ユーザー側から閲覧できない状態にしておきたいからです。そのような「下書き状態」の告知を表現するために、DraftArticleという概念(ユビキタス言語に近い)を考えました。

そして「告知記事を作成する」コマンドにより「告知」と「下書き状態の告知記事」が作成されることになるため、DraftArticleToCreateというドメインモデルを用意しました。

AnnouncementTypeモデル(Entity)

告知は「メンテナンス」や「障害」などの種類を持つことになります。これらの種類はドメインモデルとして表現する必要があると判断し、AnnouncementTypeというドメインモデルを用意しました。

制約の洗い出し

主体となるドメインモデルが決まったら、次は制約を洗い出します。

私たちのチームでは「データ破壊駆動」と呼ばれる考え方を元に、どうすればデータが壊れるかを考え、結果的にデータが壊れないように制約を洗い出すようにしています。

例えば、告知の作成者を表すCreatedByというフィールドには文字数制限があり、1〜255文字の範囲で指定する必要があります。これを考慮せずに範囲外の値を指定してしまうと255文字を超える文字列がトリミングされてデータベースに保存されてしまうなどの問題が発生します。そのような破損したデータが作られないよう、制約はバリデーション処理を実装するための情報となります。

制約を洗い出す過程でドメインモデルに修正が入ることもあります。上記のCreatedByフィールドの例で言えば、CreatedByをValueObjectとして切り出すことで凝集性が高まり、より堅牢な設計になると考えられます。

そのほか、AnnouncementTypeは「メンテナンス」「障害」「お知らせ」の3種類の値を持つことが決まっているため、Enumとして定義することにしました。Enum以外の値が指定された場合はエラーを返す制約となります。

最終的に洗い出したドメインモデル

上記はほんの一例ですが慣れるまでは根気のいる作業になります。付け焼き刃では難しく、私たちのチームではオブジェクト指向やカプセル化、密結合の回避、凝集性の向上、単一責務の原則といった基本的な設計パターンを事前に学習し、初めてドメインモデリングの本質に触れることができました(参考記事)。基本的なソフトウェア設計手法に関する知識が非常に重要であることを実感しました。

実装メモの作成

ドメインモデルと制約が決まったら、実装メモを作成しています。実装メモはドメインモデルの設計や制約を元に、どのような流れで実装するかを簡単にまとめたものです。

具体的な実装方法についてこの段階でシミュレートしておくことでドメインモデルや制約の過不足に気づくことができ、実装時の手戻りを減らすことができます。レビューワがドメインモデルと制約を理解するための取っ掛かりにもなります。

ドメインモデリングの見直し

ドメインモデリングは一回で終わりません、仕様の変更や実装の途中で「制約を追加した方がいいかも」「こういうモデルが必要かも」と気付くことがあります。その場合はイベントストーミング〜制約の見直しの工程に戻り修正します。

ドメインモデリングのフローチャート

見直し後、「告知記事を作成する」イベントのドメインモデルは以下のように修正されました。

見直し後のドメインモデル

リアーキテクトとドメインモデリングの導入を進めた結果

冒頭でお伝えした課題の多くを解決でき、非常に開発しやすいプロダクトに生まれ変わりました。

  • 設計面では
    • バリデーション処理と関連するロジックが集約され適切にカプセル化されたことで壊れにくい構造になった。
    • これまでのようにドメインロジックとインフラ部分のデータクラスが層ごとに共有されている状態が解消され、密結合が回避された。
    • ユースケースの作成とドメインモデリングによって関心の分離ができ、メソッドの肥大化を回避できるようになった。
  • 開発生産性の面では
    • 各層ごとに実装が細分化され、小さな粒度でプルリクエストを作成できるようになった。
    • メソッドの実装も小さくまとまっているためテストコードの実装が容易になった。
    • DDD関連の資料を用意することで、データ書き込み更新の安全性と保守性が高まった。

これまでは設計力が弱く、実装しつつ考えるというスタイルで開発するケースもあってレビューの指摘や手戻りが多く発生していました。ドメインモデリングをしてから実装に着手することでスムーズに開発を進められるようになりました。また、「なぜこの書き方が良いのか(悪いのか)」を判断できるようになり、明確な意図を持ってコードを実装するようになったと感じます。

ドメインモデリングと制約を洗い出す工程においては土台となる基本的なソフトウェア設計を必然的に理解しなければならず、地道な学習が必要でした。しかし結果としてチーム全体の基礎的な設計力が向上しました。設計力が向上したことで、チーム内でのプルリクのレビューやフィードバックの質も向上し、変更容易性の高いプロダクトを目指すことができるようになりました。

下図はリアーキテクト前後の開発生産性に関するスコアの比較です(Findy Team+で計上しています)。このスコア比較から、リアーキテクト前後で開発全体のスループット向上とサイクルタイムの短縮を実現できたことがわかります。

前期間(リアーキテクト前): 2024年5月22 - 2024年11月21日。 現期間(リアーキテクト後): 2024年11月22 - 2025年5月21日。

ドメインモデリングを行う前後の開発生産性の比較

  • 全体のスループット向上
    • プルリク作成数 が 383 件→415 件(約8%増)
    • マージ済みプルリク数 も 360 件→398 件(約11%増)
      • → 開発チーム全体のプルリク出し・マージ数が増え、スループットが向上している。
  • サイクルタイムの改善
    • Open → Merge:22.0h → 11.4h(約93%短縮)
    • Commit → Open:10.5h → 6.0h(約43%短縮)
    • Open → Review:4.9h → 2.8h(約43%短縮)
    • Review → Approve:10.1h → 5.2h(約49%短縮)
      • → ほとんどのフェーズで平均所要時間が大幅に減少し、開発→デプロイまでの流れが速くなっている。

最後に

CSPグループのリアーキテクト活動と実践しているドメインモデリング手法についてまとめました。

ドメインモデリングに限らず、何か新しいことに挑戦する際は難しく感じることがあるかもしれません。そのため私はすぐに解決できる手段を欲してしまいがちなのですが、一朝一夕で身につくものではないことの方が多いのだろうと思います。一人の力では限界があり、教えをこうことも非常に重要だと感じました。

今回のリアーキテクト活動が軌道に乗ったのも、他部署のエンジニアのレクチャーや支援、リアーキテクト活動に対するマネージャーの理解、チームメンバーの積極的な参加など、周囲の協力があってこそ実現できたことだと感じています。このような恵まれた環境で活動できることに感謝しつつ、第1弾の記事を締めくくります。

第2弾の記事ではチームのエースエンジニアから、実装方法についてお伝えしますのでぜひご覧ください。

宣伝

私たちCSPグループではDDDをはじめ新たな設計手法の導入、AIを活用した開発と業務改善などを行い、プロダクトの開発に取り組んでいます。

専門性の高いメンバー(時には他部署のエンジニアやマネージャーとも)協力しながら、自らのアイデアを実装し、ユーザー満足度向上を追求できる環境です。

チームとプロダクトの成長を支える一員として一緒に活動していきませんか?

少しでも興味がある方はぜひ下記採用ページをご確認ください。

dmm-corp.com