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

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
feature-api-contract-baseline
ca-skeleton
error
ca-skeleton
spring-mvc
exception-handler
ResponseEntityExceptionHandler
api-contract
2026-06-02 resolved

error: responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02

Layer: raw/errors/ — 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석.

Parent / 부모

증상

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.java
  • src/adapter-web/src/test/java/dev/caskeleton/adapter/web/error/TransportErrorHandlingTest.java
  • Spring org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler