RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

サービス分割に備えたモノリス(モジュラーモノリスとかアグリゲートとか)

f:id:moomoo-ya:20201022123603j:plain

こんにちは、株式会社ラクスで先行技術検証や、ビジネス部門に技術情報を提供する取り組みを行っている技術推進課に所属している鈴木(@moomooya)です。

ラクスの開発部ではこれまで社内で利用していなかった技術要素を自社の開発に適合するか検証し、ビジネス要求に対して迅速に応えられるようにそなえる 「発の来にせん手をうつプロジェクト(通称:かみせんプロジェクト)」 改め 「技術推進プロジェクト」 というプロジェクトがあります。

2020年度上期に「サービス分割を見越したドメイン層設計」について取り組んだので、概要を紹介したいと思います。

今までの記事はかみせんカテゴリからどうぞ。 tech-blog.rakus.co.jp


今回の目標

2017年に社内でマイクロサービスアーキテクチャについて検証した際には「サービス立ち上げ時にはマイクロサービスアーキテクチャは選択するべきではない」という結果が導かれました。

この判断結果は2020年時点でも変わらないものだと感じています。とはいえサービスが大きくなったときに「開発速度を維持するためサービスの分割を検討することになる」というのも正だと思います1

このとき必要に応じて「サービスを分割することが出来る」設計になっていなければ、年単位で移行したという事例が散見されるように、モノリスからマイクロサービスへの移行は極めて難しい判断になるでしょう。

サービス分割の肝となる部分は「ビジネスロジックドメイン層が分割可能かどうか」という点だと考えています。なので今回のテーマは「サービス分割を見越したドメイン層設計」と設定し調査検証を行っていきました。

余談:「開発速度を維持するために」サービス分割を検討するというのはなぜ

プログラマーが扱える適切なソースコード量(プログラムの規模)には上限があります。プログラミング言語等にも大きく依存しますが、1万行であるとか、1画面に収まる量であるとか、いろいろな観点で語られますが人間に認知限界が存在する限りどこかに限界があります。

そして(特にエンタープライズ領域の)プログラムのソースコードは数十万をゆうに超え、数百万を超える場合もあります。明らかに人間が扱える上限を超えています。

上限を超えたプログラムがどうなるか、というと「ロジックのミス」「想定しないバグ」「修正漏れ」が増加することは想像に難くないと思います。そのため適切なボリュームになるようプログラムを分割する必要があると考えます。

なぜ最初からサービス分割をしないのか

まずは「サービス立ち上げ時にはマイクロサービスアーキテクチャを選択しない」のはなぜなのかという話を振り返ります。マイクロサービスアーキテクチャを採用する、ということがどういうことなのか噛み砕くと以下のようになると思います。

  • 複数のWebサービスを開発・運用し、連携させる
  • (理想としては)Webサービスごとにチームを構成し、互いに内部の構造は依存しない

複数のWebサービスとして作ることでマイクロサービスのメリットとされる技術異質性が確保できたり、物理的にロジックの汚染を防ぐことは出来ますが、それらのメリットを覆すだけの運用コストが発生してしまいます。Kubernetesなどのコンテナオーケストレーションツールを高レベルで使いこなすだけの運用スキルがチーム内にあれば解決できるかもしれませんが、結局「高レベルのスキル=高コスト」なので立ち上げ時には向きません。

またWebサービスごとにチームを構成することで迅速な開発を行っていけるのもメリットですが、物理的に別サービスになっていることと、チームが分かれていることでサービス境界の定義の見直しがしにくくなります。サービス境界の定義が不適切だとマイクロサービス化して得られるはずのサービス感の独立性が失われるため、いわゆる「分散モノリス」というアンチパターンに陥ってしまいます。サービス立ち上げ時は一般的にドメイン知識も乏しく適切なサービス境界の定義は難しいため選択しにくい選択肢になります。

またサービス立ち上げ時は「扱えない規模のプログラム規模拡大」も「大きな負荷」もまだ発生していません。利益が発生していない時点で、発生していない問題にコストを投入するのは悪手と言えるでしょう。

モノリスで困ること

マイクロサービスアーキテクチャが流行し始めた2016年頃から「モノリシックアーキテクチャは悪」という主張が散見されました。これには誤りが含まれています。

まずモノリシック(1枚岩)アーキテクチャの特徴としてデプロイメントラインが1つであることが挙げられます。これは明確にマイクロサービスアーキテクチャに対してメリットです。複数のデプロイメントラインが存在するとその分デプロイコストはかさみます。

ではモノリシックアーキテクチャは何が「悪」なのか、というと体験している人も多いと思いますが、モジュール間の参照が複雑に交差することによるスパゲッティ化(Big ball of mud = 大きな泥団子、とも)が起こりやすいことがモノリシックアーキテクチャの真の問題です。

マイクロサービスアーキテクチャは構造的にスパゲッティ化が起こりにくいアーキテクチャです。しかし立ち上げ時にはコスト的に見合いません。であれば、スパゲッティ化しにくい方法(適切なモジュール分割)を適用したモノリスが実現できれば立ち上げ時の有力な候補となりえると考えました。

モジュラーモノリスとは

適切なモジュール分割を行ったモノリスを調べていくとそれが「モジュラーモノリス」と呼ばれていることを知りました。端的に言うと理想的なモジュール分割が行われた「きれいなモノリス」です。

1つのプログラム内で独立性の高いモジュールとして分割されることで、規模が大きくなったときのサービス分割もやりやすそうに見えました。

またモジュール分割の境界線(サービス境界)を見直したくなった場合でも1つのプログラムの中の話なので、マイクロサービスに比べて見直しやすいとも感じます。

当然モノリスなのでデプロイメントラインも1つです。

ビジネスロジックの設計方法

実際のビジネスロジック層の設計はDDDで出てくるアグリゲートパターン2が適用できそうです。 加えてファクトリパターン3も適用候補として準備しておくと良いと思います。ちなみに今回の検証ではファクトリパターンを候補にしていなかったため実装の段階で課題に直面することになりました。

詳細はDDD本を参照していただきたいですが、アグリゲートパターンは

  • アグリゲート(集約)
  • アグリゲーションルート
    • 外部のアグリゲートとコミュニケーション可能な唯一のオブジェクト(=エンティティ)

という概念に基づいていて、アグリゲートの生成もアグリゲーションルートのインタフェース(コンストラクタとか)により行います。

ただし、アグリゲートがある程度複雑になってくると生成ロジックも分離したほうがよくなってきます。生成ロジックの分離とカプセル化を行ったものをファクトリとして扱うのがファクトリパターンです。 ファクトリパターンと言っても実装方法(実装場所)はさまざまなようです。

  • アグリゲートルート
    • 集約ルート内にファクトリメソッドとして実装
      • もちろんコンストラクタでもよい
    • アグリゲートパターンの部分で触れたケース
  • (アグリゲート内の)別のオブジェクト
    • 生成に関連がつよいオブジェクトにファクトリメソッドをもたせる
  • (アグリゲート外の)別のオブジェクト(もしくは別のサービス)
    • 生成ロジック的にアグリゲート内に収めるべきではない場合はアグリゲートの外におくことをためらわない

また、アグリゲートパターンに代わる別の設計手法としてMichael Nygard氏により「ライフサイクルによる分割(Services by Lifecycle)」も提唱されていますが、今回は未検証です。

実装と機能修正の範囲限定

結局実装してみないとわからないので試しに実装してみました。お題は弊社「楽楽明細」のような帳票発行サービスをイメージして作ってみました。

f:id:moomoo-ya:20201021222552p:plain

アプリ(サービスレイヤ)の部分はウェブアプリケーションフレームワークのルーティング処理の実装箇所だと思ってください。 上記のような設計をして、実際に実装してみたのですが先述の通りファクトリパターンを把握していなかったため「アプリ(サービスレイヤ)」の部分に初期化処理を実装してしまいました。 それの何が悪かったかというと機能追加のアップデートを実装したときに顕在化しました。

f:id:moomoo-ya:20201021222556p:plain

請求書のテンプレートを送付方法に応じて使い分ける事ができる機能を追加しました。送付方法は「メールにPDF添付」「印刷して郵送」を想定しています。点線部分は追加、修正が入ったモジュールになります。

見ての通り「アプリ(サービスレイヤ)」に修正が入ってしまってます。今回の前提(モジュラーモノリス)であれば実質的な問題はほとんどありませんが、サービス分割後を想定するとサービス全体が停止することになってしまいます。ファクトリメソッドとしてアグリゲートルート内もしくはアグリゲート内にファクトリオブジェクトとして持っていれば「アプリ(サービスレイヤ)」には影響がなく、修正が入っていない受領者アグリゲート、売上レコードアグリゲートに関する機能は動かし続けることが可能になります。

データストアについて

今回は利用していませんでしたが、データストア、主にRDBについても分けておいたほうが良いです。

物理的にDBサーバーを別で用意するとやはり手間がかかるのでMySQLでいうdatabase(PostgreSQLだとschema)の単位で分けるとか、もっと簡易にテーブル名で分けておくとかやり方はいろいろあると思いますが、分けておくと良いでしょう。

DBの分け方については参考文献の"Monolith to Microservices"のchapter.4が参考になると思います。

結論

実際に設計、実装してみた感じでもモジュール化(モジュラーモノリス)を目指すことでサービス分割を低コストで実現することは可能そうに思えました。ただし、昔ながらのモノリスより手間が増えることは間違いありません。手間が増える量がマイクロサービスに比べてはるかに少なく現実的な選択肢になるところまで落ち着く可能性がある、というのが適切な評価だと思います。

なので結局はバランスの問題ですが、ある程度サービス拡大が見込める場合には先行投資的にモジュラーモノリスで構成し、賭けの要素が強いサービスの場合はモノリスでコスト最小で昔ながらのモノリスで構成する、といった選択になるのかな、と感じました。

今後の課題

アグリゲートパターンはやはりサービス境界(アグリゲートバウンダリ、アグリゲート境界)の定義にドメイン知識を必要とするため難しい、というのは解決されていません。モノリスであることで取り返しはつくようになるもののドメイン知識が乏しい立ち上がり時に設計することは難易度が高いと思います。

「ライフサイクルによる分割(Services by Lifecycle)」ではサービスの利用フェイズをもとにサービス境界を定義していくという発想によりサービス境界の定義を比較的容易に行うことが出来る可能性を感じるため、ライフサイクルによる分割について今後検証していきたいと考えています。

参考文献

書籍

論文

ブログ記事


  1. 他にも大きくなってきた負荷を適切に分散したい、なども理由になると思います。

  2. エリック・エヴァンス『エリック・エヴァンスのドメイン駆動設計』 p.123

  3. エリック・エヴァンス『エリック・エヴァンスのドメイン駆動設計』 p.134

Copyright © RAKUS Co., Ltd. All rights reserved.