本文へ移動

ホーム / Web / 第5部 品質と運用

第5部 品質と運用・第16章

設計(アーキテクチャ)

★★☆ 重要5トピック・面接 3問

長く変更し続けられるコードとシステムの分け方を押さえる。

ひとことで

最初はモジュールに分けたモノリスで始め、チームや負荷の都合で必要になったら分ける。

1つのアプリか、ネットワークでつながる複数のサービスかモジュラーモノリスマイクロサービス1つのアプリ(デプロイは1回)注文在庫決済会員DB(1つ)注文専用の DB在庫専用の DB決済専用の DB会員専用の DBサービスごとに CI/CD・監視・担当が要る
モノリスとマイクロサービスの形

モノリスは、アプリ全体を1つのコードベース・1つのデプロイで動かす形、マイクロサービスは、機能ごとに小さなサービスに分け、それぞれを別にデプロイし、ネットワーク越しに呼び合う形です。

項目モノリスマイクロサービス
開発の始めやすさ簡単。1つ動かせば全部動く準備が多い(通信・認証・デプロイの仕組み)
デプロイ全体を一度に。チームが増えるとぶつかるサービスごとに独立してできる
スケール全体をまとめて増やす負荷の高い所だけ増やせる
データの整合性1つの DB のトランザクションで守れるサービスをまたぐと難しい(Saga・イベント)
障害1か所のバグで全体が落ちることがある一部だけ落ちる。ただし呼び出しの連鎖で広がることも
調べやすさ1つのログ・スタックトレース分散トレースがないと追えない
向いている組織少人数〜数チーム多数のチームが独立して動く大きな組織

面接ではこう答える

モノリスはアプリ全体を1つのコードベースと1つのデプロイで動かす形、マイクロサービスは機能ごとに小さなサービスに分けて、ネットワーク越しに呼び合う形です。モノリスは始めやすく、1つの DB のトランザクションで整合性を守れます。マイクロサービスはチームごとに独立してデプロイでき、負荷の高い所だけ増やせますが、運用の負担と整合性の難しさが増えます。多数のチームが独立して動く大きな組織に向いています。

ひとことで

1つのデプロイのまま、中を業務の区切りでモジュールに分けたモノリス。分ける理由がはっきりした所から切り出す。

デプロイは1つ。中は業務ごとのモジュールに分ける1つのアプリ(1つのデプロイ)注文モジュールorders の表在庫モジュールstocks の表会員モジュールusers の表モジュールの間は、公開した関数だけで呼ぶ(他の表を直接読まない)
モジュラーモノリスの形

分けたときに増える運用の負担

  • サービスごとの CI/CD・監視・アラート・ログ・ダッシュボード・担当者。
  • ネットワーク越しの呼び出しの失敗への備え(タイムアウト・リトライ・サーキットブレーカー)と、遅延の積み重なり。
  • API の互換性の管理(片方だけ先にデプロイしても壊れないように)。
  • サービス間の認証、分散トレース、ローカルでの開発環境の再現。
  • データが分かれるので、集計や JOIN ができず、整合性を取る仕組みが要る。

多くの場合は、中をモジュールにきちんと分けたモジュラーモノリスで始め、チームの独立性や特定の機能の負荷など、分ける理由がはっきりした所から切り出すのが現実的です。

モジュラーモノリスでは、モジュールの間は公開した関数(インターフェース)だけで呼び、他のモジュールの表を直接読まない、というルールを決めます。こうしておくと、あとで1つのモジュールを別のサービスに切り出すときの作業が小さくなります。

面接ではこう答える

モジュラーモノリスは、デプロイは1つのままで、中を注文・在庫・会員のような業務の区切りでモジュールに分ける形です。モジュールの間は公開したインターフェースだけで呼び、他のモジュールの表を直接読まないルールにします。マイクロサービスに分けると、サービスごとの監視や CI、ネットワーク越しの失敗への備え、データの整合性など運用の負担が大きく増えるので、まずはこの形で始め、分ける理由がはっきりした所から切り出します。

ひとことで

サービスをまたぐ処理を、各サービスのローカルなトランザクションの列と、失敗時に取り消す補償の処理で表す考え方。

途中で失敗したら、逆順に取り消す(補償)注文を作る注文サービス在庫を確保在庫サービス決済失敗!在庫を戻す(補償)注文を取り消す(補償)
Saga:失敗したら補償の処理で取り消す

サービスごとに DB が分かれると、1つのトランザクションで「注文を作り、在庫を減らし、決済する」ことができません。Saga は、各サービスのローカルなトランザクションを順につなぎ、途中で失敗したら、それまでの手順を取り消す補償の処理を流す考え方です。

方式しくみ向いている場面
コレオグラフィ各サービスがイベントを受けて次を行う(指揮者なし)手順が少ない。サービスの独立性を重視
オーケストレーション1つの指揮役が手順を呼び出し、失敗時の取り消しも指示する手順が多い。流れを1か所で追いたい
  • 途中の状態(「注文は作ったが決済はまだ」)が外から見えるので、画面やステータスで表す。
  • メッセージは2回届くことがあるので、各手順と補償の処理は冪等に作る。
  • イベントの送信は Outbox パターンで DB の更新とずらさない。

SRE の「分散トランザクションとデータ整合性」

面接ではこう答える

サービスごとに DB が分かれると、複数のサービスにまたがる処理を1つのトランザクションでまとめられません。Saga は、注文の作成、在庫の確保、決済といった各サービスのローカルなトランザクションを順につなぎ、途中で失敗したら、在庫を戻す・注文をキャンセルするといった補償の処理を流す方法です。指揮役が手順を呼ぶオーケストレーションと、イベントで次々につなぐコレオグラフィがあります。各手順は冪等に作ります。

16.4レイヤーとドメイン

用語★★☆ 重要
ひとことで

画面・ユースケース・ドメイン・インフラを分け、変更の影響を小さくする。

コードを役割ごとの層(レイヤー)に分けると、変更の影響が広がりにくくなり、テストもしやすくなります。よくある分け方は次のとおりです。

依存は内側(ドメイン)に向けるプレゼンテーションコントローラー:HTTP の入出力アプリケーションユースケース・トランザクションドメイン業務のルール(DB を知らない)インフラDB・外部 API の実装ドメインが決めた形を実装する
レイヤーと依存の向き
層役割
プレゼンテーション(コントローラー・ハンドラー)HTTP のリクエストを受けて、入力を検証し、結果をレスポンスに変える
アプリケーション(ユースケース・サービス)「注文する」のような処理の手順を組み立てる。トランザクションの範囲
ドメイン業務のルール(在庫が足りなければ注文できない、割引の計算)
インフラ(リポジトリ・外部の接続)DB・外部 API・メール送信などの具体的な実装

やりすぎない

小さなアプリや、ほとんどが「DB に保存して表示するだけ」の画面に、層やインターフェースを何重にも作ると、1つの項目を足すのに5つのファイルを直すことになります。

  • まずはフレームワークの標準の形に従う(Rails の MVC など)。
  • 業務のルールが複雑な所(料金・在庫・権限)から、ドメインの層を分ける。
  • テストで差し替えたい外部(決済・メール)だけインターフェースにする。
  • 「後で必要になるかも」で抽象化しない。必要になったときに分ける。

面接ではこう答える

コードを役割ごとの層に分けると、変更の影響が広がりにくく、テストもしやすくなります。よくある分け方は、HTTP を受けるプレゼンテーション、処理の手順を組み立てるアプリケーション、業務のルールのドメイン、DB や外部の接続のインフラの4つです。ただし小さなアプリや表示するだけの画面に層を重ねすぎると、1つの項目を足すのに多くのファイルを直すことになるので、複雑な所から分けます。

ひとことで

業務のルールを中心に置き、DB やフレームワークへの依存を外側に追い出す設計。DDD は業務の言葉と区切りでモジュールを分ける。

依存は内側(業務のルール)へ向ける外側・コントローラー・画面・DB・外部 API の実装・フレームワーク内側・ユースケース・ドメイン(業務のルール)・DB やフレームワークを知らない
依存の向き:外側が内側に合わせる
  • クリーンアーキテクチャ(ヘキサゴナル・オニオンも同じ考え):依存の向きを内側(業務のルール)に向ける。ドメインは DB やフレームワークを知らず、インフラ側がドメインの決めたインターフェースを実装する。DB を替えても業務のルールは変わらず、テストで DB を差し替えられる。
  • DDD(ドメイン駆動設計):業務の言葉をそのままコードの名前に使い(ユビキタス言語)、業務の区切り(境界づけられたコンテキスト)でモジュールを分ける。同じ「顧客」でも、請求と配送では持つ情報が違う、のように考える。
DDD の用語意味
ユビキタス言語業務の人と同じ言葉をコードの名前に使う
境界づけられたコンテキスト言葉の意味が変わる所でモジュールを分ける(請求の「顧客」と配送の「顧客」)
集約一緒に整合性を守るデータのまとまり。更新はまとまりの入口から

面接ではこう答える

クリーンアーキテクチャは、依存の向きを内側の業務のルールに向ける考え方です。ドメインは DB やフレームワークを知らず、DB の実装のほうがドメインの決めたインターフェースに合わせます。こうすると業務のルールだけをテストでき、DB を替えても影響が小さくなります。DDD は、業務の人と同じ言葉をコードに使い、言葉の意味が変わる所でモジュールを分ける考え方です。ただし単純な画面にまで層を重ねるとやりすぎになるので、複雑な所から使います。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • モノリスとマイクロサービスの使い分け
  • 責務の分け方(レイヤー・ドメイン)

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1マイクロサービスにすべきかどう判断しますか?

模範回答

技術よりも、組織と運用のコストで判断します。マイクロサービスの一番の利点は、チームが他のチームを待たずに独立してデプロイできることなので、少人数のチームではその利点より、サービスごとの CI/CD・監視・ネットワークの失敗への備え・サービスをまたぐデータの整合性といった負担のほうが大きくなります。

なので最初はモジュールをきちんと分けたモノリスにし、チームが増えてデプロイがぶつかるようになった、特定の機能だけ負荷や要件が大きく違う、といった具体的な理由が出た所から切り出します。分けるなら業務の境界に沿って、データも一緒に分けられる所を選びます。分散トレースや監視などの基盤がそろっているかも判断の材料にします。

深掘りされたら

  • サービス間でデータの整合性はどう保ちますか?

見られているポイント組織・運用コストも含めたトレードオフ

Q2クリーンアーキテクチャのような層分けは、どこまでやりますか?

模範回答

業務のルールが複雑な所にだけ、しっかり分けます。例えば、料金の計算や在庫の引き当てのように条件が多い所は、DB やフレームワークに依存しないドメインの層にして、単体テストで細かく確かめます。

一方で、ほとんどが「保存して表示するだけ」の画面に何層もの抽象化を入れると、1つの項目を足すのに多くのファイルを直すことになり、かえって遅くなります。基本はフレームワークの標準の形に従い、テストで差し替えたい外部(決済やメール)だけをインターフェースにします。「後で必要になるかも」で抽象化せず、必要になったときに分けます。

深掘りされたら

  • DDD の「境界づけられたコンテキスト」とは何ですか?

見られているポイントやる所・やらない所の判断

Q3サービスを分けたとき、サービスをまたぐデータの整合性はどう保ちますか?

模範回答

サービスごとに DB が分かれると、1つのトランザクションでは守れません。そこで、各サービスは自分の DB を更新するのと同じトランザクションで「起きたこと」をイベントとして記録し(Outbox パターン)、それを他のサービスに届けて、受け取った側が自分のデータを更新します。一時的にずれることは受け入れ、最後には揃う(結果整合性)という考え方です。

注文と決済のように途中で失敗しうる流れは、失敗したら前の手順を取り消す処理を用意します(Saga パターン)。イベントは2回届くことがあるので、受け取る側は冪等にします。そもそも強い整合性が必要なデータは、同じサービスにまとめておくのが一番簡単です。

深掘りされたら

  • 結果整合性を画面ではどう見せますか?

見られているポイントOutbox・Saga・冪等、分けない選択肢