트러블슈팅

[트러블슈팅] org.springframework.web.bind.MissingServletRequestParameterException 에러

2023. 1. 9. 23:02
목차
  1. 1. 개요
  2. 2. 해결
  3. 3. 참고

1. 개요

기존에 단건 조회 기능만 있던 상황에서 새롭게 복수 조회 기능을 추가했는데, 모니터링 툴 상에서 갑자기 500 에러가 급증하는 것을 보고 급히 확인에 들어갔다. 에러 로그와 API 구성(재현 코드)는 아래와 같았다.

에러 로그

org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter '...' for method parameter type List is not present
	at org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.handleMissingValueInternal(RequestParamMethodArgumentResolver.java:218)
	at org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.handleMissingValue(RequestParamMethodArgumentResolver.java:193)
	at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:114)
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
    ...

컨트롤러 (재현코드)

// 단건 조회
@GetMapping("/getSomething/{id}")
fun getSingleData(@PathVariable("id") @NotBlank id: String): Data? {
    ...
}

// 복수 조회(신규 추가) <- 문제가 발생하는 API
@GetMapping("/getSomething")
fun getMultipleData(@RequestParam("ids") ids: List<String>): List<Data> {
    ...
}

 

파라미터가 요청에 포함되지 않은 것이 에러의 근본적인 원인이었다.

{host}/getSomething?ids=1,2,3,4,5    → 이런 식으로 요청이 와야 했는데

{host}/getSomething/                            → 이렇게 요청이 들어왔다..

 

이와 별개로 모니터링 툴에 에러가 급증한 원인은 MissingServletRequestParmeterException이 500에러로 떨어졌기 때문이다. 스프링은 해당 예외에 대해서 기본적으로 400(Bad Request) 에러를 떨구지만, 현재 내가 진행중인 프로젝트 구조에선 익셉션 핸들러가 여러 개 정의되어 있어 직접 지정한 특정 예외에 대해서만 4XX 에러를 반환하는 형태를 취하고 있으며 500에러는 모두 모니터링 툴에 노출되는 상황이다.

커스텀 익셉션 핸들러가 없으면 스프링은 MissingServletRequestParameterException에 대해 400에러를 반환한다

 

 

2. 해결

1) Bean Validation으로 처리 → 실패

처음엔 @NotEmpty 애너테이션을 붙여 Bean Validation을 수행하면 ConstraintViolationException이 발생할 것이고, ConstraintViolationException은 현재 익셉션 핸들러에 등록되어 있으므로 4XX 에러가 떨어질 것이라고 생각했다.

 

컨트롤러

@Validated // 중요! RequestParam과 PathVariable에 Bean Validation을 적용하기 위해선 이걸 꼭 붙여줘야 한다
@RestController
@RequestMapping("/")
class ExampleController(...) {

    // 단건 조회
    @GetMapping("/getSomething/{id}")
    fun getSingleData(@PathVariable("id") @NotBlank id: String): Data? {
        ...
    }

    // 복수 조회(신규 추가) <- 문제가 발생하는 API
    @GetMapping("/getSomething")
    fun getMultipleData(@RequestParam("ids") ids: List<String>): List<Data> {
        ...
    }

}

익셉션 핸들러

@RestControllerAdvice
class CustomExceptionHandler {

    @ExceptionHandler(value = [ConstraintViolationException::class])
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handleConstraintViolationException(exception: ConstraintViolationException, webRequest: WebRequest): ErrorResponse {
        return ErrorResponse(...)
    }
    
    // ...
    
}

 

하지만 여전히 MissingServletRequestParmeterException이 발생했다. 이유는 바로 RequestParam을 처리하는 RequestParamMethodArgumentResolver의 동작방식 때문이었다. RequestParamMethodArgumentResolver 클래스는 AbstractNamedValueMethodArgumentResolver 클래스를 상속받는데, 요청 파라미터의 value가 null인 경우에는 MissingServletRequestParmeterException(또는 MultipartException)을 발생시킨다. 

 

*RequestParamMethodArgumentResolver의 동작방식

  • 스프링 코드상의 RequestParam을 바탕으로 namedValueInfo와 nestedParamter 객체를 만들고 실제 Request Parameter의 value(resolveArgument.arg)를 구한다
  • 이때 value가 null이면 request parameter의 옵션여부(required=false인지 여부)와 default값이 존재하는지 여부에 따라 분기하여 argument를 생성해준다
    • 이때, value가 null인 경우는 쿼리 파라미터의 name이 완전히 생략된 경우이다
      • ex) {host}/getSomething/
    • RequestParam의 기본설정인 required = true인 경우, 쿼리 파라미터의 name이 존재하면 해당 타입의 기본값이 들어간다(String인 경우는 "", List인 경우는 [])
      • ex) {host}/getSomething?ids=
      • 단, required = false인 경우엔 null이 들어간다
  • 만약 해당 파라미터가 필수(required=true)이면서 default값이 존재하지 않는다면 handleMissingValue()를 호출하여 MissingServletRequestParmeterException(또는 MultipartException)을 발생시킨다

 

AbstractNamedValueMethodArgumentResolver

public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {

   //...


   @Override
   @Nullable
   public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
      
      // 파라미터명, 옵션여부, default값에 대한 정보를 가지고 있다.
      NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
      // 보다 구체적인 파라미터에 대한 정보(타입, 애너테이션 등)에 대한 정보를 가지고 있다.
      MethodParameter nestedParameter = parameter.nestedIfOptional();

      Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
      if (resolvedName == null) {
         throw new IllegalArgumentException(
               "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
      }

      Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
      if (arg == null) {
         if (namedValueInfo.defaultValue != null) {
            arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
         }
         else if (namedValueInfo.required && !nestedParameter.isOptional()) { // parameter가 필수적인지 체크
            handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); // 여기서 걸림!!
         }
         arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
      }
      else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
         arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
      }
      
      //...
    }
    
    //...   
}

 

RequestParamMethodArgumentResolver - 위의 AbstractNamedValueMethodArgumentResolver를 상속한다

public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
		implements UriComponentsContributor {

    // ...

	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
		HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);

		if (servletRequest != null) {
			Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
			if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
				return mpArg;
			}
		}

		Object arg = null;
		MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
		if (multipartRequest != null) {
			List<MultipartFile> files = multipartRequest.getFiles(name);
			if (!files.isEmpty()) {
				arg = (files.size() == 1 ? files.get(0) : files);
			}
		}
		if (arg == null) {
			String[] paramValues = request.getParameterValues(name);
			if (paramValues != null) {
				arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
			}
		}
		return arg;
	}
    
	@Override
	protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request)
			throws Exception {

		handleMissingValueInternal(name, parameter, request, false);
	}

	// 최종적으로 Exception이 던져지는 메서드
	protected void handleMissingValueInternal(
			String name, MethodParameter parameter, NativeWebRequest request, boolean missingAfterConversion)
			throws Exception {

		HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
		if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
			if (servletRequest == null || !MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
				throw new MultipartException("Current request is not a multipart request");
			}
			else {
				throw new MissingServletRequestPartException(name);
			}
		}
		else {
			throw new MissingServletRequestParameterException(name,
					parameter.getNestedParameterType().getSimpleName(), missingAfterConversion); // 결국 여기서 걸린다!!
		}
	}
}

 

 

즉, Bean Validation은 ArgumentResolver를 통해 핸들러(컨트롤러)에 전달될 요청 객체가 생성된 후에 수행되는데 이 경우엔 ArgumentResolver 내부에서 에러가 발생하여 Bean Validation이 이뤄지지 않은 것이었다.

 

이 문제와 별개로 아래 글을 보고 컨트롤러에 @Validated가 없어도 된다고 생각해서 처음엔 붙이지 않았는데, 그렇게 하니 Validation이 적용되지 않았다(글 자체는 너무 좋은 글이다). 따라서 RequestParam이나 PathVariable에 Bean Validation을 적용할 땐 @Validated를 꼭 붙여주자. 참고로 ConstraintValidator를 사용할 때도 붙여줘야 한다. 사실상 특별한 이유가 없으면 꼭 붙여주도록 하자.

https://meetup.nhncloud.com/posts/223

 

2) 익셉션 핸들러에 추가 → 성공

