ストーリー
佐藤CTOが次のミーティングで、経営陣にモノリスの課題を説明することになりました。
技術的負債とは何か(再定義)
技術的負債は単なる「汚いコード」ではありません。Ward Cunningham が定義した概念では、技術的負債は戦略的な意思決定として発生するものです。
// 技術的負債の分類(Martin Fowlerの4象限)
interface TechDebtQuadrant {
// 意図的 × 慎重
deliberatePrudent: "今はこのトレードオフを受け入れる。リスクは把握している";
// 意図的 × 無謀
deliberateReckless: "設計なんて後でやればいい";
// 無意識 × 慎重
inadvertentPrudent: "今ならもっと良い方法があったとわかる";
// 無意識 × 無謀
inadvertentReckless: "レイヤリングって何?";
}
| 種類 | 意図 | 知識 | 例 |
|---|---|---|---|
| 戦略的負債 | 意図的 | 慎重 | 市場投入を優先してリリース |
| 怠慢な負債 | 意図的 | 無謀 | テストを書かない判断 |
| 学習による負債 | 無意識 | 慎重 | 後から良い設計を発見 |
| 無知による負債 | 無意識 | 無謀 | 設計の知識不足 |
コード複雑度メトリクス
1. 循環的複雑度(Cyclomatic Complexity)
分岐の数に基づいてコードの複雑さを測定します。
// 循環的複雑度 = 1(基本パス)+ 分岐の数
// 複雑度: 1(分岐なし)
function getFullName(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
// 複雑度: 7(if × 3 + else if × 2 + && × 1 + || × 1)
function calculateDiscount(order: Order): number {
let discount = 0;
if (order.total > 10000 && order.isPremiumMember) { // +2
discount = 0.15;
} else if (order.total > 10000) { // +1
discount = 0.10;
} else if (order.total > 5000 || order.isFirstOrder) { // +2
discount = 0.05;
}
if (order.hasCoupon) { // +1
discount += 0.05;
}
return discount;
}
| 複雑度 | 評価 | リスク |
|---|---|---|
| 1-5 | 良好 | テストが容易 |
| 6-10 | 中程度 | リファクタリング検討 |
| 11-20 | 高い | 分割すべき |
| 21以上 | 非常に高い | 即座にリファクタリング |
2. 認知的複雑度(Cognitive Complexity)
SonarQube が提唱する、人間にとっての理解しやすさを測るメトリクスです。
// 循環的複雑度は同じ(5)だが、認知的複雑度は異なる
// 認知的複雑度: 低い -- フラットな構造
function processItems(items: Item[]): Result[] {
const results: Result[] = [];
for (const item of items) { // +1
if (item.isValid) { // +1
results.push(transform(item));
}
}
return results;
}
// 認知的複雑度: 高い -- ネストが深い
function processItems(items: Item[]): Result[] {
const results: Result[] = [];
for (const item of items) { // +1
if (item.isValid) { // +2(ネスト+1)
if (item.type === 'special') { // +3(ネスト+2)
if (item.priority > 5) { // +4(ネスト+3)
results.push(transform(item));
}
}
}
}
return results;
}
3. コード行数と関数の長さ
// 計測すべきメトリクス
interface CodeSizeMetrics {
linesOfCode: number; // 総コード行数
averageFunctionLength: number; // 関数の平均行数
maxFunctionLength: number; // 最大関数行数
averageFileLength: number; // ファイルの平均行数
maxFileLength: number; // 最大ファイル行数
averageClassMethods: number; // クラスの平均メソッド数
maxClassMethods: number; // クラスの最大メソッド数
}
// 警告の閾値
const THRESHOLDS = {
maxFunctionLength: 50, // 50行を超えたら要注意
maxFileLength: 500, // 500行を超えたら要注意
maxClassMethods: 15, // 15メソッドを超えたら要注意
maxCyclomaticComplexity: 10,
maxCognitiveComplexity: 15,
};
依存関係グラフ
import グラフの可視化
// TypeScriptのimport文を解析して依存関係を可視化する
// madge や dependency-cruiser などのツールが使える
// 問題のある依存パターン:循環参照
// order/OrderService.ts
import { InventoryService } from '../inventory/InventoryService';
// inventory/InventoryService.ts
import { OrderService } from '../order/OrderService'; // 循環!
// 問題のある依存パターン:レイヤー違反
// domain/entities/Order.ts
import { OrderRepository } from '../../infrastructure/OrderRepository'; // 内向きの依存が逆転!
# dependency-cruiser で依存関係を検証
npx depcruise --validate .dependency-cruiser.cjs src/
# madge で循環参照を検出
npx madge --circular src/
依存関係の可視化例
モジュール間の依存関係マトリクス:
注文 在庫 決済 ユーザー 通知 レポート
注文 - ★★ ★★★ ★★ ★ ★★
在庫 ★★ - ☆ ★ ☆ ★★
決済 ★★ ☆ - ★★★ ★ ☆
ユーザー ★ ★ ★★ - ★ ☆
通知 ★★ ★ ★ ★★ - ☆
レポート ★★★ ★★ ★★ ★ ☆ -
★★★ = 強い依存(10以上の参照)
★★ = 中程度の依存(5-9の参照)
★ = 弱い依存(1-4の参照)
☆ = 依存なし
モジュール結合度分析
求心性結合度と遠心性結合度
// 求心性結合度(Afferent Coupling: Ca)
// = 他モジュールからこのモジュールへの依存数
// 高い = 多くのモジュールがこれに依存している(変更の影響が大きい)
// 遠心性結合度(Efferent Coupling: Ce)
// = このモジュールが他モジュールに依存している数
// 高い = 多くのモジュールの変更に影響を受ける
// 不安定度(Instability: I = Ce / (Ca + Ce))
// 0に近い = 安定(多くから依存される)
// 1に近い = 不安定(多くに依存している)
interface ModuleCouplingMetrics {
moduleName: string;
afferentCoupling: number; // Ca: 被依存数
efferentCoupling: number; // Ce: 依存数
instability: number; // I = Ce / (Ca + Ce)
abstractness: number; // インターフェースの割合
}
// 例:各モジュールのメトリクス
const modules: ModuleCouplingMetrics[] = [
{ moduleName: 'User', afferentCoupling: 15, efferentCoupling: 2, instability: 0.12, abstractness: 0.6 },
{ moduleName: 'Order', afferentCoupling: 8, efferentCoupling: 12, instability: 0.60, abstractness: 0.3 },
{ moduleName: 'Inventory', afferentCoupling: 6, efferentCoupling: 8, instability: 0.57, abstractness: 0.2 },
{ moduleName: 'Report', afferentCoupling: 1, efferentCoupling: 18, instability: 0.95, abstractness: 0.1 },
];
ツールによる可視化
SonarQube
| 機能 | 説明 |
|---|---|
| 技術的負債の時間換算 | 負債の修正に必要な推定時間を算出 |
| コードスメル検出 | 重複、複雑度、命名など |
| セキュリティホットスポット | 潜在的な脆弱性を検出 |
| カバレッジ | テストカバレッジの計測 |
| Quality Gate | 品質基準を満たしているかの判定 |
Structure101(構造分析)
Structure101 が検出する問題:
- 循環依存(パッケージ間、クラス間)
- レイヤー違反
- 過剰な依存の集中
- 適切でないパッケージ構成
その他のツール
| ツール | 用途 | 言語 |
|---|---|---|
| ESLint + complexity rules | 循環的複雑度チェック | TypeScript/JS |
| madge | 循環参照検出 | TypeScript/JS |
| dependency-cruiser | 依存関係ルール検証 | TypeScript/JS |
| CodeClimate | 総合品質分析 | 多言語 |
| Snyk | 依存ライブラリの脆弱性 | 多言語 |
コードホットスポット分析
コードホットスポットは「変更頻度 x 複雑度」で特定します。Adam Tornhill の「Your Code as a Crime Scene」で提唱された手法です。
変更頻度の分析
# Gitログから変更頻度の高いファイルを特定
git log --since="6 months ago" --pretty=format: --name-only | \
sort | uniq -c | sort -rn | head -20
# 結果例:
# 142 src/services/OrderService.ts ← ホットスポット候補
# 98 src/models/Order.ts
# 87 src/services/InventoryService.ts
# 76 src/controllers/OrderController.ts
# 45 src/utils/helpers.ts
ホットスポットマップの作成
// 変更頻度と複雑度を組み合わせる
interface HotspotAnalysis {
filePath: string;
changeFrequency: number; // 過去6ヶ月の変更回数
complexity: number; // 循環的複雑度の合計
hotspotScore: number; // changeFrequency × complexity
authors: number; // 変更した人数(知識の分散度)
}
// ホットスポットの分類
//
// 複雑度
// 高 │ 要注意 ★ ホットスポット
// │ (複雑だが (複雑で頻繁に変更
// │ 変更少ない) = 最優先で対処)
// │
// 低 │ 安全 監視
// │ (シンプルで (シンプルだが
// │ 変更少ない) 頻繁に変更)
// └──────────────────────────→ 変更頻度
// 低 高
const hotspots: HotspotAnalysis[] = [
{
filePath: 'src/services/OrderService.ts',
changeFrequency: 142,
complexity: 85,
hotspotScore: 12070, // ★ 最優先
authors: 8,
},
{
filePath: 'src/utils/helpers.ts',
changeFrequency: 45,
complexity: 120,
hotspotScore: 5400, // 要注意
authors: 12,
},
];
技術的負債ヒートマップの作成
チーム全体で技術的負債を共有するためのヒートマップを作成します。
// ヒートマップのデータ構造
interface TechDebtHeatmap {
modules: ModuleDebtInfo[];
totalDebtHours: number;
totalDebtCost: number;
}
interface ModuleDebtInfo {
name: string;
debtHours: number; // 修正にかかる推定時間
severity: 'low' | 'medium' | 'high' | 'critical';
categories: DebtCategory[];
trend: 'improving' | 'stable' | 'worsening';
}
interface DebtCategory {
type: string;
count: number;
estimatedHours: number;
}
// 実際のヒートマップデータ例
const heatmap: TechDebtHeatmap = {
modules: [
{
name: '注文管理',
debtHours: 320,
severity: 'critical',
categories: [
{ type: '循環依存', count: 12, estimatedHours: 80 },
{ type: '巨大クラス', count: 5, estimatedHours: 120 },
{ type: 'テスト不足', count: 45, estimatedHours: 90 },
{ type: 'コード重複', count: 18, estimatedHours: 30 },
],
trend: 'worsening',
},
{
name: 'ユーザー管理',
debtHours: 80,
severity: 'medium',
categories: [
{ type: '命名の不統一', count: 30, estimatedHours: 20 },
{ type: 'テスト不足', count: 15, estimatedHours: 60 },
],
trend: 'stable',
},
],
totalDebtHours: 400,
totalDebtCost: 4000000, // 400時間 × 時給10,000円
};
ビジネスインパクト評価
技術的負債を経営陣に伝えるためには、ビジネスへの影響を数値化する必要があります。
// ビジネスインパクトの計算
interface BusinessImpact {
// 開発速度への影響
velocityReduction: {
current: number; // 現在のストーリーポイント/スプリント
potential: number; // 負債解消後の予測値
costPerSprint: number; // 失われているストーリーポイントの金額換算
};
// 障害コスト
incidentCost: {
averageIncidentsPerMonth: number;
averageMTTR: number; // 平均復旧時間(時間)
costPerHourDowntime: number; // ダウンタイム1時間あたりのコスト
monthlyIncidentCost: number;
};
// 人材コスト
talentCost: {
averageRampUpDays: number; // 新メンバーの立ち上がり日数
annualTurnoverRate: number; // 年間離職率
costPerHire: number; // 1人あたりの採用コスト
annualTalentCost: number;
};
// 機会損失
opportunityCost: {
delayedFeatures: number; // 遅延している機能数
estimatedRevenueImpact: number; // 推定売上影響
};
}
// 経営陣向けサマリー
function generateExecutiveSummary(impact: BusinessImpact): string {
const annualCost =
impact.velocityReduction.costPerSprint * 26 + // スプリント単位
impact.incidentCost.monthlyIncidentCost * 12 + // 月次障害
impact.talentCost.annualTalentCost + // 人材コスト
impact.opportunityCost.estimatedRevenueImpact; // 機会損失
return `
技術的負債による年間推定コスト: ${(annualCost / 10000).toFixed(0)}万円
- 開発速度低下: ${(impact.velocityReduction.costPerSprint * 26 / 10000).toFixed(0)}万円
- 障害対応: ${(impact.incidentCost.monthlyIncidentCost * 12 / 10000).toFixed(0)}万円
- 人材: ${(impact.talentCost.annualTalentCost / 10000).toFixed(0)}万円
- 機会損失: ${(impact.opportunityCost.estimatedRevenueImpact / 10000).toFixed(0)}万円
`;
}
経営陣への報告テンプレート
| 項目 | 現状 | 負債解消後 | 年間インパクト |
|---|---|---|---|
| デプロイ頻度 | 月2回 | 週3回以上 | 機能リリース速度6倍 |
| 障害発生率 | 月4件 | 月1件以下 | 障害コスト75%削減 |
| 新メンバー立ち上がり | 2ヶ月 | 2週間 | 採用効率4倍向上 |
| 開発ベロシティ | 40SP/スプリント | 60SP/スプリント | 年間約2000万円の生産性向上 |
まとめ
| ポイント | 内容 |
|---|---|
| 技術的負債の分類 | 意図的/無意識 × 慎重/無謀の4象限 |
| 複雑度メトリクス | 循環的複雑度、認知的複雑度、コード行数 |
| 依存関係分析 | import グラフ、結合度、不安定度 |
| ホットスポット | 変更頻度 x 複雑度で特定する |
| ビジネスインパクト | 開発速度、障害コスト、人材コスト、機会損失で定量化 |
| ツール | SonarQube、dependency-cruiser、madge、CodeClimate |
チェックリスト
- 技術的負債の4象限分類を理解した
- 循環的複雑度と認知的複雑度の違いを説明できる
- コードホットスポットの特定方法を理解した
- 技術的負債をビジネスインパクトとして定量化できる
- 経営陣向けの報告に必要な観点を把握した
次のステップへ
次は「依存関係の分析手法」を学びます。技術的負債の中でも特にモノリスの分割判断に重要な「依存関係」を、より深く分析する手法を身につけましょう。
推定読了時間: 30分