--- title: error / responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02 source_type: error-note status: raw related_branches: [feature-api-contract-baseline] related_projects: [ca-skeleton] tags: [error, ca-skeleton, spring-mvc, exception-handler, ResponseEntityExceptionHandler, api-contract] created: 2026-06-02 status_label: resolved --- # error: responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02 > Layer: `raw/errors/` — 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석. ## Parent / 부모 - [[raw/branch-notes/feature-api-contract-baseline]] — D8 (413 payload-too-large) 핸들러 추가 중 발생. ## 증상 `feature-api-contract-baseline` D8 구현으로 `GlobalExceptionHandler` 에 `@ExceptionHandler(MaxUploadSizeExceededException.class) handlePayloadTooLarge(...)` 를 추가하자, `adapter-web` 의 **모든** MockMvc standalone 테스트가 `setUp()` 의 `.build()` 에서 `IllegalStateException` 으로 실패. 기존에 통과하던 `EnvelopeMetaIntegrationTest` 까지 동반 실패. 메시지: ``` java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [ExceptionHandler{exceptionType=org.springframework.web.multipart.MaxUploadSizeExceededException, mediaType=*/*}]: {public ... GlobalExceptionHandler.handlePayloadTooLarge(MaxUploadSizeExceededException), public final ... ResponseEntityExceptionHandler.handleException(Exception, WebRequest) ...} ``` ## 근본 원인 / Root cause `GlobalExceptionHandler extends ResponseEntityExceptionHandler`. Spring 의 `ResponseEntityExceptionHandler.handleException(...)` 는 `@ExceptionHandler({ ... MaxUploadSizeExceededException.class, ... })` 우산(umbrella) 핸들러로, `MaxUploadSizeExceededException` 을 이미 자신의 매핑 대상으로 *선점* 한다. 같은 예외 타입에 대해 서브클래스가 별도 `@ExceptionHandler` 메서드를 추가하면 동일 (exceptionType, mediaType=*/*) 키에 두 핸들러가 등록되어 매핑이 모호(ambiguous)해지고, 핸들러 advice 등록 시점(`.build()` / 컨텍스트 기동)에 즉시 실패한다. 핵심: `ResponseEntityExceptionHandler` 가 *이미 다루는* 예외군(405/406/415/413 multipart/`HttpMessageNotReadable` 등)은 `@ExceptionHandler` 신규 메서드로 가로채면 안 되고, 대응하는 **protected `handleXxx(...)` 메서드를 override** 해야 한다. ## 해결 / Resolution `@ExceptionHandler(MaxUploadSizeExceededException.class)` 메서드를 제거하고 protected 훅을 override: ```java @Override protected ResponseEntity handleMaxUploadSizeExceededException( MaxUploadSizeExceededException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return new ResponseEntity<>( ErrorResponseFactory.body(OperationalError.PAYLOAD_TOO_LARGE, "...", null), HttpStatusCode.valueOf(OperationalError.PAYLOAD_TOO_LARGE.httpStatus())); } ``` 406(`handleHttpMediaTypeNotAcceptable`) 도 동일하게 override 로 추가. 405 의 `Allow` 헤더 누락 수정 역시 기존 override 안에서 `responseHeaders.setAllow(...)` 로 처리. 수정 후 `:adapter-web:test` PASS. ## 회고 / Lessons - **`ResponseEntityExceptionHandler` 를 상속하면, 그가 이미 선언한 예외는 `@ExceptionHandler` 가 아니라 protected override 로만 커스터마이즈한다.** 새 `@ExceptionHandler` 는 그 우산이 다루지 *않는* 예외(`MappingException`, `ConstraintViolationException`, 도메인 예외 등)에만 쓴다. - 실패가 한 테스트가 아니라 advice 를 쓰는 *모든* standalone MockMvc 테스트에서 `.build()` 시점에 터지는 건, 런타임 요청 처리 이전 *핸들러 등록* 단계의 정합성 문제라는 신호. - 어떤 예외가 우산에 포함되는지는 Spring 버전마다 늘어난다(예: `MaxUploadSizeExceededException`, `ErrorResponseException`, `HandlerMethodValidationException`). 신규 transport 핸들러 추가 시 먼저 `ResponseEntityExceptionHandler` 의 `@ExceptionHandler` 목록을 확인. ## 재발 가능성 - file-resource 브랜치가 multipart 413(`UPLOAD_SIZE_EXCEEDED`)을 추가할 때 동일 함정 가능 — override 를 더 구체화하거나 별도 advice 를 `@Order` 로 앞세우는 방식 필요. ## Sources / 근거 - `src/adapter-web/src/main/java/dev/caskeleton/adapter/web/error/GlobalExceptionHandler.java` - `src/adapter-web/src/test/java/dev/caskeleton/adapter/web/error/TransportErrorHandlingTest.java` - Spring `org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler`