Files
llm-wiki/raw/errors/mapping-exception-location-archunit-catch-2026-05-29.md
T

59 lines
4.4 KiB
Markdown

---
title: error / mapping-exception-location-archunit-catch-2026-05-29
source_type: error-note
status: raw
related_branches: [feature-boundary-validation-mapping-contract]
related_projects: [ca-skeleton]
tags: [error, ca-skeleton, archunit, fitness-function, dependency-direction, clean-architecture]
created: 2026-05-29
status_label: resolved
---
# error: mapping-exception-location-archunit-catch-2026-05-29
> Layer: `raw/errors/` — 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 또는 `wiki/troubleshooting/` 직접 생성 근거가 아니다.
## Parent / 부모
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — B7 outbound ACL 참조 추가 직후 발생.
## 증상
`./gradlew test` 실행 시 `app-bootstrap:test` 에서 `outbound_adapter_does_not_depend_on_web_or_persistence_adapters` ArchUnit assertion 실패. 단일 위반 메시지: `dev.caskeleton.sample.ticket.adapter.outbound.weather.WeatherForecastAclMapper``dev.caskeleton.sample.ticket.adapter.web.error.MappingException` 에 의존.
## 근본 원인 / Root cause
B3 결정 (`MAPPING_FAILED` 카테고리) 의 sentinel `MappingException` 을 sample-ticket 의 *adapter-web* error 패키지 (`sample.ticket.adapter.web.error.MappingException`) 에 둔 게 초기 결정이었다. 그 시점에는 mapper-internal 예외를 web 의 `GlobalExceptionHandler` 가 catch 하는 패턴만 고려했기 때문에 자연스러워 보였다.
B7 outbound ACL 매퍼 (`WeatherForecastAclMapper`) 가 동일한 sentinel 을 던지도록 추가하자, outbound adapter 가 *web* adapter 에 의존하게 된다. 이는 `..adapter.outbound..``..adapter.web..` 방향으로 sibling-adapter 의존이 발생하는 것이며, Clean Architecture 의 모듈 매트릭스에 정면으로 위배. ArchUnit 의 `outbound_adapter_does_not_depend_on_web_or_persistence_adapters` 규칙 (모듈 간 의존 방향 강제) 이 정확히 이 회귀를 catch.
## 해결 / Resolution
`MappingException``sample.ticket.application.exception` 으로 이전. 기존 `DuplicateEmailException`, `UserNotFoundException` 등 다른 application exception 들과 같은 패키지. 의존 방향이 다시:
```
adapter.web -> application.exception (GlobalExceptionHandler 가 catch)
adapter.outbound -> application.exception (ACL mapper 가 throw)
```
로 정렬되어 outbound → web 의존이 사라진다.
수정 후 `./gradlew verifyCleanArchitectureDependencies` + `./gradlew test` 모두 PASS.
## 회고 / Lessons
- **클래스의 *위치* 도 boundary contract 의 일부다.** "exception 은 어디서 catch 되는가" 보다 "exception 은 어디서 throw 되는가" 가 패키지 결정의 1순위. throw 지점이 여러 adapter 라면 application 패키지에 두어야 *cross-adapter* 의존을 만들지 않는다.
- 이 결정은 boundary contract 가 한 결정 (B3) 안에서 *완결* 되지 않고, 추가 사용처 (B7) 가 나타나면 위치를 재평가해야 한다는 것을 보여준다.
- **fitness function 은 contract 의 변경 비용을 측정하는 도구.** ArchUnit 규칙이 없었다면 outbound 가 web 에 의존하는 상태로 머지될 수 있었고, 그 다음에 다른 outbound adapter 가 추가될 때까지 누구도 알아채지 못했을 가능성. 규칙이 *변경에 따라 새로 위반이 생긴 시점에 즉시 알람* 하는 게 핵심 가치.
- 이번 catch 는 [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]] 의 *vacuous-pass 함정* 과 정반대 경험 — 규칙이 우연히 0 match 가 아니라 *진짜* 위반을 잡았다는 정상 동작의 확인.
## 재발 가능성
- 신규 adapter 추가 시 cross-cutting exception/value 의 위치를 application 패키지에 두는 컨벤션이 정착되어 있지 않으면 반복 가능. 본 branch 의 `adapter-web/CLAUDE.md` 보강에 "cross-adapter 에서 throw 되는 sentinel 은 application.exception 에 둔다" 항목 추가 권장 (별도 PR 후속).
## Sources / 근거
- `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java``outbound_adapter_does_not_depend_on_web_or_persistence_adapters` 정의.
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — adapter 간 sibling 의존 차단 결정.
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — B3 (MAPPING_FAILED) + B7 (outbound ACL) 결정.