ArchUnit으로 아키텍처 규칙 검증하기

JavaArchUnitTestingArchitecture

들어가며

프로젝트를 시작할 때 가장 먼저 손대는 일 중 하나가 코딩 컨벤션 합의입니다. 작게는 코드 포맷부터 변수명, 메서드명, 더 나아가 폴더 구조와 아키텍처까지 다양한 컨벤션을 정해두고 출발하죠. 이런 컨벤션은 보통 문서로 남깁니다.

팀원 모두가 컨벤션을 잘 따라준다면 좋겠지만, 실제로는 이해도의 차이가 있기 마련입니다. 특히 신규 팀원이 합류한 경우 문서를 온전히 이해하고 적용하기란 쉽지 않습니다. 그래서 코드 리뷰를 거치며 결을 맞춰나가는데, 여기서 의사소통 비용이 꽤 발생합니다.

코드 리뷰는 비즈니스 로직 설계가 잘 되었는지를 중심으로 봐야 한다고 생각합니다. 자잘한 컨벤션을 일일이 짚는 일은 여간 까다롭지 않습니다. 사실 IDE 포매터나 Checkstyle만으로도 기본 코딩 컨벤션은 어느 정도 검증되고 자동 보정도 됩니다. husky를 곁들이면 커밋 메시지 컨벤션까지 잡을 수 있죠.

그렇다면 네이밍 컨벤션이나 아키텍처, 레이어 컨벤션은 어떻게 보장할까요? 이런 부분을 자동으로 검증할 방법은 없을까요? 아키텍처 품질을 꾸준히 보장하면서도 개발자와 리뷰어의 부담은 덜어주는 장치가 필요했어요.

이 글에서는 Java 프로젝트의 아키텍처 규칙을 코드로 검증해주는 ArchUnit 라이브러리를 소개하고, 실제 프로젝트에 적용한 사례를 공유합니다.

컨벤션의 중요성과 현실적인 한계

컨벤션의 중요성

진행 중인 프로젝트의 컨벤션 문서
진행중인 프로젝트의 컨벤션 문서

코딩 컨벤션은 단순히 코드를 예쁘게 쓰기 위한 장치가 아닙니다. 팀원 간 소통을 원활하게 하고, 코드 품질과 일관성을 일정 수준으로 유지하며, 신규 팀원의 적응을 돕는 역할을 합니다. 특히 아키텍처 컨벤션은 프로젝트의 유지보수성과 확장성에 직접 영향을 주기 때문에 더 중요합니다.

기존 방식의 한계

보통 컨벤션은 다음과 같은 방식으로 관리됩니다.

  1. 문서화: README나 Wiki에 컨벤션을 상세히 기록
  2. 코드 리뷰: PR을 통해 컨벤션 준수 여부 확인
  3. 포맷팅 도구: 코드 스타일 자동 포맷팅

이 중 코드 리뷰는 비즈니스 로직 검토와 알고리즘 효율성 점검 같은 더 무거운 사안에 집중해야 하는 자리인데, 자잘한 컨벤션 위반까지 챙기다 보면 리뷰어 부담이 커집니다. 게다가 사람이 하는 일이라 컨벤션 위반을 놓치기도 합니다.

IDE 포매터나 CheckStyle 같은 도구는 들여쓰기·줄 바꿈처럼 스타일 관련 컨벤션은 잘 잡지만, 아키텍처나 레이어 간 의존성 같은 고수준 규칙까지는 손이 닿지 않습니다. 그렇다면 이런 높은 수준의 컨벤션은 어떻게 자동화할까요? 이럴 때 꺼내드는 도구가 ArchUnit입니다.

ArchUnit이란?

ArchUnit은 Java 코드의 아키텍처를 테스트하는 오픈소스 라이브러리입니다. 일반적인 단위 테스트처럼 작성하며, JUnit과 같은 표준 테스트 프레임워크와 함께 씁니다.

ArchUnit의 주요 기능

ArchUnit은 다음과 같은 다양한 아키텍처 규칙을 검증합니다.

  1. 패키지와 클래스의 의존성 검사: 특정 패키지나 클래스가 다른 패키지·클래스에 의존하는지 검사합니다.
  2. 레이어 아키텍처 검사: 애플리케이션 레이어(예: Controller, Service, Repository)가 올바른 방향으로 의존하는지 검사합니다.
  3. 클래스 네이밍 규칙 검사: 클래스 이름이 정해진 패턴을 따르는지 검사합니다.
  4. 애노테이션 사용 검사: 특정 클래스나 메서드에 필요한 애노테이션이 붙어 있는지 검사합니다.
  5. 순환 참조 검사: 패키지나 클래스 사이에 순환 참조가 있는지 검사합니다.
  6. 코딩 컨벤션 검사: 팀에서 정한 코딩 컨벤션을 따르는지 검사합니다.

ArchUnit의 동작 원리

ArchUnit은 JVM 바이트코드(.class 파일)를 직접 읽어 분석합니다. 이 과정에서 Java 바이트코드를 조작·분석하는 ASM 라이브러리를 사용해 클래스, 메서드, 필드 간 의존 관계와 애노테이션 같은 메타데이터를 추출합니다.

예를 들어, ArchUnit이 다음과 같은 검증을 수행할 때,

noClasses().that().resideInPackage("..service..")
          .should().dependOnClassesThat().resideInPackage("..controller..")

내부적으로는 이런 일이 벌어집니다.

  1. 지정된 패키지에서 모든 클래스 파일을 로드
  2. 각 클래스의 바이트코드를 파싱해 의존성 그래프 구축
  3. service 패키지의 클래스들이 controller 패키지의 클래스를 참조하는지 확인
  4. 위반 사항이 있으면 상세한 위치 정보와 함께 보고

특히 좋은 점은 클래스 파일을 직접 읽기 때문에 실행 중인 애플리케이션이나 런타임 환경에 의존하지 않고 정적 분석이 된다는 겁니다. 즉, 애플리케이션을 띄우지 않고도 아키텍처를 검증하므로 테스트 속도가 매우 빠릅니다.

ArchUnit 사용법

ArchUnit을 쓰려면 먼저 의존성을 추가해야 합니다. Gradle이라면 다음과 같이 적습니다.

testImplementation 'com.tngtech.archunit:archunit-junit5:1.0.1'

기본적인 사용 방법

ArchUnit을 다루는 방법은 크게 세 가지입니다.

1. 선언형 검사 (Declarative Style)

선언형 검사는 규칙을 먼저 선언한 뒤 한 번에 검사하는 방식입니다. 코드 가독성이 높고 규칙을 명확하게 표현한다는 장점이 있습니다.

@Test
public void 서비스_레이어는_컨트롤러_레이어에_의존하지_않아야_한다() {
    JavaClasses importedClasses = new ClassFileImporter().importPackages("com.myapp");

    ArchRule rule = noClasses()
        .that().resideInAPackage("..service..")
        .should().dependOnClassesThat().resideInAPackage("..controller..");

    rule.check(importedClasses);
}

2. 명령형 검사 (Imperative Style)

클래스와 메서드를 직접 순회하며 원하는 조건을 세밀하게 검사하는 방식입니다. 복잡한 규칙이나 맞춤형 검증에 적합합니다.

@Test
public void 애노테이션_규칙_검증() {
    JavaClasses classes = new ClassFileImporter().importPackages("com.myapp");

    for (JavaClass clazz : classes) {
        if (clazz.getPackageName().contains("controller")) {
            assertThat(clazz.isAnnotatedWith(RestController.class))
                .as("컨트롤러는 @RestController 애노테이션을 가져야 합니다")
                .isTrue();
        }
    }
}

3. AssertJ 연동 검사

AssertJ의 SoftAssertion으로 여러 위반 사항을 한 번에 모아서 보여주는 방식입니다. 특히 초기 개발 단계에서 모든 문제를 한눈에 파악하기 좋습니다.

@Test
public void 컨트롤러_규칙_검증() {
    SoftAssertions softly = new SoftAssertions();
    JavaClasses classes = new ClassFileImporter().importPackages("com.myapp.controller");

    for (JavaClass clazz : classes) {
        softly.assertThat(clazz.getSimpleName().endsWith("Controller"))
            .describedAs("컨트롤러 클래스명은 Controller로 끝나야 합니다: %s", clazz.getName())
            .isTrue();
    }

    softly.assertAll();
}

실제 프로젝트 적용 사례

GitHub - toduck-App/toduck-backend: Service for adult ADHD
Service for adult ADHD. Contribute to toduck-App/toduck-backend development by creating an account on GitHub.

이제 실제 프로젝트에서 ArchUnit을 어떻게 활용했는지 살펴보겠습니다. 저희 프로젝트에서는 다음 규칙들을 ArchUnit으로 검증하고 있습니다.

레이어 의존성 규칙

기존 3티어 아키텍처를 쓰면서 서비스 계층 간 의존성이 높아지고 도메인 로직과 레포지토리 호출 코드가 뒤섞여 유지보수가 힘들어지는 문제를 겪었습니다. 이를 풀기 위해 이번 프로젝트에서는 UseCase 레이어를 더해 고수준 비즈니스 로직을 분리했습니다. 기존과 다른 아키텍처라 팀원에게 명확한 규칙 설명과 이해가 필요했고, 레이어가 늘어 복잡해진 의존성을 체계적으로 검증할 방법도 함께 마련해야 했습니다.

프로젝트의 레이어 아키텍처

현재 프로젝트의 레이어 구조는 다음과 같습니다.

  • Presentation Layer: Controller, DTO, API 인터페이스
  • Domain Layer: Service, UseCase
  • Persistence Layer: Repository, Entity
  • Common Layer: Mapper 등 공통 유틸리티

이러한 레이어 간 의존성 규칙을 다음과 같이 정의해 검증합니다.

@ArchTest
static final ArchRule 레이어_의존성_규칙을_준수한다 = layeredArchitecture()
    .consideringAllDependencies()
    .layer(CONTROLLER.name()).definedBy(CONTROLLER.getFullPackageName())
    .layer(DTO.name()).definedBy(DTO.getFullPackageName())
    .layer(SERVICE.name()).definedBy(SERVICE.getFullPackageName())
    .layer(USECASE.name()).definedBy(USECASE.getFullPackageName())
    .layer(REPOSITORY.name()).definedBy(REPOSITORY.getFullPackageName())
    .layer(ENTITY.name()).definedBy(ENTITY.getFullPackageName())
    .layer(MAPPER.name()).definedBy(MAPPER.getFullPackageName())

    .whereLayer(CONTROLLER.name()).mayNotBeAccessedByAnyLayer()
    .whereLayer(SERVICE.name()).mayOnlyBeAccessedByLayers(USECASE.name())
    .whereLayer(USECASE.name()).mayOnlyBeAccessedByLayers(CONTROLLER.name())
    .whereLayer(REPOSITORY.name()).mayOnlyBeAccessedByLayers(SERVICE.name())
    .whereLayer(ENTITY.name())
    .mayOnlyBeAccessedByLayers(
        SERVICE.name(), USECASE.name(), REPOSITORY.name(), MAPPER.name(), ENTITY.name(), DTO.name()
    )
    .whereLayer(MAPPER.name()).mayOnlyBeAccessedByLayers(SERVICE.name(), USECASE.name());

이 규칙은 각 레이어가 어떤 레이어에 접근할 수 있는지 명확히 정의합니다. Controller는 UseCase만 호출하고, UseCase는 Service만, Service는 Repository만 호출하도록 단방향 의존성을 강제합니다. 그 결과 레이어 간 의존성 방향이 일관되게 유지되어 코드 구조와 흐름이 또렷해집니다.

클래스 네이밍 규칙

각 레이어의 클래스는 일관된 네이밍 규칙을 따라야 합니다. 이를 검증하는 테스트는 다음과 같습니다.

@ArchTest
static final ArchRule 컨트롤러_클래스_네이밍_규칙을_준수한다 = classes()
    .that().resideInAPackage(CONTROLLER.getFullPackageName())
    .should().haveSimpleNameEndingWith("Controller");

@ArchTest
static final ArchRule 유스케이스_클래스_네이밍_규칙을_준수한다 = classes()
    .that().resideInAPackage(USECASE.getFullPackageName())
    .should().haveSimpleNameEndingWith("UseCase");

@ArchTest
static final ArchRule 서비스_클래스_네이밍_규칙을_준수한다 = classes()
    .that().resideInAPackage(SERVICE.getFullPackageName())
    .should().haveSimpleNameEndingWith("Service");

애노테이션 사용 규칙

각 레이어의 클래스는 특정 애노테이션을 달고 있어야 합니다. 이를 검증하는 테스트는 다음과 같습니다.

@ArchTest
static final ArchRule Service_클래스는_Service_어노테이션을_가진다 =
    classes()
        .that().resideInAPackage(SERVICE.getFullPackageName())
        .should().beAnnotatedWith(Service.class);

