"매 architectural violation 의 intended architecture 와 actual code 사이의 deviation". Perry & Wolf 의 erosion (decay over time) + drift (unauthorized addition) 의 distinguishing — 매 erosion 의 explicit decay, drift 의 silent. 매 modern detection 의 fitness function (ArchUnit, dep-cruiser, Sonargraph) + reflexion model — 매 manual review 의 too-late.
Classify (true violation vs intentional exception).
Triage (debt vs immediate fix).
Fix (refactor or update intended architecture / ADR).
Prevent regression (add fitness function).
💻 패턴
Detect layer violation — ArchUnit
@AnalyzeClasses(packages=["com.acme"],importOptions=[DoNotIncludeTests::class])classLayerViolationTest{@ArchTestvaldomain_independent:ArchRule=noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat().resideInAnyPackage("..application..","..infrastructure..","..web..").because("Domain is the innermost layer (Hexagonal)")@ArchTestvalno_repo_in_controller:ArchRule=noClasses().that().resideInAPackage("..web..").should().dependOnClassesThat().resideInAPackage("..persistence..")}
Detect cycle — dependency-cruiser (TS)
npx depcruise --validate .dependency-cruiser.cjs src \
--output-type err-html --output-to depgraph.html
# Exit 1 if cycles found; html visualizes offending edges
Reflexion model — intended vs actual (jQAssistant + Cypher)
// Intended: domain → no-deps; application → domain; infra → application
// Detect any class in :Domain depending on :Infra (forbidden)
MATCH (a:Class)-[:DEPENDS_ON]->(b:Class)
WHERE a.layer = "Domain" AND b.layer = "Infrastructure"
RETURN a.fqn AS violator, b.fqn AS forbidden
Runtime violation — unauthorized service call (OpenTelemetry + Tempo)
# Alert if service A calls service C (intended only A→B, B→C)sum(rate(traces_spanmetrics_calls_total{client="service-a",server="service-c"}[5m]))>0
// When violation is *intended* (rare), document explicitly via ADR + suppress
@SuppressArchTest(rule="domain_independent",reason="ADR-0034: bridge to legacy DAO during 6-month migration",expiresOn="2026-12-01")classLegacyBridgeAdapter{/* ... */}
// Companion test enforces expiry
@ArchTestvalno_expired_suppressions:ArchRule=noClasses().should().beAnnotatedWith(SuppressArchTest::class.java).andShould(haveExpiredSuppression())// custom condition
Visualize violations — dependency matrix (D3)
// scripts/depmatrix.mjs — render violation heatmap
import{cruise}from"dependency-cruiser";importfsfrom"node:fs";constr=awaitcruise(["src"],{ruleSet:require("./.dependency-cruiser.cjs")});constviolations=r.output.summary.violations;constmatrix=buildAdjacencyMatrix(r.output.modules,violations);fs.writeFileSync("violations.json",JSON.stringify(matrix));// Then render with d3-matrix or observable-plot
CI gating with delta — only new violations fail
# Compare current vs main — fail PR only on new violations (not legacy debt)
npx depcruise src --config .dependency-cruiser.cjs --output-type json > head.json
git checkout main -- .
npx depcruise src --config .dependency-cruiser.cjs --output-type json > main.json
node scripts/diff-violations.mjs main.json head.json # exit 1 on new violations
Erosion KPI dashboard (per service / quarter)
-- violations_history table (populated nightly by CI)
SELECTservice,DATE_TRUNC('week',detected_at)ASwk,COUNT(*)FILTER(WHEREseverity='error')ASerrs,COUNT(*)FILTER(WHEREseverity='warn')ASwarnsFROMviolations_historyWHEREdetected_at>NOW()-INTERVAL'12 weeks'GROUPBYservice,wkORDERBYservice,wk;
매 결정 기준
상황
Approach
New violation detected in PR
Block merge until fixed or ADR exception added
Legacy debt — many existing violations
Baseline & gate on delta (no new)
Intentional bridge
Suppress + ADR + expiry date
Runtime call mismatch
Trace-based alerting + service mesh policy
Unclear if violation
Run reflexion model — document intended first
기본값: ArchUnit/dep-cruiser at PR time + delta gating on legacy code + ADR-tracked suppressions with expiry.
언제: classify violation as erosion vs drift vs intentional, draft suppression ADR with expiry, generate refactor plan from violation report, summarize weekly violations dashboard for tech lead.
언제 X: do not blindly accept LLM "this violation is fine" — every suppression needs ADR + expiry; LLM judgment is suggestion, not authority.
❌ 안티패턴
Suppression without expiry: permanent escape hatch — original intent lost.
No baseline: blocking PRs on existing legacy violations buries team.
Documentation-only intended architecture: cannot detect drift — no ground truth.
One-shot detection: run once, never again — violations re-grow.
Fix-the-symptom: rename file vs fix actual coupling.
Verified (Perry & Wolf "Foundations for the Study of Software Architecture" 1992, Murphy "Reflexion Models" 1995, ArchUnit / Sonargraph / dependency-cruiser docs).
신뢰도 A.
🕓 Changelog
날짜
변경
2026-05-08
Phase 1
2026-05-10
Manual cleanup — erosion vs drift, reflexion model, delta gating