--- title: ArchUnit 분석 scope — import scope(어떤 클래스가 검사되는가) vs classpath 의존성(어떻게 검사하는가) source_type: llm-generated status: draft confidence: medium tags: [archunit, clean-architecture, testing, static-analysis, jvm] related_projects: [ca-skeleton] last_reviewed: 2026-06-04 --- # ArchUnit 분석 scope — import scope(어떤 클래스가 검사되는가) vs classpath 의존성(어떻게 검사하는가) > Layer: `wiki/concepts/` — 일반 개념. 내 프로젝트 사실(`wiki/projects/`)은 `wiki-project-template` 사용. ## Summary ArchUnit rule의 결과는 두 개의 독립적인 축에 의해 결정된다. (1) **import scope** — `ClassFileImporter`/`@AnalyzeClasses`가 어떤 class를 분석 대상 집합(`JavaClasses`)으로 끌어왔는가. (2) **classpath 의존성** — 그 class를 분석할 때 ArchUnit이 JVM classpath(reflection)에 의존하는가, 아니면 bytecode만 읽는가. 첫 번째 축을 잘못 잡으면 검사하려던 class가 아예 집합에 없어 rule이 *vacuous하게* 통과한다(false-negative). 두 번째 축은 대부분의 default rule에서 무관하지만 strongly-typed annotation 접근 같은 일부 ergonomics에만 영향을 준다. ## Standard (공식 정의) - **Import 진입점**: class import의 표준 진입점은 `new ClassFileImporter().importPackages("")`이며, JUnit 통합에서는 `@AnalyzeClasses(packages = ...)`가 같은 역할을 한다. `importPackages(...)`는 varargs라 다중 package를 받을 수 있고, "단일 root package만 가능"하다는 의미가 아니다. 출처: [[raw/official-docs/archunit-user-guide]] (ARCHUNIT-UG-C2). - **import은 classpath와 무관**: ArchUnit은 classpath/JAR/folder 어디서 import했는지와 무관하게 `JavaClasses`를 구성할 수 있다. 즉 import scope는 "어떤 `.class` 파일을 읽었는가"의 문제이지 "그 class가 현재 test의 classpath에 있는가"와 자동으로 같지 않다. 출처: [[raw/official-docs/archunit-conditional-on-property-3-layer-pattern]] (AUCP-C4). - **rule 평가는 classpath에 의존하지 않음**: ArchUnit 자체의 rule API와 default rule + syntax 조합 평가는 classpath에 의존하지 않는다. 출처: [[raw/official-docs/archunit-conditional-on-property-3-layer-pattern]] (AUCP-C4). - **classpath가 영향을 주는 곳**: classpath가 있으면 annotation을 `javaClass.getAnnotationOfType(CustomAnnotation.class).value()`처럼 strongly-typed로 접근할 수 있고, 없으면 `JavaAnnotation` + `Object value = annotation.get("value")` 같은 untyped 접근을 써야 한다. 출처: [[raw/official-docs/archunit-conditional-on-property-3-layer-pattern]] (AUCP-C2, AUCP-C3). - **rule 평가 흐름**: rule은 `ArchRule` 객체로 표현되고 `myRule.check(importedClasses)` 또는 `@ArchTest`로 평가된다. `@ArchTest`가 붙은 rule은 지정된 class를 자동 import(또는 재사용)해 평가한다. 출처: [[raw/official-docs/archunit-user-guide]] (ARCHUNIT-UG-C3, ARCHUNIT-UG-C6). ## 한계 / 주의점 - **package filter가 import scope를 보장하지 않는다**: rule의 `that().resideInAPackage("..application..")`는 *이미 import된 집합 안에서* 필터링할 뿐이다. 해당 package의 class가 import scope(`@AnalyzeClasses(packages=...)` 또는 test classpath)에 애초에 없으면, 위반 코드가 존재해도 매칭 대상이 0개가 되어 rule이 통과한다. 즉 "package glob을 썼으니 그 package를 다 본다"는 착각이 가장 흔한 실패 모드다. - **두 가지 빈-집합 동작이 다르다**: (a) `that()` 결과가 비면 ArchUnit은 기본적으로 `failed to check any classes` 에러를 낸다 — 이때는 *눈에 보이는* 실패다. 빈 anchor module이 의도된 상태라면 `allowEmptyShould(true)`로 명시적으로 허용해야 한다. (b) 그러나 검사 대상 class가 *import scope 자체에 빠져* 있으면 ArchUnit은 그것을 "정상 평가했고 위반 0건"으로 인식해 `failed to check any classes` 에러조차 내지 않고 `BUILD SUCCESSFUL`로 통과한다 — 이 vacuous pass가 더 위험하다(에러 신호가 없으므로). - **`allowEmptyShould(true)`는 양날의 검**: 빈 anchor를 합법화하지만, 동시에 import scope 누락으로 인한 vacuous pass도 똑같이 통과시켜 버린다. 따라서 빈-집합 허용 정책만으로는 rule이 *실제로* 위반을 잡는지 보증할 수 없다. - **권장 보완**: (1) 검사 대상이 될 수 있는 module/package(예: sample·fixture)를 test의 import scope에 명시적으로 포함시킨다(예: Gradle `testImplementation project(':')`). (2) "위반을 데이터로 보는(violations-as-data)" negative fixture를 두고, 의도된 위반 class에 대해 `rule.evaluate(fixtureClasses).hasViolation() == true`를 별도 test로 assert해 rule이 진짜 catch하는지 commit으로 보증한다. - **classpath 의존성과 import scope를 혼동하지 말 것**: "classpath에 없어서 못 잡았다"와 "import scope에 안 넣어서 못 잡았다"는 다른 문제다. 전자는 주로 annotation ergonomics(typed accessor)에만 영향을 주고, false-negative의 실제 원인은 거의 항상 후자(import scope 누락)다. 두 축을 섞어 진단하면 엉뚱한 곳을 고친다. (이 구분의 정밀한 경계는 ArchUnit 버전·import 옵션에 따라 달라질 수 있어 `needs-confirmation`) ## Project Application 이 개념과 관련된 내 프로젝트 사실·검증 등급은 아래 project 문서에서 판정한다(concept 문서는 등급을 직접 매기지 않는다). - [[wiki/projects/ca-tmpl/clean-architecture-package-layout]] — ca-tmpl의 `CleanArchitectureTest`가 `@AnalyzeClasses(packages = "dev.caskeleton", importOptions = DoNotIncludeTests.class)`로 import scope를 잡고, `allowEmptyShould(true)`로 빈 anchor를 허용하며, `ArchitectureViolationFixtureTest`(violations-as-data)로 각 rule의 catch 동작을 보증하는 실제 적용. - [[wiki/concepts/clean-architecture-package-layout]] — 경계 강제(enforcement)의 두 축(build-graph 검사 vs source/bytecode import 검사) 일반 지식. ## Claim-backed Knowledge > 이 개념 문서의 핵심 설명은 raw source claim으로 뒷받침되어야 한다. 공식 문서 claim과 내 프로젝트 트러블슈팅 사실을 분리한다. | Knowledge Point | Supporting Claims | Confidence | Notes | |---|---|---|---| | import 진입점은 `ClassFileImporter().importPackages(...)`이며 varargs로 다중 package 가능(단일 root 강제 아님) | `raw/official-docs/archunit-user-guide.md#ARCHUNIT-UG-C2` | high | 공식 vendor doc | | ArchUnit rule API/default rule 평가는 classpath(reflection)에 의존하지 않으며, classpath/JAR/folder 어디서 import했는지와 무관 | `raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C4` | high | 공식 vendor doc. "import scope ≠ classpath presence"의 근거 | | annotation 접근 ergonomics만 classpath에 의존(있으면 typed `.value()`, 없으면 untyped `JavaAnnotation.get("value")`) | `raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C2`, `#AUCP-C3` | high | classpath가 영향을 주는 *유일한* 좁은 지점 | | rule은 `ArchRule.check(classes)` / `@ArchTest`로 평가되고, `@ArchTest`는 지정 class를 자동 import해 평가 | `raw/official-docs/archunit-user-guide.md#ARCHUNIT-UG-C3`, `#ARCHUNIT-UG-C6` | high | 공식 vendor doc | | `that()` 매칭 결과가 비면 기본적으로 `failed to check any classes` 실패 — 빈 anchor가 의도면 `allowEmptyShould(true)` 필요 | `raw/errors/archunit-empty-should-anchor-2026-05-27.md` | medium | 프로젝트 트러블슈팅 사실(`error-note`). 빈 *should* 동작 | | 검사 대상 class가 import scope에 빠지면 위반이 있어도 vacuous pass(`BUILD SUCCESSFUL`, 에러 신호 없음) — sample/fixture를 test import scope에 포함 + negative fixture로 보완 | `raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28.md` | medium | 프로젝트 트러블슈팅 사실(`error-note`). 빈 *that* / import-scope 누락 동작 | ## 내가 설명할 수 있어야 하는 것 - ArchUnit의 import scope와 classpath 의존성은 각각 무엇을 결정하는가? - package glob(`..application..`)을 썼는데도 위반을 놓치는 경우는 왜 생기는가? - `failed to check any classes` 에러가 *나는* 경우와 *나지 않고 통과해 버리는* 경우의 차이는 무엇인가? - `allowEmptyShould(true)`는 무엇을 허용하고, 무엇을 *못* 막는가? - vacuous pass를 어떻게 commit 수준에서 막는가(violations-as-data)? ## Interview Questions - ArchUnit rule이 통과했는데도 실제로는 boundary가 깨져 있을 수 있는 시나리오는? 어떻게 방지하는가? - ArchUnit의 분석이 JVM classpath에 의존하는 부분과 의존하지 않는 부분은 각각 무엇인가? - 빈 anchor package가 많은 skeleton에서 architecture test를 신뢰 가능하게 유지하려면 무엇이 필요한가? ## Do Not Overclaim - "package glob을 쓰면 그 package의 모든 class를 검사한다"는 단정 금지. 검사 대상은 *import scope ∩ glob*이며, scope에 없으면 검사되지 않는다. - "ArchUnit은 classpath가 필요하다/필요 없다"는 단정 금지. default rule 평가는 classpath 독립이지만 typed annotation 접근 같은 ergonomics는 classpath에 의존한다 — 부분적이다. - "`allowEmptyShould(true)`를 켜면 안전하다"는 단정 금지. 빈 should를 허용할 뿐, import scope 누락으로 인한 vacuous pass는 막지 못한다. - 위 빈-집합/scope 동작의 정밀한 경계는 ArchUnit 버전·import 옵션에 따라 달라질 수 있어 일부는 `needs-confirmation`이다. ## Sources - [[raw/official-docs/archunit-user-guide]] — ArchUnit User Guide (import 진입점, rule 평가, JUnit 통합) - [[raw/official-docs/archunit-conditional-on-property-3-layer-pattern]] — classpath 유무에 따른 annotation 접근 + rule API의 classpath 독립성 - [[raw/errors/archunit-empty-should-anchor-2026-05-27]] — 빈 anchor에서의 `failed to check any classes` + `allowEmptyShould` 해결(프로젝트 사실) - [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] — import scope 누락으로 인한 vacuous pass + sample module을 test scope에 포함해 해결(프로젝트 사실)