はじめに
AI コーディング時代。Claude Code、Cursor、Copilot など、コードを直接タイピングする時間よりも、AI が生成したコードをレビューする時間の方が長くなりました。
しかし、AI が作ったコードの安定性を確保するのは簡単ではありません。
AI は「動作するコード」を作るのは得意ですが、プロジェクトのアーキテクチャルール、テスト品質、コードコンベンションまで気を配るのは別の問題です。
CLAUDE.md にルールを書いておいても、結局ガイドラインに過ぎません。守らなくてもビルドは通ります。
そしてまた、AI によって生産性が向上した現時点において、人が一つ一つ追いかけながらチェックするのも無理があります。
私はこの問題を解決するために、さまざまなツールとプロセスを導入しました。
この記事は、その過程をまとめたものです。
現在やっていること
私たちのプロジェクトでは、コードの安定性のためにすでに運用中のツールが 2 つあります。
1. JaCoCo + CodeCov - カバレッジの可視化
JaCoCo でテストカバレッジを測定し、CodeCov で PR ごとに可視化しています。
PR を開くと GitHub Action がテストを実行し、Codecov がカバレッジレポートを PR コメントとして残します。
# open-pr.yml
- name: Test with Gradle
run: ./gradlew test
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
PR で「この変更によってカバレッジが何 % 変わったか」をすぐに確認できます。
# codecov.yml
codecov:
require_ci_to_pass: true
github_checks:
annotations: true
単なる可視化だけでなく、Codecov の PR チェックがカバレッジ基準を満たさなければ PR が承認されないように 設定しています。
カバレッジが下がるコードは、マージ自体がブロックされます。
2. NShiftKey
NShiftKey は、PR が開かれたときに自動でセキュリティ検査を行う GitHub check ツールです。
PR ごとに 6 つの項目を検査します。
- インフラエラー
- コードレベルのセキュリティ脆弱性
- ライブラリの脆弱性
- 機密情報の露出
- 静的解析ベースのバグ検出
- コードスメルの検出
PR を開くと NShiftKey が自動で実行され、セキュリティ上の問題が見つかると PR チェックが失敗します。
Codecov と同様に、通過しなければマージがブロックされます。
AI が生成したコードに誤ってシークレットが含まれていたり、脆弱なライブラリを import していたり、Dockerfile の設定を間違えていても、人がレビューする前に検出できます。
ただし、NShiftKey のコードスメル検出は セキュリティ観点の簡単なパターンマッチング レベルです。
例えば、ハードコーディングされたパスワードや危険な API の使用は検出できますが、重複コードや複雑度の過多といった問題は当然検出できません。
限界
これらのツールである程度は対応できましたが、根本的な限界がありました。
カバレッジの罠
現在、私たちのプロジェクトの JaCoCo カバレッジは約 90% 程度です。
数字だけ見ると悪くありません。
しかし、カバレッジが高いということは、テストがそのコードを実行したという意味であり、その実行結果が正しいかどうかまでは保証しません。
@Test
void マナースコア計算(){
mannerScoreService.calculate(userId);
// assert がない。実行しただけで結果を検証していない
}
calculate() の中で計算方式をいくら変えても(例:+ を - に変えても)、このテストは通過します。コードが壊れていても、テストはそれに気づきません。
アーキテクチャルール違反を検出できない
AI がコードを生成する際に、プロジェクトのレイヤー構造に違反する場合があります。
Controller から Repository を直接呼び出したり、Entity に @Autowired を付けたりするケースです。
これは CLAUDE.md にルールをまとめても、AI が 100% 正確に従うわけではありません。
コードスメルを体系的に検出できない
NShiftKey がセキュリティ関連の設定に限定して検査してくれますが、複雑度や不要な DB 呼び出しなど コードレベルの問題 は NShiftKey の検出範囲外であり、人の目でレビューする際にも見落としやすいです。
AI が生成するコードの量が増えるほど、さらに見落としやすくなります。
そんな中、MUSINSA という企業の技術ブログを読みました。
「AI にお願いするのではなく、試験を受けさせろ」
AI にお願いするよりも、強制的なルールを与えて失敗させるという内容でした。
この記事では ArchUnit でテストを失敗させていました。
私もここからアイデアを得て、統制するツールを導入し、AI コードの安定性と品質を改善しようと考えました。
導入するもの
- アーキテクチャルール違反 → コード生成時点で検出し、AI が自ら修正(ArchUnit + Claude Code Hooks)
- テスト品質 → 「実行しただけのテスト」と「検証するテスト」を区別(Mutation Testing)
- コードスメル → 静的解析で体系的に検出(SonarCloud)
ArchUnit 導入記
ArchUnit とは
ArchUnit は、Java のアーキテクチャルールを ユニットテストとして記述する ライブラリです。
「Controller は Repository を直接参照してはならない」といったルールをコードで定義し、違反するとテストが失敗します。
// build.gradle
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
プロジェクト構成
住民同士がコミュニケーションするコミュニティアプリ
├── auth/ # 認証(JWT + OAuth2)
├── message/ # グループチャットメッセージング(コアモジュール)
├── room/ # チャットルーム管理
├── story/ # ストーリー + フリマ取引
├── notification/ # プッシュ通知
├── user/ # ユーザープロフィール
└── common/ # 共通インフラ
各モジュール内部:
├── presentation/ # Controller + DTO
├── domain/ # Service + Repository + Entity
└── infrastructure/ # 外部連携、設定
適用したルール 10 個
ArchitectureTest に 8 つのカテゴリルールを定義しました。主なルールは以下の通りです。
1. ネーミングコンベンション
-
@RestControllerクラスは*Controllerで終わる必要があり、presentation.controllerパッケージに配置しなければなりません。@Serviceも同様です
2. エンティティの純粋性
- Entity は Spring アノテーション禁止、Service / Infrastructure への依存禁止
3. レイヤー依存性の方向
- Controller → Repository の直接参照禁止、Controller での
@Transaction、@EventListener、@Asyncの使用禁止
4. 循環依存性
- フィーチャーモジュール間のサービスレイヤーにおける循環参照を検出
- データレイヤー(Entity、Repository、Event など)のクロスモジュール参照は、モノリス構造上やむを得ないため除外
5. コード品質
-
System.out禁止、ジェネリック例外(RuntimeException)の throw 禁止、フィールド@Autowired禁止(コンストラクタインジェクションを強制) - プロダクションコードでのテストライブラリ依存禁止
6. エンティティ純粋性の拡張
- Entity に Lombok
@Setter禁止、@Async、@Scheduled禁止
7. 依存性逆転の原則
- domain.resolver インターフェースの実装クラスが domain パッケージに配置されてはなりません
- infrastructure に配置してこそ DIP が守られます
- このプロジェクトでは、モジュール間の依存性を切るために domain にインターフェース(
RoomMessageResolver、AuthUserProviderなど)を置き、infrastructure に実装クラス(RoomMessageResolverImpl、AuthUserProviderImplなど)を配置する DIP パターンを使用しています - AI が便宜上、実装クラスを domain に直接作ってしまうのを、このルールが検出します
8. API URL 形式の検証
- Controller の
@RequestMappingは必ず/api/v1で始まらなければなりません。AI が/usersのような非標準 API を作るのを防止します。
Claude Code Hooks:自動採点機
ArchUnit は試験用紙です。Claude Code Hooks は 自動採点機 です。
Claude Code Hooks とは
Claude Code のライフサイクルの特定の時点で 自動的に実行されるシェルコマンド です。
- CLAUDE.md のルールはガイドラインに過ぎません
- Hooks のルールは強制されます
実際の活用
Stop イベントを活用しました。Claude が応答を終えるたびに、ArchUnit テストが自動実行されます。
.claude/settings.json の Hook 設定は以下の通りです。
.claude/hooks/archunit-check.sh の設定は以下の通りです。
#!/bin/bash
# Claude Code Stop Hook: ArchUnit アーキテクチャ検証
# Claude が応答を終えた時に実行。違反時は exit 2 で Claude が引き続き修正するようにする。
# 無限ループ防止:Stop hook の中で再度 Stop がトリガーされた場合はスキップ
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi
PROJECT_DIR=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
if [ -z "$PROJECT_DIR" ]; then
PROJECT_DIR="/Users/gimdonghyeon/Documents/Jimotoku"
fi
cd "$PROJECT_DIR" || exit 0
# 変更された Java ソースファイルがあるか確認(テストファイルは除外)
MODIFIED=$(git diff --name-only 2>/dev/null | grep '\.java$' | grep 'src/main/' || true)
if [ -z "$MODIFIED" ]; then
exit 0 # Java ソースの変更がなければスキップ
fi
# ArchUnit テスト実行
echo "🔍 ArchUnit アーキテクチャ検証実行中..."
OUTPUT=$(./gradlew test --tests '*ArchitectureTest*' -q 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "ArchUnit アーキテクチャ違反を発見!"
echo ""
echo "$OUTPUT" | grep -A 5 "Architecture Violation\|FAILED\|> Task.*FAILED" | head -40
echo ""
echo "違反事項を修正してください。"
exit 2 # Claude が引き続き作業するようにする
fi
echo "ArchUnit アーキテクチャ検証通過"
exit 0
Claude Code を通じて作成しました
Mutation Testing:テストは本当に検証しているのか?
なぜ必要なのか
- JaCoCo カバレッジ約 90%、テストファイル 93 個、テストコード 2600 個以上。テストの数だけ見ると十分に見えます。
- しかし、Claude Code がコードを書き換えるブラックボックス(in → blackbox → out)領域の変更を検出できるほど、テストコードが良質に書かれているかどうかは別の問題です。
- そこで、Mutation Testing を導入することにしました。
Mutation Testing とは?
- テストがプロダクションコードのバグを実際に検出できるのか?
- つまり、テストの質を検証するテストです。
- プロダクションコードを意図的に変形する(mutant を生成)
- 変形されたコードに対して既存のテストを実行する
- テストが失敗すれば killed(良い。テストが変形を検出した)
- テストが通過すれば survived(悪い。テストが変形を見逃した)
例えば
public int calculateUnreadBadge(int totalUnread, int readCount) {
if (readCount > totalUnread) {
return 0;
}
return totalUnread - readCount;
}
以下のようなコードがある場合、
Mutant の例 — PITest がコードを以下のように変形します。
// Mutant 1: 条件境界の変形(ConditionalsBoundary)
if (readCount >= totalUnread) { // > → >=
// Mutant 2: 数学演算の変形(Math)
return totalUnread + readCount; // - → +
// Mutant 3: 戻り値の変形(ReturnValue)
return 0; // 結果を無条件に 0 に
悪いテスト は以下のような結果になります。
@Test
void バッジ計算() {
calculateUnreadBadge(10, 3);
// assert がない。実行しただけで終わり。
// → Mutant 1, 2, 3 すべて survived(カバレッジは 100%)
}
反対に 良いテスト は以下のように書かれます。
@Test
void 読んだ分だけバッジが減るべき() {
assertThat(calculateUnreadBadge(10, 3)).isEqualTo(7);
// → Mutant 2(+ に変えると 13)killed ✓
// → Mutant 3(0 を返す)killed ✓
}
@Test
void 読んだ数が全体より大きければ_0になる() {
assertThat(calculateUnreadBadge(5, 5)).isEqualTo(0);
assertThat(calculateUnreadBadge(5, 6)).isEqualTo(0);
// → Mutant 1(>= に変えると 5,5 で結果が変わる)killed ✓
}
- PITest は分析に時間がかかりすぎるため、CI でのみ動作するようにしました。
# deploy.yml
- name: Run Tests
run: ./gradlew test --no-daemon
- name: Run PITest (Mutation Testing)
run: ./gradlew pitest --no-daemon
continue-on-error: true
- name: Upload PITest Report
if: always()
uses: actions/upload-artifact@v4
with:
name: pitest-report
path: build/reports/pitest/
retention-days: 14
SonarCloud 導入
SonarCloud とは?
- 静的解析ツールです。コードを実行しなくても、バグ、コードスメル、セキュリティ脆弱性、重複コードを検出します。
連携するだけで、PR に分析レポートを投稿してくれます。
上記のような分析機能を提供しています。
私たちのプロジェクトでは、上の画像以外にも 5 つほどの他のパフォーマンス上の問題を発見することができました。
JaCoCo 連携
SonarCloud は JaCoCo が生成した XML レポートを読み取ってカバレッジを表示します。特別な設定なしに sonar.coverage.jacoco.xmlReportPaths を指定するだけで済みます。
この他にも
- Gemini によるコードレビュー(以前は CodeRabbit、Copilot を使っていましたが、これが一番性能が良かったです)
- Agent Team を構成したコード品質レビューなどの機能を使っています
ここでは今回新たに導入した、コード品質を高めるためのツールを紹介する記事なので、ここまでにします。
まとめると以下の通りです。
全体ツール体系の整理
コード生成時点(Claude Code)
- CLAUDE.md によるガイドライン
- ArchUnit によるアーキテクチャの試験用紙
- Claude Hooks による採点および修正の自動化
PR 時点(GitHub Actions)
- JaCoCo + CodeCov によるカバレッジの可視化
- SonarCloud による静的解析(バグ、スメル、セキュリティ)
- NShiftKey によるセキュリティ問題の検出
- Gemini によるコードレビュー
- ユニットテスト → ./gradlew test
- PITest → Mutation Testing(テスト品質の測定)
強制するツール vs 検知するツール
すべてのツールが同じ強度で動作するわけではありません。
役割に応じて 強制 または 検知 に分けることができます。
強制するツールが セーフティネット であるなら、検知するツールは 診断ツール です。
なぜこのように分けるかというと、開発速度とのトレードオフの関係にあるからです。
例えば、PITest を Claude Code に必ず適用するとなると、テストに 1 時間もかかってしまいます。
これらのツールを組み合わせて、異なる観点からコードの安定性を確保し、AI が書いたコードを信頼できるコードにするために努力しています。






