「動いているコードには触るな」。
長年、業務システムの現場で絶対のルールとして守られてきたこの言葉が、今、システムの進化を阻む要因となっています。Java 8以前で構築された基幹システムでは、開発当時のエンジニアが不在となり、仕様書も更新されないまま、複雑に絡み合ったコード(スパゲッティコード)が残されているケースが少なくありません。
実務の現場では、「最新化(モダナイゼーション)の必要性は理解しているが、システムが壊れるリスクが怖くて手を出せない」という声がよく聞かれます。人手不足が深刻な中、膨大な古いコード(レガシーコード)を読み解き、テストを書き、内部構造を整理(リファクタリング)する時間的な余裕がないのが現実です。
しかし、生成AI(大規模言語モデル:LLM)の登場により、この状況は劇的に変化しつつあります。
本記事では、生成AIを活用して古いJavaコードを安全に最新のJava 21へ移行する実践的な手法を、具体的な指示文(プロンプト)やコード例とともに論理的に解説します。技術的な負債を解消し、システムを「進化できる状態」へと戻すための実証的なアプローチとしてお役立てください。
なぜ「LLM×リファクタリング」がレガシーJavaの救世主となるのか
従来のリファクタリングツールや開発環境(IDE)の自動変換と生成AIの決定的な違いは、AIが「コードの意図(システムが何をしたいのか)」を深く理解しようとする点にあります。単なる文法の置き換えにとどまらず、システムが果たすべき役割を読み解くアプローチは、古いシステムから脱却するための強力な武器となります。
人手による移行とAI活用の工数比較データ
大規模な古いシステムの移行において、作業時間の削減は最大の課題です。例えば、10万行規模のJava 6ベースのシステムを最新のJava 21へ移行するシナリオを想定してみましょう。
人手によるコードの解析、テストケースの作成、そして実際の移行作業には膨大なリソースと時間が必要です。しかし、AIを組み込んだ最新の開発フローを導入することで、劇的な効率化が期待できます。業界の報告やモデルケースの試算によれば、従来の手法と比較して作業工数を約1/3から1/4程度まで圧縮できる可能性が示唆されています。
ここで重要になるのが、適切なAIモデルの選定です。古いコードの複雑な依存関係や業務ルール(ビジネスロジック)を正確に読み解くためには、推論能力や文脈の理解力が大幅に向上した最新モデルの活用が推奨されます。
特に効果が顕著に表れるのが「テストコードの自動生成」です。仕様書が存在しない、あるいは完全に古くなってしまったコードに対し、人間が一つひとつの処理を読み解き、テストケースを網羅的に作成するのは極めて困難です。最新のAIモデルを活用することで、膨大な文脈を一度に処理し、この工程の作業時間を大幅に削減できるケースが多く報告されています。これは単なるコスト削減の枠を超え、「人間には不可能なスピードと網羅性で、安全にコードを変更するための網を構築できる」という決定的な優位性を意味します。
「変換」ではなく「理解」させるプロンプトエンジニアリング
開発環境の標準機能でも、古い for ループをJava 8以降の Stream API(データ処理を簡潔に書く機能)に変換することは可能です。しかし、元のコードにおいて変数の名前が不適切であったり、無駄な処理が含まれていたりする場合、ツールはそのまま「新しい書き方をしただけの悪いコード」に変換してしまいます。
これに対し、生成AIを用いたアプローチでは、指示文(プロンプト)を通じて「この処理の業務ルールを要約せよ」「変数 x を業務内容に基づいた適切な名前に変更せよ」と指示することで、読みやすさと保守のしやすさを伴った真のコード改善が可能になります。最新モデルへの刷新により、複雑な指示への追従性や、コードの意図を構造化して理解する能力が一段と向上しました。これにより、単なる文法変換ではない、業務ルールの深層を汲み取ったコード改善が期待できます。
最新モデルでは、用途に応じて複数の思考モードを使い分けることが可能ですが、コード改善の業務においては、正確性と一貫性を担保するための厳密な指示文の設計が成功の鍵を握ります。モデルの仕様や最適な活用法は頻繁に更新されるため、公式ドキュメントで最新情報を定期的に確認し、仮説検証を繰り返す習慣を取り入れることをお勧めします。
品質指標(Metrics)で見るBefore/After
コード改善の成果を評価する際、客観的な指標として「サイクロマティック複雑度(コード内の分岐の多さを示す指標)」に着目すると、AI導入の効果は非常に明確になります。一般的に、この複雑度が10を超えるとバグの発生リスクが急激に高まるとされていますが、長年運用されてきた古いコードの現場では、複雑度が50を超えるような巨大な処理ブロックに遭遇することも珍しくありません。
推論能力に優れた最新の思考型モデルを用いて、適切な処理の分割や複雑な条件分岐の整理を指示することで、異常に高い複雑度を持つコードを健全な水準(例えば15以下など)まで確実に低下させることが実証されています。この複雑度の低下は、コードが読みやすくなるだけでなく、将来的なテスト作業や保守コストの大幅な削減を論理的に示しています。AIは単にコードを短く記述するツールではなく、ソフトウェアの論理構造を根底から整理し直すための強力なパートナーとして機能します。
ベストプラクティス原則:Safety First(安全性第一)の戦略
AIは強力ですが万能ではありません。「ハルシネーション(もっともらしい嘘)」のリスクがあるため、生成されたコードを盲信するのは危険です。そこで、Safety First(安全性第一)という3つの原則を提唱します。
原則1:テストなきコードにAIを触れさせるな
これが最も重要なルールです。テストコードがないプログラムに「コードをきれいにして」とAIに頼んではいけません。元の挙動が正しいか分からない状態でコードを変更すれば、改悪(デグレード)に気づけないからです。
まず行うべきは、「仕様化テスト(Characterization Test)」の生成です。これは「現在のコードがどう動いているか」を記録するテストです。バグであっても、まずは「現在の挙動」としてテストに落とし込み、挙動を固定化(ピン留め)します。
原則2:一度にすべてを変えず、レイヤーごとに攻略せよ
システム全体を一度にAIに投げると、文脈が不足し整合性が取れなくなります。以下の順序で、階層(レイヤー)ごとに攻略することを推奨します。
- 共通処理(Util系) / 値オブジェクト: 他のプログラムへの依存が少なく処理が独立しているため、AIが最も扱いやすい部分です。
- ドメインロジック: 業務ルールが集約されている部分です。ここを重点的にテストで保護します。
- サービス層 / アプリケーション層: 複数の業務ルールを跨ぐ処理です。データ保存の確定(トランザクション管理)などが絡むため慎重に進めます。
データベース接続などのインフラ層は、土台となる枠組み(フレームワーク)の変更を伴うことが多いため、単純なコードの整理とは切り離して考える必要があります。
原則3:人間は「レビュアー」ではなく「ゲートキーパー」であれ
AIが生成したコードを人間が確認する際、「まぁ動くだろう」と軽く承認してはいけません。人間は、AIが生成したコードが本番環境に入るのを阻止する「ゲートキーパー(門番)」の役割を持つべきです。
具体的には、以下のチェックリストを通過しない限りシステムに組み込まないという厳格なルールを設けます。
- AIが生成したテストがすべて成功(パス)するか?
- 自動解析ツール(SonarQubeなど)で新たな警告が出ていないか?
- セキュリティの検査で脆弱性が検出されていないか?
実践フェーズ1:LLMによる「仕様化テスト」の自動生成
具体的な実践手順に入ります。まずは、仕様書のない古いコード(ブラックボックス)に対し、生成AIを使ってテストコードを生成させます。
仕様書がないコードから振る舞いを抽出するプロンプト
以下のような、複雑な条件分岐を持つ古いJavaの処理があるとします。
// LegacyOrderService.java (Java 7)
public int calculateDiscount(String type, int amount, int years) {
int discount = 0;
if (type != null && type.equals("VIP")) {
if (years > 5) {
discount = 20;
} else {
discount = 10;
}
} else if (type != null && type.equals("MEMBER")) {
if (amount > 10000) {
discount = 5;
}
}
return discount;
}
このコードに対して、以下の指示文(プロンプト)を使用してテストケースを生成させます。
【プロンプト例】
あなたは品質保証(QA)エンジニアの専門家です。以下のJavaメソッド
calculateDiscountは、仕様書が存在しない古いコードです。
このコードの現在の挙動を完全に網羅するJUnit 5のテストプログラムを作成してください。要件:
- 境界値分析を行い、条件の分かれ目となる値(エッジケース)を網羅すること。
- データが空(
null)の場合の挙動も含めること。- テストの名前は日本語で、何を確認するテストか明確にすること(例:
VIP会員かつ5年超の場合は割引率20であること)。- 外部システムを模倣するツール(Mockitoなど)は必要であれば使用し、基本は結果の検証(アサーション)を中心に記述すること。
対象コード:
[ここにコードを貼り付け]
JUnit 5 + Mockitoによるテストケース生成の実例
AIはこの指示文から、以下のような網羅的なテストコードを生成します。
// GeneratedTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class LegacyOrderServiceTest {
private final LegacyOrderService service = new LegacyOrderService();
@Test
void VIP会員かつ5年超の場合は割引率20であること() {
assertEquals(20, service.calculateDiscount("VIP", 5000, 6));
}
@Test
void VIP会員かつちょうど5年の場合は割引率10であること() {
assertEquals(10, service.calculateDiscount("VIP", 5000, 5));
}
@Test
void MEMBER会員かつ購入額10000超の場合は割引率5であること() {
assertEquals(5, service.calculateDiscount("MEMBER", 10001, 1));
}
@Test
void MEMBER会員かつ購入額10000以下の場合は割引率0であること() {
assertEquals(0, service.calculateDiscount("MEMBER", 10000, 1));
}
@Test
void 会員タイプがnullの場合は割引率0であること() {
assertEquals(0, service.calculateDiscount(null, 10000, 1));
}
// ...他ケース省略
}
このように、人間が見落としがちな「境界値(5年と6年、10000と10001)」や「空データのチェック」も含めてテストケースを生成してくれます。
カバレッジ率を60%まで引き上げるための反復プロセス
一度の生成で完璧なテストができるとは限りません。生成されたテストを実行し、コードの網羅率(カバレッジレポート)を確認します。網羅率が低い(テストされていない分岐がある)場合は、その情報をAIにフィードバックし、仮説検証を繰り返します。
「生成されたテストを実行しましたが、
else ifの条件分岐のテストが不足しています。以下の条件を満たすテストケースを追加してください...」
このように対話的にテストを拡充し、対象プログラムの網羅率を少なくとも60%〜80%まで引き上げてから次のステップへ進みます。
実践フェーズ2:モダンJava(Java 17/21)への構文変換
テストによる安全網が確保できたら、コードの最新化(モダナイゼーション)を行います。Java 8以降の機能を活用し、読みやすさと保守のしやすさを劇的に向上させます。
命令型から宣言型へ:Stream APIへの安全な書き換え
古いコードで最も多いのが、複雑な for ループによる集計処理です。
【Before: Java 7 style】
List<String> highValueCustomers = new ArrayList<>();
for (Customer c : customers) {
if (c.getTotalSpent() > 10000 && c.isActive()) {
highValueCustomers.add(c.getName().toUpperCase());
}
}
これをAIに整理(リファクタリング)させます。
【プロンプト例】
以下のJavaコードをJava 21の最新の構文(Stream API)を用いてリファクタリングしてください。
変数名はより処理内容が分かるものに変更し、直接記述された数値(10000)は定数として定義してください。
【After: Java 21 style】
private static final int HIGH_VALUE_THRESHOLD = 10000;
List<String> highValueCustomerNames = customers.stream()
.filter(Customer::isActive)
.filter(c -> c.getTotalSpent() > HIGH_VALUE_THRESHOLD)
.map(c -> c.getName().toUpperCase())
.toList(); // Java 16+ の toList() を使用
古い書き方ではなく、Java 16で導入された簡潔な .toList() を使用している点に注目してください。AIに「最新の長期サポート(LTS)バージョンの構文を使うこと」と指定すれば、こうした最新機能も適用してくれます。
ボイラープレートの削除:LombokからRecord型への移行
Java 14(正式には16)で導入された record という機能は、データ保持用のクラスを劇的に簡素化します。
【Before: 従来の書き方】
public class UserDTO {
private final String name;
private final int age;
public UserDTO(String name, int age) {
this.name = name;
this.age = age;
}
// データを取得するメソッド(getter)などが続く...
}
【After: Java Record】
public record UserDTO(String name, int age) {}
AIには「データ保持用のクラスを検出し、可能な限り record 型に変換してください」と指示します。これにより、数千行規模の定型的なコード(ボイラープレート)を削除でき、プログラム全体の見通しが良くなります。
Switch式とパターンマッチングによる可読性向上
Java 21の目玉機能の一つが、Switch式とパターンマッチングです。複雑な if-else や型の判定処理を美しく書き換えることができます。
【Before: 複雑な型判定】
Object obj = getMessage();
String result = "";
if (obj instanceof String) {
result = ((String) obj).toLowerCase();
} else if (obj instanceof Integer) {
result = String.format("Number: %d", obj);
} else {
result = "Unknown";
}
【After: Java 21 Pattern Matching for Switch】
String result = switch (getMessage()) {
case String s -> s.toLowerCase();
case Integer i -> "Number: %d".formatted(i);
default -> "Unknown";
};
この変換は読みやすさを高めるだけでなく、コンパイラによる網羅性チェック(全パターンをカバーしているか)の恩恵を受けられるため、バグの混入を防ぐ効果もあります。
実践フェーズ3:ドキュメンテーションとナレッジの継承
コードがきれいになっても、変更理由が分からなければ、特定の担当者しか理解できない状態(属人化)を再び生むだけです。コード改善の過程で得られた知見を文書化するのもAIの役割です。
JavaDocの自動生成と更新
整理された後のコードに対して、AIに説明文(JavaDoc)を生成させます。
「以下の処理の説明文(JavaDoc)を生成してください。引数、戻り値、エラー処理だけでなく、処理の目的と、以前のコードからの主な変更点(例:Stream APIへの置換)も
@implNoteとして記載してください。」
「なぜこう変更したか」を残すアーキテクチャデシジョンレコード(ADR)
重要な設計変更を行った場合、その決定の背景を記録するADR(Architecture Decision Record)を残すことが推奨されます。これもAIに下書きさせることができます。
「今回のコード改善で、データ保持クラスを従来の手法からJavaのRecord型に変更しました。この意思決定の背景(データが変更されないことの担保、標準機能への準拠)、メリット、デメリットを論理的に整理して、ADR形式の文書を作成してください。」
属人化を解消するためのAI解説コメントの付与
特に難解な業務ルールが含まれる箇所には、AIに「平易な言葉による解説コメント」を追加させます。これは将来、新しいエンジニアがコードを読む際のガイドとなります。コード自体は英語でも、コメントは日本語で丁寧に記述させることで、チーム全体の理解度を底上げします。
アンチパターン:LLMリファクタリングの落とし穴
ここまでメリットを強調してきましたが、実証データに基づくと、注意すべき落とし穴(アンチパターン)も存在します。
コンテキスト長超過によるロジックの断絶
巨大なプログラム(例:3000行以上の巨大クラス)を一度にAIに入力すると、AIが一度に処理できる情報量(コンテキストウィンドウ)を超過し、コードの途中が省略されたり、処理が勝手に要約されたりすることがあります。
対策: 巨大なプログラムはまず小さな処理単位に分割し、少しずつコード改善を行ってください。AIに「論理的な分割案」を出させるのも有効なアプローチです。
セキュリティ脆弱性の混入リスク
AIは学習データに含まれる「古い慣習」を再現することがあります。例えば、データベースへの問い合わせ(SQL)の構築において、安全な手法を使わずに文字列を直接結合してしまうコードを生成する可能性があります。
対策: 生成されたコードは必ず自動解析ツール(SonarQube、Checkmarxなど)を通し、セキュリティ上の弱点がないか機械的かつ客観的にチェックしてください。
過度な抽象化によるデバッグ困難化
AIは時に「賢すぎるコード」を書きたがります。例えば、複雑な処理を何重にも入れ子にしたり、高度な機能を多用したりして、一見短いが人間には解読不能なコードを生成することがあります。
対策: 指示文に「シンプルさを保つ原則(KISS原則)に従い、読みやすさを最優先してください。過度に短い一行のコード(ワンライナー)は避けてください」と明記しましょう。
まとめ
古いコードの整理(リファクタリング)は、かつては「パンドラの箱」を開けるような恐怖を伴う作業でした。しかし、生成AIという強力なパートナーと、テストを基盤とする安全なアプローチを組み合わせることで、それは「制御可能なエンジニアリングタスク」へと変わります。
重要なのは、「テストで挙動を固定化する」→「AIで安全に変換する」→「人間がゲートキーパーとして検証する」という仮説検証のプロセスを遵守することです。このサイクルを回すことで、技術的な負債は徐々に解消され、システムは再びビジネスの成長を支える資産へと生まれ変わります。
もし、開発現場で「どこから手をつければいいか分からない」と立ち止まっているなら、まずはたった一つの処理のテスト生成から始めてみてください。その小さな一歩が、システムの未来を変える大きな転換点になるはずです。
コメント