ArgumentResolver?
어떤 요청이 컨트롤러에 들어왔을 경우 요청에 들어온 값으로 원하는 객체를 만들어내는 일을 ArgumentResolver인터페이스를 활용하여 구현 할 수있다.
HandlerMethodArgumentResolver
ArgumentResolver는 HandlerMethodArgumentResolver를 구현함으로써 시작된다. 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 |