実践例③:複雑なバグの調査と修正
本章では、AIコーディングツールを活用した複雑なバグ調査と修正の実践的なワークフローを紹介します。
注記: 本章はAI活用テクニックを解説するためのチュートリアルです。具体的な生産性向上の数値については、実在企業の調査データ(Vol.6 第8章参照)をご確認ください。
よくある問題シナリオ
問題例: 本番環境で断続的に発生するメモリリーク
症状:
- 1-2週間ごとにサーバーがOOM(Out of Memory)でクラッシュ
- 再現条件が不明
- 従来の調査では原因特定に至らず
環境例:
- Node.js 18 + Express
- PostgreSQL
- Redis
- 本番トラフィック: 1,000 req/min
調査フェーズ
11.2.1 Claude Codeで段階的調査
プロンプト:
Node.jsアプリケーションで断続的なメモリリークが発生しています。
状況:
- 1-2週間ごとにOOMでクラッシュ
- 特定のエンドポイントとの相関は見つかっていない
- ヒープダンプ分析では明確なリーク元が不明
段階的に調査してください:
Step 1: よくあるメモリリークパターンを列挙
Step 2: このプロジェクトのコードで該当しそうな箇所を特定
Step 3: 各候補について詳細に分析
Step 4: 最も可能性が高い原因を特定
11.2.2 AIが提示する調査結果の例
## Step 1: Node.jsでよくあるメモリリークパターン
1. イベントリスナーの未解除
2. クロージャによる参照保持
3. グローバル変数への蓄積
4. キャッシュの無制限成長
5. タイマー(setInterval)の未クリア
6. Promiseの未処理
7. DB接続プールの不適切な管理
## Step 2: プロジェクト内の候補箇所
分析結果、以下の3箇所が疑わしいです:
1. src/services/cache.ts - インメモリキャッシュ
2. src/websocket/handler.ts - WebSocketイベントリスナー
3. src/jobs/scheduler.ts - 定期実行ジョブ
## Step 3: 詳細分析
### 3.1 cache.ts(最も疑わしい)
```typescript
// 問題のあるコードパターン
const cache = new Map<string, any>();
export function setCache(key: string, value: any) {
cache.set(key, value); // TTLなし、サイズ制限なし
}
このキャッシュには:
- エントリの有効期限がない
- 最大サイズの制限がない
- 古いエントリの削除ロジックがない
時間とともに無制限に成長します。
---
## 原因特定
### 11.3.1 証拠の収集
プロンプト: cache.ts がメモリリークの原因である可能性を検証します。
以下を確認してください:
- このキャッシュを使用している箇所
- キャッシュに格納されるデータの種類
- キャッシュエントリの推定成長率
- 1週間でどの程度のメモリを消費するか推定
### 11.3.2 分析結果の例
```markdown
## 使用箇所の分析
cache.set() の呼び出し箇所:
1. src/api/users.ts:45 - ユーザーセッション情報
2. src/api/products.ts:78 - 商品検索結果
3. src/api/analytics.ts:23 - 分析データ
## 成長率の推定
- ユーザーセッション: 1セッション ≒ 2KB
- 1日のユニークユーザー: 約10,000
- 1日の新規キャッシュエントリ: 約10,000 × 2KB = 20MB
## 1週間の累積
- 7日 × 20MB = 140MB(セッションのみ)
- 商品検索・分析データを含めると: 推定 500MB/週
## 結論:押さえておくべきポイント
10日〜14日で約1GBに達し、
4GBのコンテナメモリ上限に到達する計算と一致。
**原因確定: TTLなしのインメモリキャッシュ**
修正フェーズ
11.4.1 修正方針の検討
プロンプト:
インメモリキャッシュのメモリリーク問題を修正します。
選択肢:
A. TTLとLRUを追加(node-cache等のライブラリ使用)
B. Redisに移行(既にRedis接続あり)
C. 自前でTTL付きキャッシュを実装
各選択肢のメリット・デメリットを分析し、
推奨案を提示してください。
制約:
- 本番環境への影響を最小化
- 既存のAPIを維持(後方互換性)
11.4.2 AIの推奨例
## 推奨: B. Redisに移行
### 理由
1. 既にRedis接続が存在(追加インフラ不要)
2. 分散環境でもキャッシュを共有可能
3. TTL、LRU、メモリ制限が標準機能
4. 将来のスケールアウトに対応
### 移行戦略
1. 新しいRedisキャッシュサービスを作成
2. 既存のcache.tsのインターフェースを維持
3. Feature Flagで段階的に切り替え
4. 問題なければ完全移行
11.4.3 実装
プロンプト:
Redisベースのキャッシュサービスを実装してください。
要件:
- 既存のcache.tsと同じインターフェース
- TTL対応(デフォルト1時間)
- エラーハンドリング(Redis障害時はキャッシュなしで動作)
- 型安全性を維持
@src/services/cache.ts
@src/lib/redis.ts
11.4.4 生成されたコード例
// src/services/redis-cache.ts
import { redis } from '../lib/redis';
const DEFAULT_TTL = 3600; // 1時間
export async function setCache<T>(
key: string,
value: T,
ttl: number = DEFAULT_TTL
): Promise<void> {
try {
const serialized = JSON.stringify(value);
await redis.setex(key, ttl, serialized);
} catch (error) {
console.error('Cache set failed:', error);
// Redis障害時はエラーを握りつぶす(キャッシュなしで継続)
}
}
export async function getCache<T>(key: string): Promise<T | null> {
try {
const value = await redis.get(key);
if (!value) return null;
return JSON.parse(value) as T;
} catch (error) {
console.error('Cache get failed:', error);
return null;
}
}
export async function deleteCache(key: string): Promise<void> {
try {
await redis.del(key);
} catch (error) {
console.error('Cache delete failed:', error);
}
}
// 互換性レイヤー(同期APIを非同期に移行)
export const cache = {
set: setCache,
get: getCache,
delete: deleteCache
};
テストと検証
11.5.1 テスト生成
プロンプト:
redis-cache.ts のテストを書いてください。
カバーすべきケース:
- 正常系(set/get/delete)
- TTL期限切れ
- Redis障害時のフォールバック
- 大量データの格納
モック: ioredis-mock を使用
11.5.2 生成されたテスト例
import Redis from 'ioredis-mock';
import { setCache, getCache, deleteCache } from './redis-cache';
// モックRedisをDI
jest.mock('../lib/redis', () => ({
redis: new Redis()
}));
describe('RedisCache', () => {
beforeEach(async () => {
const { redis } = require('../lib/redis');
await redis.flushall();
});
describe('setCache / getCache', () => {
it('値を保存して取得できる', async () => {
await setCache('user:1', { id: 1, name: 'Test' });
const result = await getCache('user:1');
expect(result).toEqual({ id: 1, name: 'Test' });
});
it('存在しないキーはnullを返す', async () => {
const result = await getCache('nonexistent');
expect(result).toBeNull();
});
});
describe('TTL', () => {
it('TTL後にデータが消える', async () => {
await setCache('temp', 'value', 1); // 1秒
const before = await getCache('temp');
expect(before).toBe('value');
await new Promise(r => setTimeout(r, 1100));
const after = await getCache('temp');
expect(after).toBeNull();
});
});
describe('エラーハンドリング', () => {
it('Redis障害時もエラーを投げない', async () => {
const { redis } = require('../lib/redis');
redis.disconnect();
// エラーを投げずにnullを返す
await expect(setCache('key', 'value')).resolves.not.toThrow();
const result = await getCache('key');
expect(result).toBeNull();
});
});
});
デプロイと監視
11.6.1 段階的ロールアウト
プロンプト:
Feature Flagを使った段階的移行の計画を立ててください。
要件:
- 0%から始めて100%まで
- 各段階でメモリ使用量を監視
- 問題があれば即座にロールバック
- 移行期間: 1週間
11.6.2 監視ダッシュボード設定
プロンプト:
メモリリーク修正後の監視項目を提案してください。
監視ツール: Datadog
アラート条件も含めてください。
AIが特に有効なポイント
- パターン認識: よくあるメモリリークパターンの網羅的列挙
- コード分析: 大量のコードから疑わしい箇所を迅速に特定
- 定量分析: 成長率の推定と原因の確度向上
- 修正案の比較: 複数の選択肢のトレードオフ分析
- テスト生成: エッジケースを含むテストの自動生成
人間が判断すべき部分
- 最終的な原因の確定(AIの分析を検証)
- 修正方針の決定(Redisへの移行を選択)
- ロールアウト戦略の承認
- 本番デプロイの実行
デバッグ用プロンプト集
11.9.1 段階的調査
以下のエラー/問題を段階的に調査してください。
問題: [エラーメッセージまたは症状]
Step 1: 考えられる原因を列挙
Step 2: このプロジェクトで該当しそうな箇所を特定
Step 3: 各候補の詳細分析
Step 4: 最も可能性が高い原因を特定
Step 5: 修正案を提示
11.9.2 エラーログ分析
以下のエラーログを分析してください。
[エラーログを貼り付け]
分析してほしい内容:
1. エラーの直接的な原因
2. スタックトレースから推測される問題箇所
3. 関連するコードファイル
4. 修正方法の提案
11.9.3 パフォーマンス問題の調査
以下のパフォーマンス問題を調査してください。
症状: [レスポンス遅延、CPU使用率高騰など]
確認してほしい観点:
1. N+1クエリ
2. 不要な再レンダリング
3. 非効率なアルゴリズム
4. ブロッキング処理
5. メモリ/キャッシュの問題
@[関連ファイル]
AI開発ツールの実際の効果
デバッグにおけるAIコーディングツールの実際の効果については、以下の研究を参照:
出典: Microsoft Research / GitHub
研究結果:
- タスク完了数: 26%増加
- コード品質への悪影響: なし
- 経験の浅い開発者ほど効果大
詳細はVol.6 第8章「導入事例(実在企業)」を参照してください。
まとめ:重要ポイントの振り返り
- 段階的調査: AIに段階的な調査フローを依頼
- パターン認識: よくある問題パターンの網羅的チェック
- 定量分析: 推定と検証で原因を絞り込む
- 修正比較: 複数の選択肢のトレードオフを分析
- テスト生成: 修正の妥当性をテストで担保
- 実際の効果: Vol.6 第8章の実在企業データを参照
- 教訓:AIは複雑なデバッグの強力なパートナー、ただし最終判断は人間