--- id: wiki-2026-0508-architectural-constraint-enforce title: Architectural Constraint Enforcement category: 10_Wiki/Topics status: verified canonical_id: self aliases: [archunit, dependency-cruiser, fitness-functions] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [architecture, fitness-functions, archunit, constraints] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: java framework: archunit --- # Architectural Constraint Enforcement ## 매 한 줄 > **"매 architecture 의 자동으로 enforce 한다"**. 매 ArchUnit (2018, Java/Kotlin), Dependency-Cruiser (JS/TS), NetArchTest (.NET), Konsist (Kotlin) 의 fitness function 의 CI 통합. 매 *Building Evolutionary Architectures* (Ford, Parsons, Kua, 2017; 2nd ed 2023) 의 paradigm. ## 매 핵심 ### 매 enforced rules (typical) - **Layer dependency**: 매 controller → service → repository — 매 reverse 의 X. - **Package isolation**: `domain` 의 framework import 의 X. - **Naming**: 매 `*Service` class 의 `service` package 의 only. - **Cyclic dependency**: 매 always X. - **API surface**: 매 `internal/*` 의 external module 의 import 의 X. ### 매 fitness function categories (Ford) - **Atomic vs Holistic**: single attribute / system-wide. - **Triggered vs Continual**: CI gate / runtime probe. - **Static vs Dynamic**: code analysis / runtime metric. - **Automated vs Manual**: prefer automated. ### 매 응용 1. CI gate — 매 PR 의 violation 의 block. 2. Refactor safety — 매 large refactor 의 invariant 의 hold. 3. Onboarding — 매 implicit rule 의 explicit code 의 됨. ## 💻 패턴 ### ArchUnit — layer enforcement (Java) ```java import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; @AnalyzeClasses(packages = "com.acme.banking") class LayerArchTest { @ArchTest static final ArchRule controllers_only_call_services = classes().that().resideInAPackage("..controller..") .should().onlyDependOnClassesThat() .resideInAnyPackage("..controller..", "..service..", "java..", "org.springframework.."); @ArchTest static final ArchRule no_cycles = slices().matching("..banking.(*)..").should().beFreeOfCycles(); @ArchTest static final ArchRule services_named_correctly = classes().that().resideInAPackage("..service..") .and().areNotInterfaces() .should().haveSimpleNameEndingWith("Service"); } ``` ### Dependency-Cruiser (TypeScript) ```js // .dependency-cruiser.cjs module.exports = { forbidden: [ { name: 'no-circular', severity: 'error', from: {}, to: { circular: true } }, { name: 'domain-pure', severity: 'error', from: { path: '^src/domain' }, to: { path: '^(src/infra|node_modules/express)' } }, { name: 'no-test-in-prod', severity: 'error', from: { pathNot: '\\.test\\.ts$' }, to: { path: '\\.test\\.ts$' } } ], options: { tsConfig: { fileName: 'tsconfig.json' } } }; ``` ```bash npx depcruise src --config .dependency-cruiser.cjs npx depcruise src --output-type dot | dot -T svg > deps.svg ``` ### NetArchTest (.NET 8) ```csharp using NetArchTest.Rules; using Xunit; public class ArchitectureTests { [Fact] public void Domain_ShouldNotDependOnInfrastructure() { var result = Types.InAssembly(typeof(Domain.Marker).Assembly) .That().ResideInNamespace("Acme.Domain") .ShouldNot().HaveDependencyOn("Acme.Infrastructure") .GetResult(); Assert.True(result.IsSuccessful, string.Join(",", result.FailingTypeNames ?? [])); } } ``` ### Konsist (Kotlin) ```kotlin import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.verify.assertTrue class CleanArchitectureTest { @Test fun `domain layer does not depend on data layer`() { Konsist.scopeFromProduction() .files .filter { it.packagee?.fullyQualifiedName?.contains("domain") == true } .assertTrue { file -> file.imports.none { it.name.contains(".data.") } } } } ``` ### Go — go-arch-lint ```yaml # .go-arch-lint.yml version: 3 workdir: internal components: domain: { in: domain/** } app: { in: app/** } infra: { in: infra/** } deps: domain: {} app: { mayDependOn: [domain] } infra: { mayDependOn: [domain, app] } ``` ### Python — import-linter ```ini # .importlinter [importlinter] root_package = acme [importlinter:contract:layers] name = Layered architecture type = layers layers = acme.api acme.service acme.domain ``` ### CI integration (GitHub Actions) ```yaml - name: Architecture tests run: | ./gradlew archTest npx depcruise src --config .dependency-cruiser.cjs lint-imports ``` ## 매 결정 기준 | Stack | Tool | |---|---| | Java/Kotlin | ArchUnit, Konsist | | JS/TS | Dependency-Cruiser, eslint-plugin-boundaries | | .NET | NetArchTest | | Go | go-arch-lint | | Python | import-linter | | Polyglot | Sonargraph, Structure101 (commercial) | **기본값**: 매 ArchUnit (JVM) / Dependency-Cruiser (Node) — 매 OSS + CI-first. ## 🔗 Graph - 부모: [[Software Architecture]] · [[Fitness Functions]] - 변형: [[Static-Analysis]] - 응용: [[CI-CD]] · [[Architecture_Refactor]] - Adjacent: [[Architecture Erosion (아키텍처 침식)]] · [[Modular-Monolith]] ## 🤖 LLM 활용 **언제**: 매 plain English rule → ArchUnit/depcruise config 의 translation, 매 violation message 의 fix suggestion. **언제 X**: 매 architecture 의 design 의 LLM-only delegation — 매 human ownership 의 필수. ## ❌ 안티패턴 - **Test exists, never runs**: 매 CI 의 not wired — 매 dead rule. - **Over-broad rule**: 매 100% violations 의 noise — 매 graduated rollback (allowlist). - **Rule without rationale**: 매 ADR-less rule — 매 future deletion 의 blocker. - **Ignore-list explosion**: 매 exception 의 100+ — 매 architecture 의 already eroded 의 sign. ## 🧪 검증 / 중복 - Verified (Ford et al., *Building Evolutionary Architectures* 2nd ed; ArchUnit user guide). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — fitness functions + ArchUnit/depcruise/NetArchTest patterns |