@ArchTest
static final ArchRule UseCase_클래스는_UseCase_어노테이션을_가진다 =
    classes()
        .that().resideInAPackage(USECASE.getFullPackageName())
        .should().beAnnotatedWith(UseCase.class);

@ArchTest
static final ArchRule Controller_클래스는_RestController_어노테이션을_가진다 =
    classes()
        .that().resideInAPackage(CONTROLLER.getFullPackageName())
        .should().beAnnotatedWith(RestController.class);

@ArchTest
static final ArchRule Controller_메서드는_인증된_메서드만_포함한다 =
    methods()
        .that().areDeclaredInClassesThat().resideInAPackage(CONTROLLER.getFullPackageName())
        .and().arePublic()
        .should().beAnnotatedWith(PreAuthorize.class);

현재 프로젝트는 Swagger로 API 문서를 자동 생성합니다. 공통 응답 형식과 도메인별 에러 코드 체계를 도입했고, 이 구조가 API 문서에도 그대로 반영되어야 했습니다.

기존 Swagger 어노테이션만으로는 현재의 공통 응답 구조를 제대로 표현하지 못해 커스텀 어노테이션을 만들었습니다. 기본 Swagger 어노테이션을 그냥 쓰면 정확한 응답 구조가 문서화되지 않으므로, 반드시 커스텀 @ApiResponseExplanations를 쓰도록 강제하는 규칙이 필요했습니다.

@ArchTest
static final ArchRule API_메서드는_Operation_어노테이션을_가진다 =
    methods()
        .that().areDeclaredInClassesThat().resideInAPackage(API.getFullPackageName())
        .should().beAnnotatedWith(Operation.class);

@ArchTest
static final ArchRule API_메서드는_ApiResponseExplanations_어노테이션을_가진다 =
    methods()
        .that().areDeclaredInClassesThat().resideInAPackage(API.getFullPackageName())
        .should().beAnnotatedWith(ApiResponseExplanations.class);

모든 API 메서드가 @Operation과 @ApiResponseExplanations 같은 커스텀 Swagger 애노테이션을 갖춰야 한다는 사실을 위와 같이 검증합니다.

코딩 스타일 규칙

일부 코딩 스타일 규칙도 ArchUnit으로 검증합니다. 저희는 System.out.println()처럼 표준 스트림에 접근하는 호출을 금지하고 있습니다.

@ArchTest
private final ArchRule 표준스트림에_접근하지_말아야한다 = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;

위는 ArchUnit이 기본 제공하는 규칙을 가져다 쓴 예입니다.

이 외에도 다음과 같은 유용한 기본 규칙들이 제공됩니다.

  • NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING: Java 기본 로깅 API 사용 금지
  • NO_CLASSES_SHOULD_USE_JUNIT_ASSERTIONS: JUnit 대신 다른 테스트 라이브러리 사용 권장
  • NO_CLASSES_SHOULD_USE_FIELD_INJECTION: 스프링의 필드 주입 대신 생성자 주입 권장
  • NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS: 일반적인 예외 대신 구체적인 예외 사용 권장
  • NO_CLASSES_SHOULD_USE_JODATIME: Java 8 이후에는 내장 시간 API 사용 권장

저희는 테스트 코드 가독성을 끌어올리려고 JUnit 대신 AssertJ를 강제합니다. AssertJ는 플루언트 인터페이스 덕에 테스트 코드가 일반 영어 문장처럼 읽히고, 에러 메시지도 더 직관적이며, 매처(matcher)도 풍부합니다. 이를 위해 다음과 같은 ArchUnit 규칙을 적용합니다.

@ArchTest
public static final ArchRule 가독성을_위해_Junit을_사용하지_않는다 =
    noClasses()
        .should().accessClassesThat()
        .haveFullyQualifiedName(org.junit.Assert.class.getName())
        .because("Junit 대신 AssertJ를 사용하세요.");

ArchUnit 도입의 장단점

ArchUnit으로 아키텍처 테스트를 운영하며 여러 이점을 체감했습니다. 물론 어떤 도구든 마찬가지지만 장점만 있는 건 아니라서, 단점도 같이 짚어보겠습니다.

