LESSON 30分

ストーリー

佐藤CTOが次のミーティングで、経営陣にモノリスの課題を説明することになりました。

佐藤CTO
“モノリスが辛い”では経営陣は動かない。“この部分の変更に1ヶ月かかり、その間に毎週2件の障害が発生し、年間で推定3000万円のコストが発生している”と言えば、判断材料になる
あなた
数字で語るということですか
佐藤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分