들어가며: 폴리레포 환경의 아키텍처 거버넌스 문제
Netflix는 수만 개의 Java 리포지토리를 운영하는 폴리레포(Polyrepo) 전략을 사용합니다. 각 팀이 독립적으로 개발하지만, 공통 빌드 로직과 라이브러리 사용 규칙을 일관되게 유지하는 것은 매우 어려운 과제입니다. 특히, 하위 호환성을 깨는 변경(breaking change)이 발생하면 전체 서비스에 영향을 줄 수 있습니다.
이 글에서는 Netflix JVM Platform 팀이 이러한 문제를 해결하기 위해 만든 Nebula ArchRules 플러그인을 소개합니다. 단순한 툴 소개를 넘어, 왜 기존 정적 분석 도구(PMD, SpotBugs) 대신 ArchUnit을 선택했고, 어떻게 이를 5,000개 리포지토리 규모로 확장했는지 그 설계 원리를 살펴봅니다.
참고: 이 글은 Netflix Tech Blog의 'Scaling ArchUnit with Nebula ArchRules'를 기반으로 합니다. 더 자세한 내용은 원문을 참고하세요.

ArchUnit이 정적 분석 도구(PMD, SpotBugs)보다 나은 이유
1. AST가 아닌 바이트코드(Bytecode) 분석
PMD와 같은 도구는 소스 코드의 AST(Abstract Syntax Tree)를 분석합니다. 이는 언어에 종속적입니다. Kotlin, Scala 등 JVM 언어마다 AST 구조가 다르기 때문에 규칙을 각 언어에 맞게 다시 작성해야 합니다. 반면, ArchUnit은 ASM을 사용해 컴파일된 바이트코드를 직접 분석합니다. 따라서 어떤 언어로 작성되었든, 실제 실행되는 코드를 기준으로 규칙을 적용할 수 있습니다.
2. 타입 안전(Type-Safe)하고 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'])<=0)
and
(count(..//Name[@Image='DateTimeZone.forID'])<=0)
) or (
(
(count(..//Name[@Image='DateTimeZone.UTC'])>0)
or
(count(..//Name[@Image='DateTimeZone.forID'])>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 메서드를 호출하는지"와 같은 맥락 있는 규칙을 쉽게 작성할 수 있습니다.

Nebula ArchRules: 규칙을 공유하고 자동 실행하는 플랫폼
ArchUnit 자체는 단일 리포지토리에서 JUnit 테스트로 실행되도록 설계되었습니다. Netflix의 요구사항은 조직 전체의 모든 리포지토리에서 동일한 규칙을 실행하는 것이었습니다. 이를 위해 Nebula ArchRules는 두 가지 핵심 플러그인을 제공합니다.
1. ArchRules Library Plugin: 규칙을 라이브러리로 패키징
이 플러그인은 Gradle 프로젝트에 archRules라는 추가 소스 세트(source set)를 생성합니다. 여기에 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 is preferred over 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는 이를 내부 개발자 포털에 연동하여 모든 팀이 자신의 코드에 어떤 위반 사항이 있는지 대시보드에서 확인할 수 있도록 했습니다.

실무 적용 사례: 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는 방대한 마이크로서비스 fleet의 기술 부채(technical debt)를 가시화하고, 우선순위를 정해 효율적으로 해결할 수 있게 되었습니다.
한국 개발 생태계에서의 적용 맥락
한국 SI/스타트업 환경에서도 이 접근법은 매우 유용합니다. 특히:
- 멀티 모듈 프로젝트: 하나의 리포지토리에 여러 모듈이 있을 때, 모듈 간 의존성 규칙을 ArchRules로 정의할 수 있습니다.
- 공통 라이브러리 관리: 회사 내 공통 라이브러리(예: 로깅, 보안, HTTP 클라이언트)의 사용법을 강제하고 싶을 때 번들 규칙이 효과적입니다.
- 레거시 마이그레이션: Spring Boot 버전 업그레이드나 Jakarta EE 전환 시, 사용이 금지된 API를 실시간으로 감지할 수 있습니다.
주의사항: ArchUnit은 바이트코드를 분석하므로, 리플렉션(reflection)이나 동적 프록시(dynamic proxy)로 호출되는 코드는 감지하지 못할 수 있습니다. 또한 규칙이 너무 많아지면 빌드 시간이 증가할 수 있으므로, 우선순위가 낮은 규칙은 경고(warning) 수준으로 설정하는 것이 좋습니다.
다음 단계 학습 방향
- ArchUnit 공식 문서를 읽고 더 다양한 규칙 유형(클래스, 메서드, 필드, 어노테이션)을 익혀보세요.
- Nebula ArchRules 오픈소스 라이브러리를 참고하여 자신만의 규칙 라이브러리를 만들어보세요.
- OpenRewrite와 연계하여 자동 리팩토링 파이프라인을 구축하는 방법을 연구해보세요.