Java/Spring

[Spring] QueryString 객체로 받기

태감새 2023. 4. 23. 19:18

ArgumentResolver?

어떤 요청이 컨트롤러에 들어왔을 경우 요청에 들어온 값으로 원하는 객체를 만들어내는 일을 ArgumentResolver인터페이스를 활용하여 구현 할 수있다.

HandlerMethodArgumentResolver

ArgumentResolverHandlerMethodArgumentResolver를 구현함으로써 시작된다. HandlerMethodArgumentResolver인터페이스는 두개의 메서드를 제공한다.

public interface HandlerMethodArgumentResolver {  

    boolean supportsParameter(MethodParameter parameter);  

    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,  
         NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;  

}
  • supportsParameter
    • 해당 parameter가 Resolver에 의해 수행될 타입인지 판단 후 boolean을 반환
  • resolveArgument
    • parameter로 필요한 작업하는 메서드

HandlerMethodArgumentResolver실행시점?
당연하게도 Controller에 도달하기 전에 실행된다. handle을 실행하기 위해 필요한 parameter를 생성하기 위해서 Resolver가 실행된다.

프로젝트 상황

  • QueryString이 많아서 Controller의 매개변수가 너무 많음
  • 파라미터 값에 대한 유효성 처리와 Dto로의 전환을 Controller에서 수행
@GetMapping("/search")
public String searchByWord(
    Model model,
    @RequestParam(value = "query", defaultValue = "") String query,
    @RequestParam(value = "sort", defaultValue = "") Integer sort,
    @RequestParam(value = "year", defaultValue = "0") Integer year,
    @RequestParam(value = "star", defaultValue = "") Integer star,
    @RequestParam(value = "minPrice", defaultValue = "") Integer minPrice,
    @RequestParam(value = "maxPrice", defaultValue = "") Integer maxPrice,
    @RequestParam(value = "publish", defaultValue = "") String publish,
    @RequestParam(value = "author", defaultValue = "") String author,
    @RequestParam(value = "totalRow", defaultValue = "10") Integer totalRow,
    @RequestParam(value = "category", defaultValue = "") String category,
    @RequestParam(value = "babyCategory", defaultValue = "") String babyCategory,
    @RequestParam(value = "cursor", defaultValue = "1") Long cursor
) {
    FilterDto filter = createDto(query, sort, year, star, minPrice, maxPrice, publish, author, totalRow, category, babyCategory, cursor);
    BookListDto result = bookService.searchByCursor(filter);
    model.addAttribute("result", result);

    return "search";
}

코드에서는 생략했지만 createDto에서 기본값 설정 후 Dto 객체로 변환

Resolver 도입

@ParamToDto

  • 구현 Resolver인지 확인하기 위한 커스텀 Annotation
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.PARAMETER)  
public @interface ParamToDto {  
}

ParamToDtoResolver

@Component  
@Slf4j  
public class ParamToDtoResolver implements HandlerMethodArgumentResolver {  

   private ObjectMapper mapper;  

   public ParamToDtoResolver(ObjectMapper mapper) {  
      this.mapper = mapper;  
   }  

   // 해당 파라미터가 Resolver가 필요한 type인지 판단  
   @Override  
   public boolean supportsParameter(MethodParameter parameter) {  
      return parameter.getParameterAnnotation(ParamToDto.class) != null;  
   }  

   // Resolver가 필요하면 실행되는 메서드. 파라미터를 객체로 매핑하는 로직 구현  
   @Override  
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {  
      try {  
         // request 반환 
         HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest();  
         // URI -> Json 형태로 변경. \(%5C) -> \\(%5C%5C)로 replace
         String json = queryToJson(request.getQueryString()).replaceAll("%5C","%5C%5C");  
         // UTF-8로 디코딩
         String decodedJson = URLDecoder.decode(json, "UTF-8"); 
         // 객체에 Mapping 
         Object obj = mapper.readValue(decodedJson, parameter.getParameterType());  
         return obj;  
      } catch (Exception e) {  
         log.warn("Cause : {}, Message : {}" ,e.getCause(), e.getMessage());  
         throw new CustomException(ErrorCode.INVALID_PARAMETERS);  
      }  
   }  

   // parameter json으로 변환  
   private String queryToJson(String query) {  
      String res = "{\"";  
      for (int i = 0; i < query.length(); i++) {  
         if (query.charAt(i) == '=') {  
            res += "\"" + ":" + "\"";  
         } else if (query.charAt(i) == '&') {  
            res += "\"" + "," + "\"";  
         } else {  
            res += query.charAt(i);  
         }  
      }  
      res += "\"" + "}";  
      return res;  
   }  
}

Controller

@GetMapping("/search")
public String searchByWord(Model model, @ParamToDto FilterDto filter) {
    filter.checkParameterValid();
    BookListDto result = bookService.searchByCursor(filter);
    model.addAttribute("result", result);

    return "search";
}

트러블 슈팅

QueryString에 '&' or '=' 포함된 경우

  • 기존 코드
HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest();
String decodedJson = URLDecoder.decode(json, "UTF-8"); 
String json = queryToJson(request.getQueryString());  
Object obj = mapper.readValue(decodedJson, parameter.getParameterType()); 

queryToJson에서 예외 발생
-> Json으로 변경하는 조건절에 걸려서 생긴 문제라고 판단
-> Json 변환과 디코딩의 순서를 바꿔서 '&','='가 인식되지 않도록 변경

HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest();
String json = queryToJson(request.getQueryString()); 
String decodedJson = URLDecoder.decode(json, "UTF-8"); 
Object obj = mapper.readValue(decodedJson, parameter.getParameterType()); 

QueryString에 '\'이 포함된 경우

String에 '\'이 포함되면 이스케이프 문자로 인식하기 때문에 Object로 Mapping과정에 예외 발생
디코딩 전에 '%5C'-> '%5C%5C'로 replace해서 해결
'\' -> (encoding) '%5C'

HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest();
String json = queryToJson(request.getQueryString()).replaceAll("%5C","%5C%5C"); 
String decodedJson = URLDecoder.decode(json, "UTF-8"); 
Object obj = mapper.readValue(decodedJson, parameter.getParameterType()); 

참고

테코블

'Java > Spring' 카테고리의 다른 글

SpringMVC (3) - SpringMVC  (0) 2023.04.27
JPA 간단정리  (0) 2023.04.25
[Spring] Github Action 적용하기 (+ properties 추가)  (0) 2023.04.23
SpringMVC (2) - Servlet Container와 Spring Container  (0) 2023.04.20
SpringMVC (1) - WAS와 WebServer  (0) 2023.04.19