Spring Data JPA에서 페이지네이션을 쉽게 도와준다.
이 기능을 이용해 내가 원하는 커스텀 어노테이션을 만들어봤다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface PaginationLimit {
int maxSize() default 2000;
}
메소드 단위로 사용하기 위해 @Target(ElementType.METHOD)
을 선언했다.
런타임까지 사용할 것이기에 @Retention(RetentionPolicy.RUNTIME)
을 선언했다.
@Component
public class PaginationArgumentResolver extends PageableHandlerMethodArgumentResolver {
@Override
public Pageable resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
final Pageable pageable = super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
if (methodParameter.hasMethodAnnotation(PaginationLimit.class)) {
final int maxSize = methodParameter.getMethodAnnotation(PaginationLimit.class).maxSize();
if (pageable.getPageSize() > maxSize) {
throw new IllegalArgumentException("Page size must be under " + maxSize);
}
}
return pageable;
}
}
Pageable 객체를 resolve 해주는 클래스가 따로 있다.
바로 PageableHandlerMethodArgumentResolver
인데 거기서 resolveArgument
함수만 오버라이드하여 정의해줬다.
설정한 최댓값보다 큰 지 확인하고 만약 더 크다면 예외를 터트려 rest controller advice에서 따로 처리해줬다.
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final PaginationArgumentResolver paginationArgumentResolver;
//JWT 인증 아규먼트 리졸버 추가
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(paginationArgumentResolver);
}
마지막으로 WebMvcConfigurer
에서 아규먼트 리졸버에 추가해줬다.
이제 컨트롤러 함수에 어노테이션으로 페이지의 최대 크기를 개별적으로 정할 수 있게 됐다.
@PaginationLimit(100)
@GetMapping("/v1/articles")
public Page<?> getArticles(Pageable pageable) {
return articleRepository.findAll(pageable);
}
100개 이상의 size를 요청하면 예외가 처리된다.
근데 커스텀 어노테이션으로 개별 최대 사이즈를 설정하는 것 말고도 글로벌 설정을 따로 정해두고 싶었다.
보통은 application.properties
나 application.yml
에 설정을 할 수 있다.
spring.data.web.pageable.max-page-size: 100
예를 들어 위에서처럼 정의하면 스프링에서 100개 이상의 요청에 대해 예외처리 없이 100개만 반환해준다.
그런데
내가 커스텀 어노테이션을 만들고 설정 등록하고 난 뒤에 이 글로벌 설정이 먹히질 않았다.
즉, 위 글로벌 설정을 하면
@GetMapping("/v1/articles")
public Page<?> getArticles(Pageable pageable) {
return articleRepository.findAll(pageable);
}
이 URL로 들어온 요청의 page size가 100을 초과하면 100개만 응답해야한다.
근데 글로벌 설정은 먹질 않고 default값인 2000이 최대 크기로 설정돼있다.
내부 구현 로직을 제대로 알지 못한 상태로 커스텀하는게 정말 위험하다는 것을 깨달았다.
application.yml
에 글로벌 설정을 했는데, 우선 spring.data.web.pageable.max-page-size
이 어디서 사용되고 있는지 확인해봤다.
SpringDataWebProperties
에서 pageable 객체의 setMaxPageSize 함수에서 최대 페이지 값을 설정해주고 있었다.
/**
* Configuration properties for Spring Data Web.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@ConfigurationProperties("spring.data.web")
public class SpringDataWebProperties {
private final Pageable pageable = new Pageable();
private final Sort sort = new Sort();
...
public void setMaxPageSize(int maxPageSize) {
this.maxPageSize = maxPageSize;
}
그럼 여기서 설정한 maxPageSize는 어디서 사용되고 있는지
getmaxPageSize()
함수를 따라가 봤다.
!!
문제가 됐던 곳을 찾았다.
SpringDataWebAutoConfiguration
에서 아래 PageableHandlerMethodArgumentResolverCustomizer
를 빈으로 등록하여 사용하고 있는데 잘 보면 @ConditionalOnMissingBean
이 있다.
@Configuration(proxyBeanMethods=false)
public class SpringDataWebAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public PageableHandlerMethodArgumentResolverCustomizer pageableCustomizer() {
return (resolver) -> {
Pageable pageable = this.properties.getPageable();
resolver.setPageParameterName(pageable.getPageParameter());
resolver.setSizeParameterName(pageable.getSizeParameter());
resolver.setOneIndexedParameters(pageable.isOneIndexedParameters());
resolver.setPrefix(pageable.getPrefix());
resolver.setQualifierDelimiter(pageable.getQualifierDelimiter());
resolver.setFallbackPageable(PageRequest.of(0, pageable.getDefaultPageSize()));
resolver.setMaxPageSize(pageable.getMaxPageSize());
};
}
}
PageableHandlerMethodArgumentResolverCustomizer
은 보통 유저가 페이지네이션을 커스텀하는데 사용할 수 있는 클래스이다.
근데
@ConditionalOnMissingBean
의 의미는 해당 Bean이 기존에 정의되지 않은 경우에만, 등록되도록 작업 되어 있다.
내가 PageableHandlerMethodArgumentResolver
를 상속하여 커스텀한 리졸버를 등록하면서 위 객체가 빈으로 등록되지 않은 것이다..
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final PaginationArgumentResolver paginationArgumentResolver;
//JWT 인증 아규먼트 리졸버 추가
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
paginationArgumentResolver.setMaxPageSize(100);
resolvers.add(paginationArgumentResolver);
}
일단 그래서 PageableHandlerMethodArgumentResolver
에 있는 setMaxPageSize()를 이용하여 글로벌 설정을 해줬다.
그런데 application.yml
에서 설정할 수 있는 것이 아니라 뭔가 찝찝하다.
추후에 스프링과 더 친해지고 나면 더 나은 해결책을 찾을 수 있으리라 믿는다...
페이지네이션 최댓값을 글로벌하게 설정하는 것 말고도 커스텀 에노테이션으로 개별 설정할 수 있게 만들어봤다.
그런데 커스텀을 하고 나서 기존 글로벌 설정이 동작하지 않는 문제가 있었다.
글로벌 설정은 내가 커스텀하여 직접 빈 설정을 하지 않은 경우에만 사용할 수 있는 문제가 있어 내가 등록한 빈에서 글로벌 설정을 함수를 호출하여 설정했다.
뭔가 석연치 않은 해결 방법이라 나중에 웃으며 이 글을 수정하기를 바라며 글을 마칩니다..