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

4.4 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / mapping-exception-location-archunit-catch-2026-05-29 error-note raw
feature-boundary-validation-mapping-contract
ca-skeleton
error
ca-skeleton
archunit
fitness-function
dependency-direction
clean-architecture
2026-05-29 resolved

error: mapping-exception-location-archunit-catch-2026-05-29

Layer: raw/errors/ — 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석. canonical 정제 전 raw 후보이며, wiki/blog/ 또는 wiki/troubleshooting/ 직접 생성 근거가 아니다.

Parent / 부모

증상

./gradlew test 실행 시 app-bootstrap:test 에서 outbound_adapter_does_not_depend_on_web_or_persistence_adapters ArchUnit assertion 실패. 단일 위반 메시지: dev.caskeleton.sample.ticket.adapter.outbound.weather.WeatherForecastAclMapperdev.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

MappingExceptionsample.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-28vacuous-pass 함정 과 정반대 경험 — 규칙이 우연히 0 match 가 아니라 진짜 위반을 잡았다는 정상 동작의 확인.

재발 가능성

  • 신규 adapter 추가 시 cross-cutting exception/value 의 위치를 application 패키지에 두는 컨벤션이 정착되어 있지 않으면 반복 가능. 본 branch 의 adapter-web/CLAUDE.md 보강에 "cross-adapter 에서 throw 되는 sentinel 은 application.exception 에 둔다" 항목 추가 권장 (별도 PR 후속).

Sources / 근거