AIでレガシーコードをリファクタリングする実践ガイド — テストも型もない現場で「壊さない」ための5ステップ
AIでレガシーコードをリファクタリングする実践ガイド — テストも型もない現場で「壊さない」ための5ステップ
はじめに:「AIに任せたら動かなくなった」はなぜ起きるのか
新規実装の記事は多いが、レガシー改修の記事は少ない
GitHub CopilotやClaude Code、CursorといったAIコーディングツールの活用事例が増え、「AIでコードを書く速度が3倍になった」という報告をよく目にするようになりました。しかしよく読むと、その大半はグリーンフィールド(新規実装)前提の話です。
実務の現場はどうでしょうか。多くのエンジニアが毎日触れているのは、「前任者が書いた、型情報もなく、テストも存在しない、ドキュメントは3年前で止まっているコード」ではないでしょうか。
Zenn・Qiitaを探しても、こうしたレガシーコードへのAI活用を正面から扱った記事はほとんど存在しません。本記事はそのギャップを埋めることを目的としています。
レガシーコードがAIにとって難しい3つの理由
AIがレガシーコードの改修を苦手とする理由は明確です。
① コンテキストが暗黙知に埋もれている
型情報がなく、ドキュメントも失われている状態では、AIは「見えている文字列」だけを手がかりに推論するしかありません。「なぜこの実装になっているか」という背景知識が抜け落ちたまま変更が行われます。
② 正解を判定する手段がない
テストがなければ、変更後のコードが正しいかどうかを自動で検証できません。「コンパイルが通った=正しい」という誤った確信が生まれやすい状況です。
③ 影響範囲が読めない
動的参照(リフレクション、文字列によるジョブ登録)や命名規約の揺れがあるコードベースでは、静的解析だけでは参照関係を完全に把握できません。
本記事で得られること
- 「壊さないための順序」 — ゼロ番目のステップから始める考え方
- コピペで使えるプロンプトとチェックリスト
- 4週間のロードマップによる段階的な進め方
対象ツールはClaude CodeやCursorなどのエージェント型AIです。新規実装・フルリライト・自動マイグレーションは本記事のスコープ外とします。
大前提:リファクタリングの前に「壊れたときの戻し方」を設計する
リファクタリングを始める前に、必ずやるべきことがあります。それは**「最悪のケースから逆算した設計」**です。
変更を3種類に分類する — 可逆 / 補償可能 / 不可逆
すべての変更は、以下の3種類に分類できます。
| 分類 | 内容 | 例 |
|---|---|---|
| Reversible(可逆) | git revert で即座に戻せる |
コードのみの変更 |
| Compensatable(補償可能) | 手順を踏めば復旧できる | 設定変更、キャッシュのクリア |
| Irreversible(不可逆) | 戻せない、または戻すコストが極大 | DBマイグレーション、外部API呼び出し、ファイル削除 |
不可逆な操作は、AIに自動実行させてはいけません。 これはレガシーコードへのAI適用における最重要ルールです。
AIの動作範囲を技術的に制限する4層防御
「暴走したとき、最大でどれだけの被害が出るか」を見積もるのが先決です。エージェント型AIを使う場合、以下の4層で動作範囲を制限することを推奨します。
① allowedTools — AIが使えるツールを最小限に絞る
② permissions.deny — 触らせないパス・ファイルを明示的に指定
③ Hooks / PreToolUse — 実行直前に人間が検査できる仕組みを挟む
④ canUseTool — 最終的な実行可否を判定する最後のゲート
この多層防御の考え方は、Claude Code Agent SDKのワークフロー設計でも推奨されているアプローチです。
【チェックリスト】着手前に確認する7項目
-
git statusがクリーンな状態か(未コミットの変更がないか) - ブランチが保護されており、直接
mainに push できない状態か - 変更対象ファイルのバックアップまたはタグが作成されているか
- 不可逆な操作が含まれていないか確認したか
- AIに触らせてはいけないファイルを
permissions.denyに登録したか - 今回のセッションで変更する論理スコープを1つに絞ったか
- 「このセッションで何があっても戻せるか」を確認したか
STEP 0:依存関係マップを作る — AIに渡すコンテキストの前処理
なぜ「マップを作ってから触る」が鉄則なのか
盲目的にgrepして変更を始めると、必ず参照漏れが発生します。AIは「与えられたコンテキストの中に見えていない依存」を、存在しないものとして扱います。これがレガシーコード改修における最大の落とし穴です。
最初のセッションのルールはシンプルです。コードの変更はゼロ。マップ作成のみ。
このルールを明文化することで、「とりあえずリファクタリングさせてみよう」という誘惑を防ぎます。
AIに渡すべきコンテキスト5要素
コードをAIに投げる前に、以下の5要素を整理してください。
- 目標の明確化 — 何をどう良くしたいか(「可読性向上」ではなく「このメソッドの責務を分離する」のレベルで)
- 変更不可境界 — 触ってはいけないファイル、インターフェース、外部契約
- 入出力サンプル — 実データに近い形でのリクエスト/レスポンス例
- 隣接モジュールのスニペット — 呼び出し元・呼び出し先の実装の一部
- 命名規則・コーディング規約 — プロジェクト固有のルール
CLAUDE.md をコンテキスト圧縮ツールとして使う
毎セッション同じ説明を繰り返すのは非効率です。プロジェクトルートに CLAUDE.md を置くことで、AIへの基本コンテキストを常駐化できます。効果的に使えば、セッションあたり2,000〜5,000トークンの節約が見込めます。
# CLAUDE.md — レガシープロジェクト用テンプレート
## プロジェクト概要
- 言語: Python 3.8(型ヒントほぼなし)
- フレームワーク: Django 2.2(EOL済み)
- テストカバレッジ: 約12%
## 変更禁止エリア
- `payments/` 配下のファイル(外部決済APIとの契約インターフェース)
- `legacy_reports/` — 廃止予定だが現在も参照あり
## 重要な暗黙ルール
- `UserSession` オブジェクトはスレッドセーフではない
- `config/settings_prod.py` は直接編集しない(Ansible管理)
## コーディング規約
- 関数名はスネークケース
- エラーはすべて `AppError` を継承した例外でラップする【コピペ用】依存関係マップ作成プロンプト
以下のコードベースの依存関係マップを作成してください。
コードの変更は一切行わないでください。
出力してほしいもの:
1. モジュール間の依存関係図(テキスト形式)
2. エントリポイント一覧(外部から呼ばれる関数・クラス)
3. 外部I/O一覧(DB、API、ファイル、キャッシュ)
4. [対象ファイル名] が参照しているモジュールと参照されているモジュール
5. 変更した場合に影響が及ぶ可能性があるファイルのリスト
対象: [ファイルパスまたはモジュール名]
STEP 1:テストがない状態で安全網を張る
特性化テスト(Characterization Test)とは
Michael Feathersの著書『レガシーコード改善ガイド』で提唱された手法です。「あるべき動作」を定義するのではなく、「現在の動作」をありのままに記録するテストです。
重要な点は、バグも含めて固定することです。特性化テストは「正しさの証明」ではなく「変更前後の差分検知器」として機能します。
# 特性化テストの例(Pythonの場合)
def test_calculate_tax_characterizes_current_behavior():
"""
WARNING: このテストは現在の挙動を記録したものです。
バグが含まれている可能性があります。
リファクタリング前の「ベースライン」として使用してください。
"""
result = calculate_tax(price=10000, tax_rate=0.08)
# 本来は800のはずだが、現状の実装は800.0001を返す
assert result == 800.0001 # 現在の挙動をそのまま固定ゴールデンマスターテストで大きな出力単位を丸ごと守る
帳票・レポート・APIレスポンスなど、出力が大きくなるケースにはゴールデンマスターテストが有効です。
import json
from pathlib import Path
def test_invoice_generation_golden_master():
"""ゴールデンマスターテスト:請求書生成の出力全体を固定する"""
result = generate_invoice(order_id=12345, _now=datetime(2024, 1, 1))
golden_master_path = Path("tests/golden_masters/invoice_12345.json")
if not golden_master_path.exists():
# 初回実行:ゴールデンマスターを作成
golden_master_path.write_text(json.dumps(result, ensure_ascii=False))
pytest.skip("ゴールデンマスターを作成しました。次回から差分検知が有効になります。")
expected = json.loads(golden_master_path.read_text())
assert result == expected, "出力が変化しています。意図した変更か確認してください。"非決定性(日時・乱数・ID)は引数で注入できる形にリファクタリングするか、モックで固定します。
鉄則:テストはリファクタリング「前」に追加する
これは守られないことが多いルールですが、守るだけで事故率が大幅に下がります。後付けテストは「変更後の挙動」を追認するだけになります。変更前に特性化テストを追加して、その後リファクタリングを行ってください。
AIにテストを生成させるときの注意点
AIにテスト生成を依頼する場合、以下の点に注意が必要です。
- 本番の使用パターンからテストを起こすよう指示する(理想のケースではなく実際の呼び出し方を使う)
- AI生成テストは必ず人間(できれば業務SME)がレビューする
- 「テストが通ること」を目的化させない(テストが間違っている可能性がある)
STEP 2:スモールステップでリファクタリングを進める
1サイクルのテンプレート
リファクタリングの1サイクルは以下の手順で行います。
1. スコープ決定 — 今回変更する論理単位を1つに絞る(5分)
2. プロンプト — AIに変更を依頼する(明確なコンテキストとともに)
3. 差分レビュー — AI提案の差分を人間がレビュー(後述のチェックリスト使用)
4. テスト実行 — 特性化テスト・ゴールデンマスターを実行して差分がないか確認
5. コミット — 1論理変更=1コミット(コミットメッセージにAI指示内容を残す)
「1セッション=1論理変更」を厳守する
バッチ処理で複数の変更をまとめて依頼しないでください。 まとめて変更させると、問題が発生したときにどの変更が原因かを特定するのが困難になります。
個別コミットで粒度を保つことは、git revert による安全な巻き戻しの可能性を保つことでもあります。
# 良い例:論理単位ごとにコミット
git commit -m "refactor: UserValidator.validateEmail メソッドの早期リターンを整理
AI指示内容: 複数のネストされたif文を早期リターンパターンに変換
変更範囲: src/validators/user_validator.py の validateEmail メソッドのみ
特性化テスト確認済み: tests/test_user_validator.py::test_validate_email_*"4週間ロードマップ
| 期間 | 目標 | コード変更 |
|---|---|---|
| Week 1〜2 | 全リポジトリの依存関係マップ作成 | ゼロ |
| Week 3〜4 | 高リスク箇所への特性化テスト追加 → CIへ統合 | テストのみ |
| Month 2〜 | 単一の技術的負債から実リファクタリング開始 | 本番コード |
最初の2週間でコードを1行も変えないことに違和感を覚えるかもしれません。しかしこの投資が、後のリファクタリングを安全に進める土台になります。
どこから手をつけるか — スコープの切り方
最初のリファクタリング対象は、以下の基準で選んでください。
- 変更頻度が高い — よく修正されるファイルほど、改善効果が大きい
- 障害発生率が高い — ホットスポットから先に整理する
- 小さく・独立していて・テストしやすい — 初手は必ずここから
「最も複雑で重要なモジュール」から始めたい気持ちはわかりますが、それは3ヶ月後に経験を積んでからにしてください。
STEP 3:AIの提案をレビューする — 「コンパイルが通る=正しい」ではない
AIは静的コード分析で10〜20%の確率で誤る
実際の調査データとして、AIによる静的コード分析は10〜20%の確率で誤りを含むことが報告されています。特に型情報のないコードベースでは精度がさらに落ちます。「コンパイルが通った」「テストが全部グリーンになった」という結果だけでは、正しく変更できたとは言えません。
「意味的変更(Semantic Change)」という落とし穴
ビルドは成功するのに、実行時の挙動が変わるパターンが存在します。これを「意味的変更」と呼びます。
具体的な例を挙げます。
# Before(リファクタリング前)
def get_user(user_id):
try:
return db.query(User).filter_by(id=user_id).one()
except NoResultFound:
return None # ← None を返すことが呼び出し元の期待値
# After(AIが「改善」した提案)
def get_user(user_id):
return db.query(User).filter_by(id=user_id).first() # ← 例外ハンドリングが消えたfirst() と one() + 例外キャッチの組み合わせは同じように見えますが、NoResultFound 以外の例外の扱いが変わっています。コンパイルは通り、正常系テストも通過しますが、異常系の挙動が変わっています。
レビュー観点チェックリスト(7項目)
AIの提案をレビューする際は、以下の7項目を必ず確認してください。
- 副作用 — 副作用は増えていないか、意図せず減っていないか
- エラーハンドリング — 例外の粒度・種類・握りつぶしが変わっていないか
- 動的参照 — リフレクション、文字列によるジョブ登録などを見落としていないか
- 境界値・異常系 — null/None、空文字、境界値の扱いが変わっていないか
- パフォーマンス — N+1クエリ、計算量の悪化が発生していないか
- ログ・メトリクス — 監視に必要な出力が削除されていないか
- スコープ — 依頼していない範囲の変更が含まれていないか
マルチファイル変更ほど危ない
3〜4ファイルを超える変更では、AIが非自明な参照を見落とすリスクが急増します。Celeryタスク(非同期処理)、DIコンテナの登録、設定ファイル経由での参照などは、静的解析では発見しにくいです。
変更ファイル数を常に意識し、多くなりすぎたらセッションを分割してください。
STEP 4:AIでドキュメント・コメントを補完してコード理解を加速する
コード理解フェーズを「数ヶ月→数日」に短縮する
新しいメンバーがレガシーコードを理解するのに数ヶ月かかるのは珍しくありません。AIを使えば、依存関係・データフロー・業務ロジックの抽出を自動化し、このコスト を大幅に削減できます。
AIに生成させるドキュメントの内容は以下が効果的です。
- 関数の事前条件・事後条件・副作用の説明
- 呼び出しシーケンス図(テキスト形式)
- 「なぜこの実装になっているか」の推定と疑問点の洗い出し
「特定エンジニアの頭の中にしかないロジック」を明文化する
属人化した仕様をAIに言語化させ、関係者にレビューさせて「合意された仕様」に育てるプロセスが重要です。
以下のコードの業務ロジックを説明してください。
「なぜこの実装になっているか」という背景の推定も含め、
不明点は【要確認】としてラベルを付けてください。
対象: [コードスニペット]
ドキュメントをビルド生成物として扱う
静的なドキュメントファイルは、コードが変更されると瞬時に陳腐化します。ドキュメントを成果物ではなくビルド生成物として扱い、コードの変更に追従して自動再生成する運用を目指してください。
注意:AIが書いたコメントを「仕様」と誤認しない
AIはコードから推測してコメントを生成するため、誤った推測が仕様として残るリスクがあります。確度をラベリングして残す運用を推奨します。
def calculate_discount(user_id, order_amount):
"""
ユーザーの割引額を計算する。
[確定] order_amountが0以下の場合は0を返す
[推定] VIPユーザー(tier=3)は固定10%割引と推定されるが要確認
[要確認] order_amountの単位が円か銭かが不明(呼び出し元の実装を確認すること)
"""よくある失敗パターンと対処法
失敗1:いきなり「このコードをリファクタリングして」と投げる
最も多い失敗です。依存関係マップもなく、特性化テストもなく、スコープも曖昧なまま投げると、AIは「見えている範囲で最善と思われる変更」を行います。その変更が副作用を引き起こしても、検知する手段がありません。
対処法: 必ずSTEP 0のマップ作成から始める。
失敗2:AI生成テストを検証せずCIに入れる
AIが生成したテストは一見正しそうに見えますが、誤った前提に基づいていることがあります。業務知識のない状態でテストが書かれると、「間違った挙動を正解として固定する」テストが出来上がります。
対処法: AI生成テストは必ず業務知識を持つ人間がレビューする。
失敗3:まとめて変更させてコミットが巨大化する
10ファイルを一括変更してコミットすると、問題発生時に原因特定と巻き戻しが困難になります。
対処法: 「1セッション=1論理変更=1コミット」を守る。
失敗4:ドキュメント生成とコード変更を同時にやる
2つのことを同時に行うと、どちらかが不完全になります。また、「ドキュメントを生成するためにコードを少し変えた」という意図しない変更が紛れ込みやすくなります。
対処法: ドキュメント生成セッションとリファクタリングセッションを明確に分ける。
失敗5:不可逆な操作をエージェントに自動実行させる
DBマイグレーションの自動実行、外部APIへの書き込み、本番データの削除など、不可逆な操作をAIエージェントに自動実行させることは絶対に避けてください。
対処法: permissions.deny と Hooks/PreToolUse で技術的に制限する。「AIに止められない操作はさせない」を原則とする。
まとめ:AIは「速く書く道具」ではなく「安全に理解する道具」
レガシーコードへのAI活用で最も重要なのは、AIを「実装の加速装置」として使わないことです。AIは「コードベースの理解を助け、変更候補を提示し、ドキュメントを補完する道具」として位置づけると、失敗を大幅に減らせます。
5ステップの再掲
- 大前提: ロールバック設計を先に行い、不可逆操作を明確にする
- STEP 0: 依存関係マップを作成する(コード変更ゼロのセッション)
- STEP 1: 特性化テスト・ゴールデンマスターで安全網を張る
- STEP 2: スモールステップで1論理変更ずつ進める
- STEP 3: 7項目チェックリストでAIの提案をレビューする
- STEP 4: ドキュメント・コメントをAIで補完してコード理解を加速する
今日から始める最初の一歩
今すぐできることは1つだけです。依存関係マップ作成セッションを1回やってみてください。
コードを変更しない、ただマップを作るだけのセッションです。このセッションから得られる情報量に驚くはずです。そして「AIに渡せていなかったコンテキスト」の多さに気づいた時、レガシーコードへのAI活用の正しいアプローチが見えてくるでしょう。
参考文献
- Michael Feathers『レガシーコード改善ガイド』(Working Effectively with Legacy Code)
- Claude Code Agent SDKワークフロー設計(2026年版)
- AIエージェントのRollback設計
関連記事
AIエージェントのループはなぜ止まらないのか — a16zに学ぶ「収束する停止条件」の設計
AIエージェントのループはなぜ止まらないのか — a16zに学ぶ「収束する停止条件」の設計 --- エージェントは「終わり」を知らない 「朝起きたら、昨夜動かしたエージェントがAPIコストを$300分消費していた」——AIエージェントを実務で触り始めたエンジニアなら、こういったヒヤリ体験を一度は経験しているのではないでしょうか。 実はこれ、エージェントが「壊れている」わけではありません。AIモデル...
Pythonパフォーマンス最適化の実践ガイド — 「なんとなく遅い」から「この行が遅い」へ
Pythonパフォーマンス最適化の実践ガイド — 「なんとなく遅い」から「この行が遅い」へ 「なんとなく遅い気がする」という直感を頼りにコードを書き直した結果、実行時間がほとんど変わらなかった——そんな経験はないでしょうか。推測による最適化は、9割の確率でボトルネック以外の場所に手を入れてしまいます。本記事では「計測 → 特定 → 最適化 → 再計測」のサイクルを7つのツールで具体化し、読了後には...
【実装解説】Claudeのメモリを棚卸しするカスタムスキル「memory-inventory」の作り方 ─ CLAUDE.mdが嘘をつき始める前に
【実装解説】Claudeのメモリを棚卸しするカスタムスキル「memory-inventory」の作り方 ─ CLAUDE.mdが嘘をつき始める前に はじめに:あなたのClaudeは、いつから嘘をつくようになったか 「このPRはまだオープンのままですね、注意が必要です」 そう言われてリンクを開いたら、すでにマージ済みだった。 こういう体験、心当たりはありませんか? 大丈夫、私も最初は「気のせいかな」...
メルカリの「AI-Native開発」とは?ツール導入で終わらせない組織変革の実践知
メルカリの「AI-Native開発」とは?ツール導入で終わらせない組織変革の実践知 「AIツールは配ったが、使う人と使わない人に分かれてしまった」——多くの開発組織が直面する壁です。メルカリはこれを、ツールではなくプロセスと組織の再設計として解いています。本記事では公開された実践事例をもとに、AI-Native開発の中身を具体的に読み解きます。 --- AI-Native開発とは何か — 「AIツ...