はじめに
本記事は筆者がJJUG CCC 2025 Fallにて発表した内容をQiita記事にまとめたものとなります。
発表時のスライド資料はSpeakerDeckにアップロードしているため、こちらをご覧ください。
JJUG CCC
Javaの技術向上・発展等を目的とした非営利団体「JJUG」(Japan Java User Group)が主催するカンファレンスイベントです。
詳細については以下のサイトをご覧ください。
https://www.java-users.jp/page/about/
背景:生成AIがソフトウェア開発の現場で活用されつつある
生成AIは、文章作成や文書要約などの業務の場面からウェブ検索まで様々な場面で利用されるようになりました。
IT分野だと、ソフトウェア開発においてコーディング作業を実施してもらうケースが増えています。
具体的には、V字モデルの詳細設計~単体テスト工程といった実装フェーズではAIエージェントによる自律的な開発を導入する事例は社内でも増えています。

V字型モデル(IPAが公開している「ソフトウェアテスト見積りガイドブック」より引用)
AIを用いた開発の手法については近年いろいろなものが提案されており、代表的なものとしては以下のようなものがあります。
- バイブコーディング:開発者が細かな実装を逐一指示するのではなく、目的や雰囲気(バイブ)を自然言語でAIに伝え、AIがそれを解釈してコード生成や設計を行う手法です。試作やアイデア検証を高速に進められる点が特徴です。
- 仕様書駆動開発:先に仕様書や要件定義を明確に記述し、それを入力としてAIに設計・実装・テストコードの生成を行わせる手法です。要件の明確化と成果物の一貫性を重視し、大規模開発や品質管理が求められる場面に適しています。
背景:生成AIがコーディングした成果物をレビューするのは負荷が高い
AIをソフトウェアの実装に利用するシーンは増えており、それと同時に人間がレビューする負担が高まってきていると筆者は考えます。
具体的には以下のような背景があります。
単純な処理の実装はAIに任せられる反面、責任が重い判断は人間側に残る
AIによるコード生成は、CRUD処理のような定型的かつ構造が明確な実装において特に効果を発揮します。
このような処理では、要求仕様と実装内容の対応関係が比較的単純であるため、AIが生成したコードもレビュアーや設計者の意図から大きく逸脱することが少ないです。
そのため、一定の前提条件が共有されていれば、レビュー負荷も比較的軽く抑えられます。
一方で、業務ロジックが複雑であったり、障害時の影響範囲が大きい処理だと話が変わってきます。
こうした処理では、コードそのものだけでなく、業務背景や暗黙知、将来の拡張性まで含めた判断が求められます。
しかし、AIはそのような文脈や責任の重さを十分に理解できるわけではないため、最終的な妥当性判断は人間が担う必要があります。
その結果、AIが実装を担えば担うほど、人間の仕事は「書く」作業から「判断し、責任を持つ」作業へと偏っていく構造が生まれます。
AIが短時間で大量のコードを生成するようになりそれらをすべてレビューする必要がある
AIは短時間で大量のコードを生成できるため、表面的には開発速度が大きく向上したように見えます。
しかし、その生成物をそのまま採用できるわけではなく、すべてのコードに対して人間によるレビューが必要になります。
特に、AIはプロジェクト固有の背景知識や過去の経緯を十分に理解していないため、意図せず設計方針から外れた実装を行う可能性があります。
従来、経験豊富なエンジニアが実装を担当する場合、案件特有の事情や暗黙の前提をある程度信頼して任せることができました。
しかし、AIが実装を行う場合、そのような信頼の前提は成立しません。
その結果、成果物に対する最終的な責任はすべて人間側が負うことになり、レビュー時には細部まで確認せざるを得なくなります。
さらに、開発規模が大きくなるほどソフトウェアの複雑度は増し、レビュー対象となるコード量も増加します。
AIによって生成速度だけが加速すると、レビュー工程がボトルネックとなり、人間の負担はむしろ増大します。
AI導入による開発生産性向上が、そのまま人間の作業負荷軽減につながらない一因となっています。
課題:生成AIにコーディングをさせると、レビューしづらいコードが生成されることがある
生成AIに要件や要望を入力すると、それに対応するコードを即座に生成してくれます。
この利便性から、実装作業そのものを人間の代わりにAIに任せるケースも増えてきました。
しかし、生成されたコードをレビューする立場に立ったとき、そのコードは必ずしも人間にとってレビューしやすい形になっているとは限りません。
実際に何度かプロンプトを変えてコード生成を試してみると、「動作としては正しそうだが、読み解くのがつらいコード」が出力されることがあります。
その典型的な特徴として、複数の責務が1つのメソッドやクラスに集約されている点が挙げられます。
入力値の検証、業務ロジックの判断、データの永続化、例外処理といった処理がひとまとめになっており、コード全体を追わなければ何をしているのか分からない状態です。
このようなコードは、レビュー時にどこまでが一つの責務か判断しづらかったり内容を理解することに時間がかかったりなどの問題を引き起こします。
結果として、レビュアーは本来注目すべき設計判断や業務要件の妥当性ではなく、コードの読み解きに時間を割くことになります。
実際に、生成AIに要件を与えて作成させたコードの一例が以下です。
複数の機能が密結合しておりレビューしやすいコードとは言えません。
生成AIが出力したメソッド
@Override
public void actionPerformed(ActionEvent e) {
// update enemies
for (Enemy en : enemies) {
en.update();
}
// physics and input update
// horizontal control
if (left) player.vx -= accel;
if (right) player.vx += accel;
// apply friction
player.vx *= friction;
if (player.vx > maxSpeed) player.vx = maxSpeed;
if (player.vx < -maxSpeed) player.vx = -maxSpeed;
// gravity
player.vy += gravity;
// jump (only when on ground)
if (jump && player.onGround) {
player.vy = jumpImpulse;
player.onGround = false;
}
// horizontal move
player.x += player.vx;
// simple camera: follow player with offset
int followX = 200; // how many px from left before camera moves
if (player.x - cameraX > followX) {
cameraX = (int) (player.x - followX);
}
if (cameraX < 0) cameraX = 0;
if (cameraX > levelWidth - WIDTH) cameraX = levelWidth - WIDTH;
// vertical move
player.y += player.vy;
// Collision detection & resolution with blocks (AABB)
player.onGround = false;
Rectangle pBounds = player.getBounds();
for (Block b : blocks) {
Rectangle br = b.getBounds();
if (!pBounds.intersects(br)) continue;
Rectangle inter = pBounds.intersection(br);
if (inter.isEmpty()) continue;
// resolve the smallest overlap
if (inter.width < inter.height) {
// horizontal collision: push player left/right
if (pBounds.getCenterX() < br.getCenterX()) {
// collided from left
player.x -= inter.width;
player.vx = 0;
} else {
// collided from right
player.x += inter.width;
player.vx = 0;
}
} else {
// vertical collision: push up or down
if (pBounds.getCenterY() < br.getCenterY()) {
// landing on top
player.y -= inter.height;
player.vy = 0;
player.onGround = true;
} else {
// hit from below
player.y += inter.height;
player.vy = 0;
}
}
pBounds = player.getBounds(); // update bounds after resolving
}
// Check item collection
Iterator<Item> itIt = items.iterator();
while (itIt.hasNext()) {
Item it = itIt.next();
if (pBounds.intersects(it.getBounds())) {
score += it.value;
itIt.remove();
}
}
// Check enemy collisions
for (Enemy en : enemies) {
if (pBounds.intersects(en.getBounds())) {
// game over: reset player and score
player.x = 100;
player.y = HEIGHT - 60;
player.vx = 0;
player.vy = 0;
cameraX = 0;
score = 0;
break; // avoid multiple resets
}
}
// Keep player within world
if (player.x < 0) { player.x = 0; player.vx = 0; }
if (player.x > levelWidth) { player.x = levelWidth; player.vx = 0; }
if (player.y > HEIGHT - 20) { player.y = HEIGHT - 20; player.vy = 0; player.onGround = true; }
repaint();
}
実現したいこと:もっと人間のレビュー負荷が低くなるようにしたい
AIによるコード生成を開発現場に導入すると、成果物の品質を担保するためにどうしてもレビュー工数を割く必要があります。
そのため、レビューの手間を減らすためのアプローチを検討する必要があります。
先ほどのコードを思い出してください。
AIが生成するコードの中には、長すぎるメソッドやマジックナンバーの多用など、適切でなさそうな記述や保守性・可読性に問題のあるコードを含む場合があります。
このようなコードをそのままレビューに回してしまうと、レビュー効率が低下してしまいます。
したがって、人間がレビューする前の段階で、コードの質を担保できる仕組みを作ることが重要です。
アプローチ:「レビューしづらさ」をメトリクスに基づいて検知し、検知した箇所をAIで修正してもらう
アーキテクチャの説明にあたり、いくつかの事前知識を説明します。
前提知識:リファクタリング
リファクタリングとは、外部から見た振る舞いを変えずに、ソースコードの内部構造を改善することを指します。
機能追加や仕様変更とは異なり、あくまで「動作は同じだが、より理解しやすく変更しやすいコードにする」ことが目的です。
代表的なリファクタリングの例としては、以下のようなものがあります。
- メソッドが肥大化している場合に処理を分割する
- 責務が曖昧なクラスを役割ごとに分離する
- 意図が分かりづらい変数名やメソッド名を改善する
- 重複したロジックを共通化する
これらは一見すると些細な変更に見えますが、本工程を継続的に実施することでソフトウェアの可読性や保守性に大きな差が生まれ、技術的負債を軽減することに繋がります。
生成AIが出力するコードは「人間が与えた仕様通りに動くこと」を最優先にしているため、構造的な整理や将来の変更容易性までは十分に考慮されていないケースが多く見られます。
そのため、AIが生成したコードに対してリファクタリングを施すことは、長期的な品質を保つための重要な工程になります。
前提知識:コードスメル
コードスメルとは、コード中に現れる「設計や保守性に問題が潜んでいそうな兆候」のことを指します。
これはバグそのものではありませんが、放置すると将来的に不具合や改修コスト増大につながりやすい状態を示すサインです。
代表的なコードスメルには、次のようなものがあります。
- 1つのメソッドが長すぎて何をしているか分からない
- 条件分岐が複雑にネストしている
- 似たようなコードがあちこちにコピーされている
- クラスやメソッドの責務が不明確
これらのコードスメルは、人間が書いたコードだけでなく、生成AIが出力したコードにも頻繁に現れます(実際にAIで生成したコードを対象に分析をかけた結果を後ほど紹介します)。
本記事では、コードスメルを手がかりとして問題点を洗い出し、適切なリファクタリングによって改善していきます。
アプローチの具体的な説明
AIエージェントを利用してコードを生成
最初に、コーディングを実施するAIに開発するソフトウェアの要件などを定義したプロンプトを与えて、その要件を満たすようなプログラムを実装してもらいます。
与えるプロンプトについては以下のようなものとなります。
要件:
- 環境:Java 17、Swingベース(外部ゲームエンジンは使用しない)、Maven
- 視点:真上(トップダウン)
- 操作:キーボード(←→で旋回、↑で加速、↓でブレーキ/後退)
- 物理:簡易な慣性(速度、加速度、旋回による向き変化)。衝突は簡単な反発で可。
- コース:鈴鹿サーキットの構造を想定。トラック(路面)とオフロードを識別し、オフロードでは最大速度が低下する(例:路面100%、オフロード50%)。
- ラップ計測:スタート/ゴールライン通過でラップ開始・終了。現在ラップ時間、ラップ履歴、ベストラップを表示。
- HUD:スピードメーター(数値可)、現在ラップタイム、ベストラップ、ラップ数(例:1/3)を常時表示。
- 画面サイズ:デフォルト800×600。ウィンドウリサイズ対応で表示を保つ。
- パッケージ:実行可能なJAR、README(実行方法と操作方法)、ソース(Maven/Gradleプロジェクト推奨)。
納品物:
- runnable.jar
- ソース(リポジトリ構成)
- README(実行・ビルド手順、操作説明)
静的分析ツールでソースコードのコードスメルを解析
生成されたソースコードに対してコードスメルの検出を実行します。
この時、検出用の静的分析ツールとして SonarQube1 を利用しています。
コードスメルと生成されたソースコードをインプットとしてリファクタリングを実施
最後に、得られたコードスメルの情報を踏まえてコードのリファクタリングを実行します。
具体的には、AIに対してコードスメルの情報を与えてそれらを修正するようプロンプトで命令します。
その際のプロンプトは以下の通りです。
※プロンプトではアクセストークンを渡す運用となっていますが、実際にはアクセストークンを直接プロンプトで指定しないような工夫が必要になりそうです。
以下のWebAPIは<プロジェクト名>のコードスメルを取得するためのものです。
<呼び出すAPIの情報>
また、アクセストークンは以下となります。
<アクセストークンの情報>
このWebAPIを使って、コードスメル情報をJSON形式で取得してください。
取得したコードスメル情報に基づいてリファクタリングを行ってください。
リファクタリング完了後、以下のコマンドをコマンドプロンプト上で実行してソースコードの変更結果をSonarQubeへ同期してください。
<SonarQubeに情報を同期するためのコマンド>
上記処理を繰り返し実行し、JSONの"total"の値が5件未満になるようにしてください。
実験実施
アーキテクチャの有効性を確認するために実験を実施します。
最初に、以下のサイトを参照していくつかのゲームアプリを作りました。
https://nowokay.hatenablog.com/entry/2025/09/25/194808
アプリではJavaのSwingライブラリを活用しています。Swingはドラッグやタイトル表示、メニューバーなど多くのGUI機能を提供する軽量オブジェクトであり、このモジュール単独でアプリを構築可能です。
作ったアプリは以下の通りです。
| ゲーム名 | ステップ数 | クラス数 | 概要 |
|---|---|---|---|
| レーシングゲーム | 436 | 4 | 2Dでキーボード操作 |
| 2Dアクションゲーム | 619 | 7 | ステージを進んでアイテム収集したり敵を倒したりする |
| シューティングゲーム | 2,728 | 10 | インベーダーゲームのようにNPCと戦う |
SonarQubeで検出されたコードスメルは以下となります。
なお、本実験ではコードスメルのうち重要度が高いもの(MEDIUM、HIGH、BLOCKER)を対象としております。
| 種類 | 検出数 | 重要度 |
|---|---|---|
| 非staticな変数をtransient または serializable にする | 26 | HIGH |
| 空メソッドにコメント追加、実装を完了させる | 8 | HIGH |
| メソッドの認知的複雑度を下げる | 6 | HIGH |
| 「?(文字列)」を static でアクセス | 5 | HIGH |
| 未使用のプライベートフィールドを削除 | 4 | MEDIUM |
| コメントアウトされたコードを削除 | 2 | MEDIUM |
| ネストした三項演算子を独立文にする | 2 | MEDIUM |
| System.out をロガーに置き換える | 2 | MEDIUM |
| リテラル「?(文字列)」の重複を定数化 | 2 | HIGH |
| switch に default ケースを追加 | 2 | HIGH |
| 不要な変数代入を削除 | 1 | MEDIUM |
| パラメータ数が多すぎるコンストラクタ | 1 | MEDIUM |
| 暗黙的 public コンストラクタを private にする | 1 | MEDIUM |
| 未使用のメソッドパラメータを削除 | 1 | MEDIUM |
| 「?(文字列)」はフィールド名 | 1 | BLOCKER |
実験結果
実験の結果、上記のコードスメルについてリファクタリングに成功していることを確認できました。
変更内容を目視で確認したところ、直感から大きく乖離しておらずapproveできるものでした。
検出されていたコードスメルはいずれも該当箇所を部分的に変更することで改善できるものだったため、AIが自動修正可能なものだったことが要因かと思われます。
実際に変更された例をいくつかお見せします。
非staticな変数をtransient または serializable にする
このリファクタリングでは、シリアライズの対象外の変数に明示的にtransient修飾子を付与する修正となります。
public class GamePanel extends JPanel implements ActionListener, KeyListener {
public static final int WIDTH = 900;
public static final int HEIGHT = 480;
private final Timer timer;
- private final Player player;
+ private final transient Player player;
...
}
空メソッドにコメント追加、実装を完了させる
このリファクタリングでは、具象クラスにてオーバーライドした空メソッドを作成している場合に当該メソッドの呼び出しを想定しないことを明示するためのリファクタリングです。
下記の場合、keyTypedメソッドは具象クラスのため処理をしないメソッドをOverrideしていますが、実際に呼び出されることを開発者側は意図していません。
そのため、呼び出しが発生した際に「サポートしていないメソッド」というメッセージをUnsupportedOperationExceptionをスローするようAIが修正しました。
@Override
public void keyTyped(KeyEvent e) {
+ throw new UnsupportedOperationException("keyTyped is not supported in this implementation");
}
メソッドの認知的複雑度を下げる
このコードスメルは、メソッドの可読性が低い際に出力されるものです。
具体的には、先述のような責務の集まった長いメソッドが該当します。
修正前後のコードをいかに示します。
生成AIが出力したリファクタリング前のメソッド
@Override
public void actionPerformed(ActionEvent e) {
// update enemies
for (Enemy en : enemies) {
en.update();
}
// physics and input update
// horizontal control
if (left) player.vx -= accel;
if (right) player.vx += accel;
// apply friction
player.vx *= friction;
if (player.vx > maxSpeed) player.vx = maxSpeed;
if (player.vx < -maxSpeed) player.vx = -maxSpeed;
// gravity
player.vy += gravity;
// jump (only when on ground)
if (jump && player.onGround) {
player.vy = jumpImpulse;
player.onGround = false;
}
// horizontal move
player.x += player.vx;
// simple camera: follow player with offset
int followX = 200; // how many px from left before camera moves
if (player.x - cameraX > followX) {
cameraX = (int) (player.x - followX);
}
if (cameraX < 0) cameraX = 0;
if (cameraX > levelWidth - WIDTH) cameraX = levelWidth - WIDTH;
// vertical move
player.y += player.vy;
// Collision detection & resolution with blocks (AABB)
player.onGround = false;
Rectangle pBounds = player.getBounds();
for (Block b : blocks) {
Rectangle br = b.getBounds();
if (!pBounds.intersects(br)) continue;
Rectangle inter = pBounds.intersection(br);
if (inter.isEmpty()) continue;
// resolve the smallest overlap
if (inter.width < inter.height) {
// horizontal collision: push player left/right
if (pBounds.getCenterX() < br.getCenterX()) {
// collided from left
player.x -= inter.width;
player.vx = 0;
} else {
// collided from right
player.x += inter.width;
player.vx = 0;
}
} else {
// vertical collision: push up or down
if (pBounds.getCenterY() < br.getCenterY()) {
// landing on top
player.y -= inter.height;
player.vy = 0;
player.onGround = true;
} else {
// hit from below
player.y += inter.height;
player.vy = 0;
}
}
pBounds = player.getBounds(); // update bounds after resolving
}
// Check item collection
Iterator<Item> itIt = items.iterator();
while (itIt.hasNext()) {
Item it = itIt.next();
if (pBounds.intersects(it.getBounds())) {
score += it.value;
itIt.remove();
}
}
// Check enemy collisions
for (Enemy en : enemies) {
if (pBounds.intersects(en.getBounds())) {
// game over: reset player and score
player.x = 100;
player.y = HEIGHT - 60;
player.vx = 0;
player.vy = 0;
cameraX = 0;
score = 0;
break; // avoid multiple resets
}
}
// Keep player within world
if (player.x < 0) { player.x = 0; player.vx = 0; }
if (player.x > levelWidth) { player.x = levelWidth; player.vx = 0; }
if (player.y > HEIGHT - 20) { player.y = HEIGHT - 20; player.vy = 0; player.onGround = true; }
repaint();
}
リファクタリング後のメソッド
@Override
public void actionPerformed(ActionEvent e) {
if (gameOver) return;
frame++;
// delegate to smaller helpers for readability and lower cognitive complexity
// 背景の星のスクロールを更新
updateBackground();
// 一定間隔で敵を生成
spawnEnemies();
// 敵の移動、射撃、画面外削除を処理
updateEnemiesList();
// プレイヤーの弾の移動と画面外削除を処理
updatePlayerBullets();
// 敵の弾の移動と画面外削除を処理
updateEnemyBullets();
// ミサイルの移動と敵追尾を処理
updateMissiles();
// アイテムの移動と画面外削除を処理
updateItems();
// オプションの移動を処理
updateOptions();
// キー入力に基づくプレイヤーの移動と境界チェック
updatePlayerMovement();
// 射撃ボタン押下時の弾生成
handleShooting();
// 弾と敵、プレイヤーと敵/弾の衝突判定とスコア更新
handleCollisions();
repaint();
}
上記のように、責務をメソッド単位で分割すればメソッド内で実行する処理の概要を素早く把握でき可読性が向上する。
まとめ
生成AIを活用したコーディングは、実装スピードを大きく向上させる一方で、人間にとってレビューしやすい構造になるとは限らないという課題があります。
特に、複数の責務が混在したコードや可読性の低い実装は、人間のレビュー工程における大きな負担となります。
本記事では、この課題に対するアプローチとして、保守性向上を目的としたリファクタリングを取り上げました。
具体的には、静的解析によってコードスメルを検出し、その結果をもとに生成AIがリファクタリングを行うことで、レビュー前の段階でコード品質を底上げします。
コードスメルの検出には SonarQube などの静的解析ツールを活用でき、機械的に問題点を洗い出せる点が有効です。
このようにしてリファクタリングされたコードをレビュー対象とすることで、レビュアーの負担を軽減し、ロジックの確認や業務要件の妥当性といった本質的な観点に集中できます。
また、AIに実装だけでなくリファクタリングの工程にも担わせることで、人間が確認しやすい形に整えたうえでレビューに回せる点も本アプローチの重要なポイントです。
※リファクタリングによるデグレードを防ぐためには、テストコードの整備をセットで行うことが前提となる点には注意が必要です。
生成AIの現場投入は急速に進んでいますが、現時点では開発を完全にAIへ委ねる段階には至っておらず、人間によるレビューは依然として不可欠な認識です。
本記事で紹介したアプローチは、その避けられないレビュー工程を効率化するための現実的な一歩として有効だと考えています。
