Spring

@RequestParam 사용 시 주의사항

ybs 2021. 9. 11. 23:58
반응형

요즘은 스프링 컨트롤러를 만들 때 @GetMapping, @PostMapping 으로 HTTP Method 를 명시하거나, @RequestMapping을 쓰더라도 HTTP Method 를 명시적으로 추가하는게 일반적이다.

 

그런데 예전 레거시 스프링 컨트롤러를 재개발 하는 상황에서 @RequestMapping 에 HTTP Method 가 명시적으로 적혀있지 않거나, 있다고 하더라도 GET, POST 둘다 받도록 하는 경우가 있어 똑같이 맞춰서 개발해야 했다.

 

GET + @RequestParam 인 경우는 query parameters(query string) 만 고려해서 처리하면 되서 간단한데, 

POST 요청까지도 같이 받는다면 @RequestParam 이 query parameters(query string) 뿐만 아니라, form data(body로 key1=value1&key2=value2 넘어오는 케이스, content-type은 application/x-www-form-urlencoded), 그리고 multipart 요청도 받을 수 있다는걸 인지하고 있어야 한다. 이는 Spring MVC 에서 해당되며 Servlet 스펙이다.

 

cf) Spring Webflux 는 @RequestParam 이 query parameters(query string) 만 받아준다.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestParam.html

 

RequestParam (Spring Framework 5.3.9 API)

Whether the parameter is required. Defaults to true, leading to an exception being thrown if the parameter is missing in the request. Switch this to false if you prefer a null value if the parameter is not present in the request. Alternatively, provide a d

docs.spring.io

 

문제는 이제부터 시작인데, 이전 request body 한번만 읽어올수 있는 제약 글에서 설명한것처럼 request body를 여러번 읽기 위해 만든 CacheBodyFilter 를 통해 CachedBodyHttpServletRequest 로 감싸서 body 정보를 캐시 하는 경우

@Component
public class CacheBodyFilter extends OncePerRequestFilter {
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
		throws ServletException, IOException {
		filterChain.doFilter(new CachedBodyHttpServletRequest(request), response);
	}
}

컨트롤러에서 POST + @RequestParam 일 때 form data 를 읽어오지 못한다. body 캐시를 위해 만들어진 CachedBodyHttpServletRequest 를 이용하면서 기존 로직을 안타게 되므로 충분히 그럴 수 있겠다는 생각은 들지만 정확히 어느 구간에서 발생하는 이슈인지 확인해보자.

 

@RequestParam 값을 가져오는데 getParameterValues 메서드를 이용한다.

public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
		implements UriComponentsContributor {
	...
	
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    	...
        
        String[] paramValues = request.getParameterValues(name);
        
        ...
    }
}

일반적으로 RequestParamMethodArgumentResolver 에서 request.getParameterValues(name) 을 호출하면 org.apache.catalina.connector.Request 구현체로 간다.

@Override
public String[] getParameterValues(String name) {

    if (!parametersParsed) {
        parseParameters();
    }

    return coyoteRequest.getParameters().getParameterValues(name);
}

처음이면 parametersParsed 가 false 니까  parseParameters 메서드 실행된다.

protected void parseParameters() {
	...
    
	parameters.processParameters(formData, 0, len);
    
	...
}

여기서 formData를 갖고 org.apache.tomcat.util.http.Parameters 클래스 processParameters 메서드를  호출하는데 거기서 내부적으로 addParameter 메서드를 호출하고 paramHashValues 에 add한다.  

public void addParameter( String key, String value ) throws IllegalStateException {
	...
	
	paramHashValues.put(key, values);

	...
}

 

하지만 RequestBodyCacheFilter 를 통하는 경우, RequestParamMethodArgumentResolver 에서 request.getParameterValues(name) 을 호출하지만 내부적으로 CachedBodyHttpServletRequest 의 getParameterValues 를 찾는데 해당 메서드가 구현이 안되어 있으므로 값을 못갖고 오는것이다.

 

다시말해 @RequestParam 원래 스펙은 form data 정보도 읽어올 수 있지만 body 캐시로 인해 못하게 되는것이다. 자체 구현한 CachedBodyHttpServletRequest 에서 그 처리를 해주는 것도 하나의 방법이다. 

 

아래 코드는 POST + body 일 때 추가적으로 ybs 값을 얻어오는 로직이다.

@RequestMapping("/path")
public String hello(HttpServletRequest request, @RequestParam(value = "YBS") String ybs) {

  // POST + body 면 추가적으로 얻는 작업 수행해야됌
  if ("POST".equalsIgnoreCase(request.getMethod()) && isRequestUsingBody(request)) {

    // try-catch 생략
    byte[] requestBodyBytes = new byte[request.getContentLength()];
    ServletInputStream inputStream = request.getInputStream();
    inputStream.read(requestBodyBytes);
        
    // convert 로직도 특별한건 없어서 생략했다(key=value 쌍을 파싱해서 Map에 저장).
    Map<String, Stringp[]> paramMap = convert(new String(requestBodyBytes, StandardCharsets.UTF_8));
    ybs = paramMap.get("YBS");
  }
}

private boolean isRequestUsingBody(HttpServletRequest request) {
  return request.getContentLength() > 0 && isBlank(request.getQueryString());
}

 

cf) getParameterMap 도 내부적으로 getParameterValues 를 이용하므로, POST + @RequestParam 상황에서 form data를 못갖고 오는건 똑같다.

 

 

반응형