Files
llm-wiki/raw/errors/responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02.md
T

70 lines
4.5 KiB
Markdown

---
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<Object> 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`