장점

  1. 자동화된 아키텍처 검증 수동 코드 리뷰에만 기대지 않고 빌드 단계에서 아키텍처 규칙을 자동으로 검증합니다. 덕분에 코드 리뷰는 비즈니스 로직에 더 집중하게 됩니다.
  2. 빠른 피드백 사이클 스프링 컨텍스트를 띄우지 않고 바이트코드만 분석해서 테스트가 매우 빠릅니다. 개발자는 빠르게 피드백을 받고 곧장 수정합니다.
  3. 가독성 높은 테스트 구문 ArchUnit은 플루언트 인터페이스 스타일이라 마치 일반 문장을 읽는 것처럼 이해됩니다. 테스트 코드 자체의 가독성과 유지보수성도 따라 올라갑니다.
  4. 점진적 적용 가능 기존 코드베이스에도 단계적으로 적용합니다. 예를 들어 freeze() 메서드를 쓰면 현재 위반 사항은 묻어두면서 새로 생긴 위반만 잡아냅니다.

단점

  1. 초기 설정 비용 규칙을 정의하고 테스트를 짜는 데 초기 투자가 듭니다. 팀 전체가 ArchUnit에 익숙해지는 데도 시간이 걸려요.
  2. 유지보수 부담 아키텍처가 진화하면 테스트 코드도 따라 갱신해야 합니다. 특히 대규모 리팩토링에선 손댈 테스트가 꽤 늘어납니다.
  3. 규칙의 엄격함 때로는 너무 엄격한 규칙이 창의성이나 유연성을 깎아냅니다. 예를 들어 "모든 컨트롤러 클래스명은 Controller로 끝나야 한다" 같은 규칙은 상황에 따라 불필요한 제약이 되기도 하죠.
  4. 예외 처리의 복잡성 모든 규칙에는 예외가 있기 마련인데, 그 예외 케이스를 ArchUnit으로 다루는 일이 만만치 않습니다. 다행히 ArchUnit은 ignoreDependency()나 because() 같은 메서드로 예외를 정의해두지만, 예외가 늘면 테스트 코드가 그만큼 복잡해집니다.
ArchRule rule = noClasses()
    .that().resideInPackage("..service..")
    .should().dependOnClassesThat().resideInPackage("..controller..")
    .ignoreDependency(ServiceA.class, ControllerB.class)
    .because("특별한 이유로 예외 처리");

테스트 코드의 또 다른 의미: 살아있는 문서

우리가 작성하는 테스트 코드는 단순한 기능 검증을 넘어 프로젝트의 지식을 담는 문서 역할도 자주 합니다. 단위 테스트나 통합 테스트만 봐도 비즈니스 로직 흐름과 입출력 예시가 잡히죠.

ArchUnit 테스트도 같은 결입니다. 단순한 규칙 검증을 넘어 프로젝트의 아키텍처를 또렷이 보여주는 '살아있는 문서'가 됩니다.

예를 들어, 다음과 같은 테스트 코드를 보면,

@ArchTest
static final ArchRule 레이어_의존성_검증 = layeredArchitecture()
    .consideringAllDependencies()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Repository").definedBy("..repository..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");

이 짧은 코드만으로도 프로젝트가 3개의 레이어로 구성되어 있고 레이어 간 관계가 어떻게 짜여 있는지 한눈에 들어옵니다. 문서를 읽는 것보다 훨씬 또렷하고 간결하게 아키텍처가 잡힙니다.

ArchUnit 테스트는 가이드라인 역할도 해요. 규칙을 어긴 코드를 작성하면 테스트가 곧장 실패하므로, 개발자가 아키텍처 규칙을 미처 몰랐더라도 올바른 방향으로 끌려갑니다. 큰 팀이나 복잡한 프로젝트에선 이 효과가 특히 큽니다.

CI에서 위반 사례가 잡힌 모습
실제 CI 과정에서 위반 사례를 확인한 모습

특히 인상 깊었던 점은 코드 리뷰 과정이 크게 바뀌었다는 것입니다. 리뷰어가 "이 컨트롤러는 이름 규칙을 따라야 해요"라거나 "유즈케이스 레이어에서 직접 리포지토리에 접근하면 안 돼요" 같은 기계적인 피드백을 줄 일이 거의 사라졌죠. 대신 비즈니스 로직이나 코드 효율성처럼 더 무게 있는 지점에 집중하게 되었어요.

결론

ArchUnit은 Java 프로젝트의 아키텍처 일관성을 지키는 데 꽤 강력한 도구입니다. 기존 단위 테스트 프레임워크와 그대로 엮어 쓰기 때문에 도입 장벽도 낮습니다.

프로젝트를 진행하다 보면 문서로만 존재하던 아키텍처 규칙이 슬그머니 무너지는 경우가 많아요. 코드 리뷰만으로는 다 잡아내기 어렵고, 그대로 두면 기술 부채로 쌓입니다. 실제로 ArchUnit을 도입한 뒤 아키텍처 관련 이슈가 크게 줄었고, 두 명의 신규 팀원이 합류했을 때도 아키텍처를 빠르게 파악하는 데 도움이 됐다는 피드백을 받았습니다.

ArchUnit은 단순히 규칙을 강제하는 도구가 아니라, 팀의 아키텍처 지식을 코드로 굳혀 공유하는 방법입니다. 그 결과 팀은 더 일관되고 유지보수하기 쉬운 코드를 쓰게 되고, 개발 생산성과 코드 품질도 함께 따라 올라갑니다.

여담: 하네스 엔지니어링이라는 새로운 관점

이 글을 작성한 이후인 2026년 초, OpenAI와 Anthropic을 중심으로 하네스 엔지니어링(Harness Engineering) 이라는 개념이 본격적으로 대두되었습니다. 하네스 엔지니어링이란, AI 에이전트(LLM 기반 코딩 도구)가 코드를 생성할 때 올바른 방향으로 동작하도록 감싸는 제약·검증·피드백 시스템 전체를 설계하는 엔지니어링 분야입니다.

Martin Fowler는 하네스를 "AI 에이전트를 통제하기 위한 도구와 관행"이라 정의했고, OpenAI Codex 팀은 실제로 100만 줄 규모의 프로덕션 코드를 수작업 없이 생성하면서 그 핵심에 하네스가 있었다고 밝혔습니다. 흥미로운 점은 LangChain이 모델은 그대로 두고 하네스만 개선해서 벤치마크 순위를 Top 30에서 Top 5로 끌어올렸다는 사실입니다. 모델보다 모델을 둘러싼 시스템이 더 중요해진 셈입니다.

하네스 엔지니어링의 핵심 원칙은 네 가지로 요약됩니다.

  1. Constrain — 에이전트가 할 수 있는 것을 제한한다
  2. Inform — 에이전트가 해야 할 것을 알려준다
  3. Verify — 에이전트가 올바르게 수행했는지 검증한다
  4. Correct — 잘못되었을 때 교정한다

돌아보면, 이 글에서 다룬 ArchUnit은 이 중 ConstrainVerify에 정확히 해당합니다. 레이어 의존성 규칙을 정의해 구조적 제약을 걸고(Constrain), 빌드 또는 CI에서 자동으로 위반 여부를 검증하는(Verify) 것이죠. 사람이 작성한 코드든, AI 에이전트가 생성한 코드든 ArchUnit 테스트를 통과해야 머지되는 구조라면, 아키텍처 일관성은 동일하게 보장됩니다.

여기에 AGENTS.md(또는 CLAUDE.md) 같은 프로젝트 규칙 명세 파일을 추가하면 Inform 영역까지 커버할 수 있습니다. 에이전트가 코드를 생성하기 전에 아키텍처 규칙, 네이밍 컨벤션, 금지 사항 등을 컨텍스트로 읽어들이게 하는 것이죠. 특히 ArchUnit 테스트 파일의 위치를 명시해두면, 에이전트는 해당 테스트 코드를 직접 읽고 프로젝트의 아키텍처 제약을 파악할 수 있습니다. 앞서 ArchUnit 테스트가 "살아있는 문서" 역할을 한다고 했는데, 그 문서의 독자가 사람에서 AI 에이전트로 확장되는 셈입니다. 그리고 에이전트의 실패 패턴을 ArchUnit 규칙이나 AGENTS.md에 피드백하는 루프를 만들면 Correct 단계까지 완성됩니다.

결국 ArchUnit으로 아키텍처 규칙을 코드화한 것은, AI 에이전트 시대에서 하네스의 핵심 구성요소를 이미 구축한 셈입니다. "에이전트에게 잘 해달라고 부탁하는 것"이 아니라 "잘못할 수 없는 환경을 만드는 것", 이것이 프롬프트 엔지니어링과 하네스 엔지니어링의 근본적 차이입니다.

〜〜〜