はじめに:ポリレポ環境におけるアーキテクチャガバナンスの問題

Netflixは数万のJavaリポジトリを運用するポリレポ戦略を採用しています。各チームが独立して開発する一方で、共通ビルドロジックやライブラリ使用ルールを一貫して維持することは非常に困難です。特に、後方互換性を壊す変更(breaking change)が発生すると、全サービスに影響を与える可能性があります。

この記事では、Netflix JVM Platformチームがこの問題を解決するために開発した Nebula ArchRules プラグインを紹介します。単なるツールの紹介に留まらず、なぜ従来の静的解析ツール(PMD、SpotBugs)ではなくArchUnitを選択し、どのようにして5,000リポジトリ規模に拡張したのか、その設計原理を詳しく見ていきます。

参考: この記事はNetflix Tech Blogの「Scaling ArchUnit with Nebula ArchRules」を基にしています。詳細は原文をご参照ください。

Netflix microservices architecture diagram with thousands of Java repositories connected by Gradle plugins Software Concept Art

ArchUnitが静的解析ツール(PMD、SpotBugs)より優れている理由

1. ASTではなくバイトコード解析

PMDなどのツールはソースコードのAST(Abstract Syntax Tree)を解析します。これは言語に依存します。Kotlin、ScalaなどJVM言語ごとにAST構造が異なるため、ルールを各言語に合わせて書き直す必要があります。一方、ArchUnitは ASMを使用してコンパイルされたバイトコードを直接解析します。そのため、どの言語で書かれていても、実際に実行されるコードを基準にルールを適用できます。

2. タイプセーフでIDEフレンドリーなルール記述

PMDのXPathベースのルールは次のようになります:

<!-- PMD XPathルール例:DateTimeが明示的なZoneなしで生成されないようにする -->
<rule name="AvoidDateTimeWithoutZone" language="java"
      message="DateTime must be instantiated with an explicit time zone">
    <properties>
        <property name="xpath">
            <value>
                //AllocationExpression/ClassOrInterfaceType[
                @Image='DateTime' and (
                (count(..//Name[@Image='DateTimeZone.UTC'])&lt;=0)
                and
                (count(..//Name[@Image='DateTimeZone.forID'])&lt;=0)
                ) or (
                (
                (count(..//Name[@Image='DateTimeZone.UTC'])&gt;0)
                or
                (count(..//Name[@Image='DateTimeZone.forID'])&gt;0)
                ) and (../Arguments/ArgumentList and count(../Arguments/ArgumentList/Expression) = 1)
                )
                ]
            </value>
        </property>
    </properties>
</rule>

これと同じルールをArchUnitで書くと次のようになります:

// ArchUnitルール:DateTimeコンストラクタにZone引数がない場合は警告
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class DateTimeRules {
    public static final ArchRule AVOID_DATETIME_WITHOUT_ZONE = 
        priority(Priority.MEDIUM)
            .noClasses()
            .should()
            .callConstructorWhere(
                // コンストラクタにDateTimeZone引数がない場合
                target(doesNotHave(rawParameterTypes(DateTimeZone.class)))
                // DateTimeクラスのコンストラクタの場合
                .and(targetOwner(assignableTo(DateTime.class)))
            );
}

違いは明白です。ArchUnitは 純粋なJavaコードでルールを記述し、IDEのオートコンプリート、リファクタリング、型チェック機能をそのまま活用できます。単体テストも簡単です。

3. クラス関係グラフの活用

ArchUnitはASMでクラスパス全体を読み込み、クラス間の依存関係グラフを構築します。これにより、「AクラスがBクラスのdeprecatedメソッドを呼び出していないか」といったコンテキストのあるルールを簡単に記述できます。

Developer analyzing ArchUnit rule violations report on a terminal with code snippets Developer Related Image

Nebula ArchRules:ルールを共有し自動実行するプラットフォーム

ArchUnit自体は単一リポジトリでJUnitテストとして実行するように設計されています。Netflixの要件は 組織全体の全リポジトリで同一ルールを実行することでした。このために、Nebula ArchRulesは2つの核心プラグインを提供します。

1. ArchRules Library Plugin:ルールをライブラリとしてパッケージング

このプラグインはGradleプロジェクトに archRules という追加ソースセットを作成します。ここに ArchRulesService インターフェースを実装したクラスを記述します。

// build.gradle(ルールライブラリプロジェクト)
plugins {
    id 'nebula.archrules.library'
}

dependencies {
    // ArchUnit API依存関係
    implementation 'com.tngtech.archunit:archunit:1.2.0'
}
// src/archRules/java/com/example/rules/GuavaRules.java
package com.example.rules;

import com.netflix.nebula.archrules.service.ArchRulesService;
import com.tngtech.archunit.lang.ArchRule;

import java.util.HashMap;
import java.util.Map;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

public class GuavaRules implements ArchRulesService {

    // Guava Optionalの代わりにJava Optionalを使用するよう強制
    static final ArchRule OPTIONAL = 
        priority(Priority.MEDIUM)
            .noClasses()
            .should()
            .dependOnClassesThat()
            .haveFullyQualifiedName("com.google.common.base.Optional")
            .because("Java OptionalがGuava Optionalよりも推奨されます");

    @Override
    public Map<String, ArchRule> getRules() {
        Map<String, ArchRule> rules = new HashMap<>();
        rules.put("guava-optional", OPTIONAL);
        return rules;
    }
}

こうして作成されたルールは arch-rules classifierで別のJarにパッケージングされて配布されます。

2. ArchRules Runner Plugin:ルールを自動実行

適用対象リポジトリでは次のように設定します:

// build.gradle(ルールを適用されるプロジェクト)
plugins {
    id 'nebula.archrules.runner'
}

dependencies {
    // スタンドアロンルールライブラリ:全ソースセットに適用
    archRules 'com.example:my-standalone-rules:1.0.0'
    
    // バンドルルールライブラリ:自動検出(依存関係として追加すると自動適用)
    testImplementation 'com.example:my-test-helper:2.0.0'  // このライブラリにバンドルされたルールが自動実行
}

// ルールカスタマイズ
archRules {
    ruleClass("com.netflix.nebula.archrules.deprecation") {
        priority = "HIGH"
    }
}

バンドルルール(Bundled Rule Libraries) が特に強力です。ライブラリ開発者が自身のライブラリの使用方法に特化したルールを一緒に配布すれば、ユーザーは別途設定なしでそのルールが自動適用されます。

3. レポーティング:人が読みやすい失敗情報

ルール違反時に次のような詳細メッセージが提供されます:

ルール違反:'Deprecated API使用禁止'
優先度:HIGH
クラス:com.example.MyService
位置:MyService.java:42
説明:Deprecated API 'com.oldlib.LegacyClass.oldMethod()' が使用されています。
       このAPIは削除対象のため、'com.newlib.NewClass.newMethod()' に置き換えてください。

この情報はJSONファイルでも出力され、Netflixはこれを内部開発者ポータルと連携し、全チームが自分のコードにどのような違反があるかをダッシュボードで確認できるようにしました。

Cloud infrastructure diagram showing polyrepo strategy and shared build logic across teams Technical Structure Concept

実践適用事例:APIライフサイクル管理

Netflix JVM Platformチームはこのプラグインを使用して ライブラリAPIライフサイクル管理 問題を解決しました。ライブラリ開発者は次のようなアノテーションをAPIに付けます:

  • @Deprecated(Java標準):もう使用しない
  • @Public(カスタム):外部で使用してもよいAPI
  • @Experimental(カスタム):まだ安定していない
  • デフォルト:内部専用(internal)

そしてArchRulesで次のようなルールを記述します:

// ライブラリパッケージ外部でDeprecated API使用禁止
ArchRuleDefinition.priority(Priority.MEDIUM)
    .noClasses()
    .that(resideOutsideOfPackage(packageName + ".."))
    .should()
    .dependOnClassesThat(
        resideInAPackage(packageName + "..")
            .and(are(deprecated()))
    )
    .orShould()
    .accessTargetWhere(
        targetOwner(resideInAPackage(packageName + ".."))
            .and(target(is(deprecated()))
                .or(targetOwner(is(deprecated()))))
    )
    .allowEmptyShould(true)
    .because("Deprecated APIs are subject to removal");

これでライブラリ開発者はメインブランチCIビルドごとに、すべてのダウンストリームプロジェクトで自身のAPIがどのように使われているかレポートを確認できます。「このdeprecated APIを実際に使っているプロジェクトは3つだけだから、先にマイグレーションを依頼してから削除しよう」といった意思決定が可能になります。

結果と成果

  • 358個のルールを 5,000以上のリポジトリで実行中
  • 約100万件のイシュー検出
  • 約1,000件のHigh Priorityイシュー特定

これによりNetflixは膨大なマイクロサービス群の技術負債(technical debt)を可視化し、優先順位をつけて効率的に解決できるようになりました。

日本開発エコシステムにおける適用コンテキスト

日本のSIer/スタートアップ環境でもこのアプローチは非常に有用です。特に:

  • マルチモジュールプロジェクト:1つのリポジトリに複数モジュールがある場合、モジュール間の依存関係ルールをArchRulesで定義できます。
  • 共通ライブラリ管理:社内共通ライブラリ(例:ロギング、セキュリティ、HTTPクライアント)の使用方法を強制したい場合、バンドルルールが効果的です。
  • レガシーマイグレーション:Spring BootバージョンアップやJakarta EE移行時に、使用禁止APIをリアルタイムで検出できます。

注意点: ArchUnitはバイトコードを解析するため、リフレクションや動的プロキシで呼び出されるコードは検出できない場合があります。またルールが多すぎるとビルド時間が増加する可能性があるため、優先度の低いルールは警告(warning)レベルに設定することをお勧めします。

次のステップ学習方向

  1. ArchUnit公式ドキュメントを読み、より多様なルールタイプ(クラス、メソッド、フィールド、アノテーション)を習得しましょう。
  2. Nebula ArchRulesオープンソースライブラリを参考に、自分だけのルールライブラリを作成してみてください。
  3. OpenRewriteと連携し、自動リファクタリングパイプラインを構築する方法を研究してみましょう。

合わせて読みたい記事

本コンテンツは、信頼性の高い情報源をもとにAIツールを活用して作成され、編集者によるレビューを経て公開されています。専門家によるアドバイスの代替となるものではありません。