따라서 컨트롤러 대신 MissingServletRequestParmeterException를 핸들링하는 익셉션 핸들러를 추가하고 커스텀 에러로 변환하는 처리를 해줬다. 비록 해결방법은 간단했지만, 이번 에러를 해결하면서 스프링 MVC의 작동원리에 대해 다시금 공부할 수 있었다.

@RestControllerAdvice
class CustomExceptionHandler {
    
    @ExceptionHandler(value = [MissingServletRequestParameterException::class])
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handleMissingServletRequestParameterException(exception: MissingServletRequestParameterException, webRequest: WebRequest): ErrorResponse {
        return ErrorResponse(...)
    }
    
    // ...
    
}

 

수정 이후 에러가 확연히 줄었다

 

 

3. 참고

  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.html
저작자표시 비영리 변경금지 (새창열림)

'트러블슈팅' 카테고리의 다른 글

[트러블슈팅] JpaPagingItemReader 사용 시 doReadPage 메서드 내에서 NullPointerException 발생  (0) 2023.04.16
[트러블슈팅] gradle 빌드시 java.lang.IllegalAccessError: class org.gradle.internal.compiler.java.ClassNameCollector 발생  (0) 2023.03.28
[트러블슈팅] Kotlin에서 An annotation argument must be a compile-time constant이 발생하는 문제  (0) 2023.01.06
[트러블슈팅] inteilliJ에서 Syntax highlighting has been temporarily because of an internal error 발생  (1) 2022.11.24
[트러블슈팅] org.springframework.dao.InvalidDataAccessApiUsageException: javax.persistence.Query.executeUpdate requires active transaction  (0) 2022.11.23
  1. 1. 개요
  2. 2. 해결
  3. 3. 참고
'트러블슈팅' 카테고리의 다른 글
  • [트러블슈팅] JpaPagingItemReader 사용 시 doReadPage 메서드 내에서 NullPointerException 발생
  • [트러블슈팅] gradle 빌드시 java.lang.IllegalAccessError: class org.gradle.internal.compiler.java.ClassNameCollector 발생
  • [트러블슈팅] Kotlin에서 An annotation argument must be a compile-time constant이 발생하는 문제
  • [트러블슈팅] inteilliJ에서 Syntax highlighting has been temporarily because of an internal error 발생
코택
코택
코택
TaxFree
코택
전체
오늘
어제
  • 분류 전체보기 (369)
    • Spring (29)
      • Spring (18)
      • 스프링 핵심 원리 - 고급편 (11)
    • Spring Batch (4)
    • JPA (4)
    • CS (89)
      • 자료구조 (2)
      • 네트워크 (5)
      • 운영체제 (1)
      • 데이터베이스 (4)
      • SQL (7)
      • 알고리즘 이론 (4)
      • 알고리즘 문제 풀이 (66)
    • 웹 (28)
      • React.js (4)
      • Next.js (1)
      • Node.js (14)
      • FastAPI (4)
      • Django (5)
    • 프로그래밍 언어 (45)
      • Python (5)
      • Java + Kotlin (29)
      • JavaScript + TypeScript (11)
    • 테스트코드 (26)
      • ATDD, 클린 코드 with Spring (4)
      • 이규원의 현실 세상의 TDD: 안정감을 주는 코드.. (20)
    • 인프라 (6)
      • AWS (2)
      • Kubernetes (4)
    • 트러블슈팅 (25)
    • 책 (89)
      • Effective Java (54)
      • Effective Kotlin (14)
      • 도메인 주도 개발 시작하기: DDD 핵심 개념 정.. (11)
      • 웹 프로그래머를 위한 데이터베이스를 지탱하는 기술 (6)
      • 도메인 주도 설계 첫걸음 (4)
    • Git (10)
    • 회고 (5)
    • etc (8)

블로그 메뉴

  • 홈
  • 방명록
  • 관리
  • GitHub
  • LinkedIn

공지사항

  • 스킨 관련

인기 글

태그

  • BOJ
  • http
  • atdd
  • Shortest Path
  • dp
  • 장고
  • 그래프 탐색
  • Git
  • mysql
  • 브루트포스
  • 백준
  • 파이썬
  • 깊이 우선 탐색
  • fastapi
  • 그래프

최근 댓글

최근 글

hELLO · Designed By 정상우.
코택
[트러블슈팅] org.springframework.web.bind.MissingServletRequestParameterException 에러
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.