RequestExceptionHandler.java
package com.project.auth.presentation.support.exception;
import com.project.auth.application.support.exception.CommonErrorCode;
import com.project.auth.application.support.logging.LogSanitizer;
import com.project.auth.presentation.support.response.ApiResult;
import com.project.auth.presentation.support.response.ApiResultFactory;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.ErrorResponse;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
public class RequestExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(RequestExceptionHandler.class);
private final ApiResultFactory apiResultFactory;
public RequestExceptionHandler(ApiResultFactory apiResultFactory) {
this.apiResultFactory = apiResultFactory;
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiResult<Void>> handleMessageNotReadableException(
HttpMessageNotReadableException exception,
HttpServletRequest request
) {
log.warn("Request body not readable. method={} requestPath={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
PresentationErrorCode.INVALID_REQUEST_BODY.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.INVALID_REQUEST_BODY))
.body(apiResultFactory.failure(
PresentationErrorCode.INVALID_REQUEST_BODY.code(),
PresentationErrorCode.INVALID_REQUEST_BODY.message()
));
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiResult<Void>> handleMethodNotSupportedException(
HttpRequestMethodNotSupportedException exception,
HttpServletRequest request
) {
log.warn("Method not supported. method={} requestPath={} attemptedMethod={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
LogSanitizer.normalize(exception.getMethod()),
PresentationErrorCode.METHOD_NOT_ALLOWED.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.METHOD_NOT_ALLOWED))
.body(apiResultFactory.failure(
PresentationErrorCode.METHOD_NOT_ALLOWED.code(),
PresentationErrorCode.METHOD_NOT_ALLOWED.message()
));
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ApiResult<Void>> handleMissingParameterException(
MissingServletRequestParameterException exception,
HttpServletRequest request
) {
log.warn("Missing request parameter. method={} requestPath={} parameter={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
LogSanitizer.normalize(exception.getParameterName()),
PresentationErrorCode.MISSING_PARAMETER.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.MISSING_PARAMETER))
.body(apiResultFactory.failure(
PresentationErrorCode.MISSING_PARAMETER.code(),
PresentationErrorCode.MISSING_PARAMETER.message()
));
}
@ExceptionHandler(TypeMismatchException.class)
public ResponseEntity<ApiResult<Void>> handleTypeMismatchException(
TypeMismatchException exception,
HttpServletRequest request
) {
log.warn("Type mismatch for parameter. method={} requestPath={} parameter={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
LogSanitizer.normalize(exception.getPropertyName()),
PresentationErrorCode.TYPE_MISMATCH.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.TYPE_MISMATCH))
.body(apiResultFactory.failure(
PresentationErrorCode.TYPE_MISMATCH.code(),
PresentationErrorCode.TYPE_MISMATCH.message()
));
}
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<ApiResult<Void>> handleMediaTypeNotSupportedException(
HttpMediaTypeNotSupportedException exception,
HttpServletRequest request
) {
log.warn("Unsupported media type. method={} requestPath={} contentType={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
LogSanitizer.normalize(String.valueOf(exception.getContentType())),
PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE))
.body(apiResultFactory.failure(
PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE.code(),
PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE.message()
));
}
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<ApiResult<Void>> handleMediaTypeNotAcceptableException(
HttpMediaTypeNotAcceptableException exception,
HttpServletRequest request
) {
log.warn("Not acceptable media type. method={} requestPath={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
PresentationErrorCode.NOT_ACCEPTABLE.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.NOT_ACCEPTABLE))
.body(apiResultFactory.failure(
PresentationErrorCode.NOT_ACCEPTABLE.code(),
PresentationErrorCode.NOT_ACCEPTABLE.message()
));
}
@ExceptionHandler(MissingRequestHeaderException.class)
public ResponseEntity<ApiResult<Void>> handleMissingRequestHeaderException(
MissingRequestHeaderException exception,
HttpServletRequest request
) {
log.warn("Missing request header. method={} requestPath={} header={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
LogSanitizer.normalize(exception.getHeaderName()),
PresentationErrorCode.MISSING_HEADER.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.MISSING_HEADER))
.body(apiResultFactory.failure(
PresentationErrorCode.MISSING_HEADER.code(),
PresentationErrorCode.MISSING_HEADER.message()
));
}
@ExceptionHandler(ServletRequestBindingException.class)
public ResponseEntity<ApiResult<Void>> handleServletRequestBindingException(
ServletRequestBindingException exception,
HttpServletRequest request
) {
log.warn("Request binding failed. method={} requestPath={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
PresentationErrorCode.REQUEST_BINDING_FAILED.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.REQUEST_BINDING_FAILED))
.body(apiResultFactory.failure(
PresentationErrorCode.REQUEST_BINDING_FAILED.code(),
PresentationErrorCode.REQUEST_BINDING_FAILED.message()
));
}
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ApiResult<Void>> handleNoResourceFoundException(
NoResourceFoundException exception,
HttpServletRequest request
) {
log.warn("No resource found. method={} requestPath={} resourcePath={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
LogSanitizer.requestPath(exception.getResourcePath()),
PresentationErrorCode.RESOURCE_NOT_FOUND.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.RESOURCE_NOT_FOUND))
.body(apiResultFactory.failure(
PresentationErrorCode.RESOURCE_NOT_FOUND.code(),
PresentationErrorCode.RESOURCE_NOT_FOUND.message()
));
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ApiResult<Void>> handleMaxUploadSizeExceededException(
MaxUploadSizeExceededException exception,
HttpServletRequest request
) {
log.warn("Upload payload too large. method={} requestPath={} maxBytes={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
exception.getMaxUploadSize(),
PresentationErrorCode.PAYLOAD_TOO_LARGE.code());
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.PAYLOAD_TOO_LARGE))
.body(apiResultFactory.failure(
PresentationErrorCode.PAYLOAD_TOO_LARGE.code(),
PresentationErrorCode.PAYLOAD_TOO_LARGE.message()
));
}
/**
* 더 구체적인 핸들러로 잡히지 않은, 프레임워크가 직접 던진 상태 캐리어 예외 처리.
* 상위에서 결정된 4xx vs 5xx 분기가 클라이언트까지 보존되도록 carriedStatus를 그대로 사용한다.
*
* 본문 분류: 5xx는 내부 분류 노출을 막기 위해 COMMON-999로 정규화하고, 4xx는
* UNHANDLED_CLIENT_ERROR로 폴백한다. 클라이언트 SDK가 "요청 잘못" vs "서버 결함"을
* 여전히 구분할 수 있게 하기 위함이다.
*
* ResponseStatusException과 ErrorResponseException은 둘 다 {@link ErrorResponse}를 구현하면서
* 동시에 {@link Throwable}을 상속하므로 로그 호출부의 (Throwable) 캐스트가 안전하다.
* 향후 Throwable이 아닌 ErrorResponse 구현체를 이 @ExceptionHandler 목록에 추가하면
* 런타임 ClassCastException이 발생하므로, 이 핸들러는 명시한 두 예외 타입으로만 한정한다.
*/
@ExceptionHandler({ResponseStatusException.class, ErrorResponseException.class})
public ResponseEntity<ApiResult<Void>> handleErrorResponseException(
ErrorResponse exception,
HttpServletRequest request
) {
HttpStatusCode carriedStatus = exception.getStatusCode();
int statusValue = carriedStatus.value();
if (statusValue >= 500) {
log.error("Framework-thrown 5xx status. method={} requestPath={} status={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
statusValue,
(Throwable) exception);
return ResponseEntity.status(carriedStatus)
.body(apiResultFactory.failure(
CommonErrorCode.INTERNAL_SERVER_ERROR.code(),
CommonErrorCode.INTERNAL_SERVER_ERROR.message()
));
}
log.warn("Framework-thrown 4xx status. method={} requestPath={} status={} errorCode={}",
request.getMethod(),
LogSanitizer.requestPath(request.getRequestURI()),
statusValue,
PresentationErrorCode.UNHANDLED_CLIENT_ERROR.code());
return ResponseEntity.status(carriedStatus)
.body(apiResultFactory.failure(
PresentationErrorCode.UNHANDLED_CLIENT_ERROR.code(),
PresentationErrorCode.UNHANDLED_CLIENT_ERROR.message()
));
}
}