4.5 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 / responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02 | error-note | raw |
|
|
|
2026-06-02 | 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:
@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.javasrc/adapter-web/src/test/java/dev/caskeleton/adapter/web/error/TransportErrorHandlingTest.java- Spring
org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler