ストーリー
佐藤CTOがモニターに依存関係グラフを表示しました。線が無数に交差し、まるで蜘蛛の巣のようです。
静的分析ツール
静的分析は、コードを実行せずにソースコードの構造を解析する手法です。
ESLint による依存ルールの検証
// .eslintrc.js -- import制約の設定例
module.exports = {
rules: {
'no-restricted-imports': ['error', {
patterns: [
// ドメイン層からインフラ層へのimportを禁止
{
group: ['../infrastructure/*', '../adapters/*'],
message: 'ドメイン層からインフラ層への依存は許可されていません',
},
// モジュール間の直接import禁止(公開APIのみ許可)
{
group: ['../order/internal/*', '../inventory/internal/*'],
message: 'モジュール内部への直接アクセスは禁止です。公開APIを使用してください',
},
],
}],
},
};
dependency-cruiser による依存ルール検証
// .dependency-cruiser.cjs
module.exports = {
forbidden: [
{
name: 'no-circular',
comment: '循環依存を禁止',
severity: 'error',
from: {},
to: { circular: true },
},
{
name: 'no-domain-to-infra',
comment: 'ドメイン層からインフラ層への依存を禁止',
severity: 'error',
from: { path: '^src/domain' },
to: { path: '^src/(infrastructure|adapters)' },
},
{
name: 'no-cross-module-internals',
comment: 'モジュール間の内部アクセスを禁止',
severity: 'warn',
from: { path: '^src/modules/([^/]+)/' },
to: {
path: '^src/modules/([^/]+)/',
pathNot: '^src/modules/$1/', // 同一モジュール内は許可
},
},
],
options: {
doNotFollow: { path: 'node_modules' },
tsConfig: { fileName: 'tsconfig.json' },
reporterOptions: {
dot: { theme: { graph: { rankdir: 'TB' } } },
},
},
};
import グラフ分析
import 関係の抽出と可視化
// TypeScriptのAST(抽象構文木)を使ったimport分析
import * as ts from 'typescript';
import * as path from 'path';
interface ImportRelation {
source: string; // importしているファイル
target: string; // importされているファイル
importedNames: string[]; // importしている名前
isTypeOnly: boolean; // 型のみのimportか
}
function analyzeImports(sourceFile: ts.SourceFile): ImportRelation[] {
const relations: ImportRelation[] = [];
ts.forEachChild(sourceFile, (node) => {
if (ts.isImportDeclaration(node)) {
const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
const importClause = node.importClause;
const importedNames: string[] = [];
if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
for (const element of importClause.namedBindings.elements) {
importedNames.push(element.name.text);
}
}
relations.push({
source: sourceFile.fileName,
target: moduleSpecifier,
importedNames,
isTypeOnly: importClause?.isTypeOnly ?? false,
});
}
});
return relations;
}
モジュール間の依存強度を測定
// モジュール間の依存強度を数値化する
interface ModuleDependency {
fromModule: string;
toModule: string;
importCount: number; // import文の数
symbolCount: number; // importされているシンボルの数
isCircular: boolean; // 循環依存か
couplingStrength: 'weak' | 'moderate' | 'strong';
}
function calculateCouplingStrength(symbolCount: number): string {
if (symbolCount <= 2) return 'weak';
if (symbolCount <= 5) return 'moderate';
return 'strong';
}
// 分析結果の例
const dependencies: ModuleDependency[] = [
{
fromModule: 'order',
toModule: 'inventory',
importCount: 8,
symbolCount: 12,
isCircular: true, // 問題!
couplingStrength: 'strong',
},
{
fromModule: 'order',
toModule: 'user',
importCount: 3,
symbolCount: 3,
isCircular: false,
couplingStrength: 'moderate',
},
];
データベース依存関係のマッピング
モノリスでは通常、すべてのモジュールが1つのデータベースを共有しています。この共有状態を分析することが、サービス分割の鍵です。
テーブル間の参照関係
-- 外部キーによる参照関係を分析
-- PostgreSQLの例
SELECT
tc.table_name AS source_table,
ccu.table_name AS referenced_table,
tc.constraint_name,
kcu.column_name AS fk_column,
ccu.column_name AS referenced_column
FROM
information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE
tc.constraint_type = 'FOREIGN KEY'
ORDER BY
tc.table_name;
// データベース依存関係の分析結果
interface DatabaseDependency {
sourceTable: string;
targetTable: string;
foreignKeys: string[];
joinQueriesCount: number; // このJOINが使われているクエリの数
belongsToModule: string; // どのモジュールが主に使用しているか
}
// テーブルの所属分析
const tableDependencies: DatabaseDependency[] = [
{
sourceTable: 'orders',
targetTable: 'users',
foreignKeys: ['user_id'],
joinQueriesCount: 45,
belongsToModule: 'order',
},
{
sourceTable: 'order_items',
targetTable: 'products',
foreignKeys: ['product_id'],
joinQueriesCount: 32,
belongsToModule: 'order', // products テーブルは inventory モジュール
},
{
sourceTable: 'payments',
targetTable: 'orders',
foreignKeys: ['order_id'],
joinQueriesCount: 28,
belongsToModule: 'payment',
},
];
テーブルアクセスパターンの分析
// どのモジュールがどのテーブルを読み書きしているかを分析
interface TableAccessPattern {
table: string;
readModules: string[]; // 読み取りしているモジュール
writeModules: string[]; // 書き込みしているモジュール
isSharedWrite: boolean; // 複数モジュールが書き込みしている(問題)
}
const accessPatterns: TableAccessPattern[] = [
{
table: 'users',
readModules: ['user', 'order', 'payment', 'notification', 'report'],
writeModules: ['user'],
isSharedWrite: false, // OK: user モジュールだけが書き込み
},
{
table: 'orders',
readModules: ['order', 'inventory', 'payment', 'report'],
writeModules: ['order', 'payment'], // 問題: 2つのモジュールが書き込み
isSharedWrite: true,
},
{
table: 'products',
readModules: ['inventory', 'order', 'report', 'search'],
writeModules: ['inventory', 'order'], // 問題: orderが直接productを更新
isSharedWrite: true,
},
];
共有ライブラリの識別
共有コードの分類
// 共有コードの種類と対処方針
interface SharedCode {
path: string;
type: 'utility' | 'domain-model' | 'infrastructure' | 'cross-cutting';
usedByModules: string[];
recommendation: string;
}
const sharedCode: SharedCode[] = [
{
path: 'src/shared/utils/dateUtils.ts',
type: 'utility',
usedByModules: ['order', 'inventory', 'report'],
recommendation: '共有ライブラリとしてパッケージ化',
},
{
path: 'src/shared/models/Money.ts',
type: 'domain-model',
usedByModules: ['order', 'payment', 'inventory'],
recommendation: '各サービスにコピーして独立管理',
},
{
path: 'src/shared/database/connection.ts',
type: 'infrastructure',
usedByModules: ['*'],
recommendation: 'サービスごとに独自の接続管理に変更',
},
{
path: 'src/shared/middleware/auth.ts',
type: 'cross-cutting',
usedByModules: ['*'],
recommendation: 'API Gatewayまたは認証サービスに移行',
},
];
循環依存の検出
循環依存はモノリスの分割を困難にする最大の障壁の一つです。
// 循環依存の検出アルゴリズム(深さ優先探索)
class CircularDependencyDetector {
private graph: Map<string, Set<string>> = new Map();
private cycles: string[][] = [];
addDependency(from: string, to: string): void {
if (!this.graph.has(from)) this.graph.set(from, new Set());
this.graph.get(from)!.add(to);
}
detectCycles(): string[][] {
const visited = new Set<string>();
const recursionStack = new Set<string>();
const path: string[] = [];
for (const node of this.graph.keys()) {
if (!visited.has(node)) {
this.dfs(node, visited, recursionStack, path);
}
}
return this.cycles;
}
private dfs(
node: string,
visited: Set<string>,
recursionStack: Set<string>,
path: string[],
): void {
visited.add(node);
recursionStack.add(node);
path.push(node);
const neighbors = this.graph.get(node) || new Set();
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
this.dfs(neighbor, visited, recursionStack, path);
} else if (recursionStack.has(neighbor)) {
// 循環を検出
const cycleStart = path.indexOf(neighbor);
this.cycles.push([...path.slice(cycleStart), neighbor]);
}
}
path.pop();
recursionStack.delete(node);
}
}
// 使用例
const detector = new CircularDependencyDetector();
detector.addDependency('OrderService', 'InventoryService');
detector.addDependency('InventoryService', 'OrderService'); // 循環!
detector.addDependency('OrderService', 'PaymentService');
const cycles = detector.detectCycles();
// [['OrderService', 'InventoryService', 'OrderService']]
循環依存の解消パターン
| パターン | 方法 | 適用場面 |
|---|---|---|
| インターフェース抽出 | 共通インターフェースを上位に定義 | 双方向の依存を一方向に |
| イベント駆動 | 直接呼び出しをイベント発行に変更 | 非同期で十分な場合 |
| 仲介者パターン | 中間サービスを導入 | 複雑な依存の解消 |
| 依存の逆転 | 依存方向を設計し直す | アーキテクチャレベルの改善 |
// Before: 循環依存
// OrderService → InventoryService → OrderService
// After: イベント駆動で解消
class OrderService {
constructor(
private eventBus: EventBus,
private inventoryPort: InventoryPort, // インターフェースへの依存
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const available = await this.inventoryPort.checkStock(dto.items);
const order = await this.orderRepo.save(new Order(dto));
// 直接呼び出しの代わりにイベントを発行
await this.eventBus.publish(new OrderCreatedEvent(order));
return order;
}
}
// InventoryServiceはOrderServiceに直接依存しない
class InventoryEventHandler {
constructor(private inventoryService: InventoryService) {}
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.inventoryService.reserveStock(event.order.items);
}
}
変更結合分析(Change Coupling)
一緒に変更されることが多いファイルは、論理的に結合しています。
# Gitログから一緒に変更されるファイルのペアを分析
# 過去6ヶ月のコミットを分析
git log --since="6 months ago" --pretty=format:'%H' | while read commit; do
git diff-tree --no-commit-id --name-only -r "$commit"
echo "---"
done
// 変更結合度の分析
interface ChangeCoupling {
file1: string;
file2: string;
coChangeCount: number; // 一緒に変更された回数
file1TotalChanges: number; // file1の総変更回数
file2TotalChanges: number; // file2の総変更回数
couplingStrength: number; // coChangeCount / min(total1, total2)
}
// 分析結果例
const changeCouplings: ChangeCoupling[] = [
{
file1: 'src/services/OrderService.ts',
file2: 'src/services/InventoryService.ts',
coChangeCount: 35,
file1TotalChanges: 50,
file2TotalChanges: 40,
couplingStrength: 0.875, // 非常に強い結合!
},
{
file1: 'src/models/Order.ts',
file2: 'src/controllers/OrderController.ts',
coChangeCount: 28,
file1TotalChanges: 40,
file2TotalChanges: 30,
couplingStrength: 0.933, // 同一モジュール内なので問題なし
},
];
// モジュール間の変更結合は問題のサイン
// 同一モジュール内の変更結合は自然
依存関係マトリクスの作成
分析結果を統合して、サービス分割の判断材料となる依存関係マトリクスを作成します。
interface DependencyMatrix {
modules: string[];
// matrix[i][j] = モジュールiからモジュールjへの依存強度
codeDepMatrix: number[][]; // コード依存
dataDepMatrix: number[][]; // データ依存
changeDepMatrix: number[][]; // 変更結合
combinedMatrix: number[][]; // 総合評価
}
// 依存関係マトリクスの作成
function createDependencyMatrix(
codeDeps: ModuleDependency[],
dataDeps: DatabaseDependency[],
changeDeps: ChangeCoupling[],
): DependencyMatrix {
// 各依存を0-1のスケールに正規化して結合
// 重み: コード依存 40%, データ依存 35%, 変更結合 25%
const weights = { code: 0.4, data: 0.35, change: 0.25 };
// ... マトリクス計算 ...
return matrix;
}
// 結果の解釈
// ┌──────────────────────────────────────────┐
// │ 注文 在庫 決済 ユーザー 通知 │
// │ 注文 - 0.8 0.7 0.3 0.2 │
// │ 在庫 0.7 - 0.1 0.2 0.1 │
// │ 決済 0.6 0.1 - 0.4 0.3 │
// │ ユーザー 0.2 0.1 0.3 - 0.2 │
// │ 通知 0.1 0.1 0.1 0.2 - │
// └──────────────────────────────────────────┘
//
// 注文 ↔ 在庫: 0.8/0.7 -- 非常に強い双方向依存(分割困難)
// 注文 ↔ 決済: 0.7/0.6 -- 強い依存(慎重に分割)
// 通知 ↔ 他: 0.1-0.2 -- 弱い依存(分割しやすい候補)
まとめ
| ポイント | 内容 |
|---|---|
| 静的分析 | ESLint、dependency-cruiser でルールベースの検証 |
| import分析 | AST解析でモジュール間の依存強度を測定 |
| DB依存分析 | テーブル参照関係とアクセスパターンの把握 |
| 循環依存 | 検出と解消パターン(インターフェース、イベント駆動) |
| 変更結合 | Gitログから論理的な結合度を測定 |
| 依存マトリクス | コード・データ・変更の3軸で総合評価 |
チェックリスト
- 静的分析ツールの役割と設定方法を理解した
- importグラフからモジュール間の依存強度を分析できる
- データベースの依存関係をマッピングできる
- 循環依存の検出と解消パターンを理解した
- 変更結合分析の方法を理解した
- 依存関係マトリクスの作成と解釈ができる
次のステップへ
次は「移行判断のフレームワーク」を学びます。これまでの分析結果を踏まえて、モノリスからマイクロサービスへの移行を判断するための体系的なフレームワークを身につけましょう。
推定読了時間: 30分