Spring 에서 제공하는 Pageable 을 사용해서 페이징 기능을 구현했다. 이때 F12 서비스에 맞지 않게 많은 page size 를 요청하는 경우 서비스 레이어에 들어오기 전에 제한할 필요가 있었다.
@GetMapping("/reviews")
public ResponseEntity<ReviewPageResponse> showPage(final Pageable pageable) {
final ReviewPageResponse reviewPageResponse = reviewService.findPage(pageable);
return ResponseEntity.ok(reviewPageResponse);
}
Spring 에서 제공하는 PageableHandlerMethodArgumentResolver 를 사용해서 기존 설정을 바꿀 수 없었다.
// PageableHandlerMethodArgumentResolverSupport
private static final int DEFAULT_MAX_PAGE_SIZE = 2000;
public class PagingRequest {
@Max(value = 100, message = "사이즈는 100 이하여야 합니다.")
private int size;
private int page;
private Sort sort;
private PagingRequest(final int size, final int page, final Sort sort) {
this.size = size;
this.page = page;
this.sort = sort;
}
public Pageable toPageRequest() {
return PageRequest.of(size, page, sort);
}
}
해당 DTO 는 ServletModelAttributeMethodProcessor 에 의해 바인딩 된다.(이유는 아래 추가 내용 참고) 이때 문제가 발생한다. ?sort=createdAt,desc 과 같은 형식을 Sort 객체로 바인딩하기 위해서는 SortHandlerMethodArgumentResolver(PageableHandlerMethodArgumentResolver 가 가지고 있다)가 필요하다.
public class CustomPageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolver {
@Override
public Pageable resolveArgument(final MethodParameter methodParameter, final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
String pageSize = webRequest.getParameter(getParameterNameToUse(getSizeParameterName(), methodParameter));
validate(pageSize);
return super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
}
private void validate(final String pageSize) {
if (Integer.parseInt(pageSize) > 100) {
throw new PageSizeOutOfBoundsException();
}
}
}
WebMvcConfigurer 를 구현해서 ArgumentResolver 를 등록해줬다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new CustomPageableHandlerMethodArgumentResolver());
}
}
size 값이 100이 넘으면 예외가 발생한다.
PageableHandlerMethodArgumentResolver 를 상속해서 page size 검증 기능을 추가한뒤 Pagealbe 을 처리하는 ArgumentResolver 가 두 개인 상황에서 커스텀한 PageableArgumentResolver 가 기존의 ArgumentResolver 를 100% 대신해서 동작하는지 확인할 필요가 있었다.
CustomPageableHandlerMethodArgumentResolver가 먼저 삽입됐기 때문에 CustomPageableHandlerMethodArgumentResolver 가 PageableHandlerMethodArgumentResolver 를 완전히 대신하게 됐다.
CustomPageableHandlerMethodArgumentResolver 가 먼저 삽입되는 이유는 단순하다. Spring Boot 에서는 디렉토리 순서(알파벳)대로 스캔하면서 bean 을 생성하기 때문에 WebConfig 가 먼저 등록되고
spring-data-commons 라이브러리의 (org.springframework.boot:spring-boot-starter-data-jpa
의존성을 추가하면 같이 추가된다, 여기에 PageableHandlerMethodArgumentResolver 가 들어있다) SpringDataWebConfiguration 가 등록된다.
어떠한 어노테이션도 붙이지 않았는데 Pageable은 어떻게 바인딩 되는 걸까?
@RequestParam
, @ModelAttribute
은 생략할 수 있으며 int, String 같은 단순한 자료형은 @RequestParam
으로 인식하고 그 외의 객체들은 @ModelAttribute
로 인식한다. 그래서 우리가 흔히 쓰는 @RequestBody
를 누락하면 자동으로 ServletModelAttributeMethodProcessor 가 argumentResolver로서 바인딩하게 된다.
그렇다면 Pageable은 SimpleValueType이 아니므로 당연히ServletModelAttributeMethodProcessor가 바인딩을 해줄 것이다. 하지만 그림과 같이 PageableHandlerMethodArgumentResolver가 선택된다.
이게 어떻게 가능한걸까? 이유는 단순했다.
// RequestMappingHandlerAdapter
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
if (KotlinDetector.isKotlinPresent()) {
resolvers.add(new ContinuationHandlerMethodArgumentResolver());
}
// 여기에서 PageableHandlerMethodArgumentResolver 가 등록된다.
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
RequestMappingHandlerAdapter 에서 ArgumentResolver들을 초기화할 때 ServletModelAttributeMethodProcessor 를 가장 마지막에 넣어줬기 때문이다.(항상 제일 마지막에 삽입된다, customAgumentResolver는 Catch-all 위에서 넣어준다)
PageableHandlerMethodArgumentResolver 는 파라미터의 어노테이션을 확인하지 않고 단지 파라미터가 Pageable 타입이면 된다.
// PageableHandlerMethodArgumentResolver
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Pageable.class.equals(parameter.getParameterType());
}
결국 우리는 Pageable을 사용하기 위해서는 어노테이션을 붙이지 않고 그저 Pageable 타입만 사용하면 된다.
진짜 좋네요,,,