| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 마이크로서비스
- ci/cd
- 도커
- 아키텍처
- 공통모듈
- java
- Java 8
- 트러블슈팅
- MSA
- 백엔드
- dockercompose
- 백엔드면접준비
- 자바
- Flyway
- gradle
- 인프라
- docker
- 마이크로서비스아키텍처
- 컨테이너
- 멀티모듈
- 분산시스템
- GCP
- 마이그레이션
- springboot
- SpringCloud
- github actions
- PostgreSQL
- GitHub Packages
- Database
- CS
- Today
- Total
NYO_O
페이징 표준화를 위한 Custom PageResolver 본문
지난 시간, Enum과 계층형 YAML을 결합한 전역 예외 처리 구조를 공통 모듈에 구축하는 방법을 살펴보았습니다. 오늘은 공통 모듈에 담을 두 번째 기능으로 페이징 표준화를 다루어 보겠습니다.
2026.05.27 - [Tech/MSA] - 공통 모듈 적용 - 일관된 전역 예외 처리 로직 구축
공통 모듈 적용 - 일관된 전역 예외 처리 로직 구축
지난 시간, 공통 모듈을 도메인 서비스에 연동하는 전체 과정을 살펴보았습니다. Auto-configuration을 구성해두면 의존성 선언만으로 공통 모듈의 기능이 각 서비스에 자연스럽게 녹아드는 구조였
ddangnyo.tistory.com
MSA 환경에서 페이징 처리를 각 서비스에 맡겨두면 서비스마다 허용하는 페이지 크기가 달라지는 문제가 생깁니다.
"이 서비스는 size=100도 되는데 저 서비스는 왜 안 되지?"
클라이언트 입장에서는 일관성 없는 동작이 혼란스럽고, 서버 입장에서는 클라이언트가 size=10000 같은 값을 넘겨도 그대로 처리해버리는 보안/성능 이슈가 생길 수 있습니다. 공통 모듈에 페이징 정책을 한 곳에서 정의해두면 모든 서비스가 동일한 기준으로 동작하게 됩니다. 오늘은 이 구조를 어떻게 구현하는지 정리해 보겠습니다.
Spring의 기본 페이징 처리 방식
Spring MVC는 컨트롤러 파라미터에 Pageable을 선언하면 PageableHandlerMethodArgumentResolver가 요청의 page, size, sort 쿼리 파라미터를 자동으로 바인딩해줍니다.
@GetMapping("/items")
public Page<ItemResponse> getItems(Pageable pageable) {
return itemService.getItems(pageable);
}
// GET /items?page=0&size=20&sort=createdAt,desc
편리한 기능이지만 기본 설정만으로는 클라이언트가 넘기는 size 값을 그대로 수용합니다. 별도 제한을 걸지 않으면 size=10000 같은 요청도 그대로 처리될 수 있습니다.
CustomPageableArgumentResolver
PageableHandlerMethodArgumentResolver를 상속받아 resolveArgument()를 오버라이드하는 방식으로 구현합니다. 부모 클래스가 이미 쿼리 파라미터 바인딩을 처리해주기 때문에, 우리는 바인딩된 결과에서 size 값만 검증하면 됩니다.
package org.gupang.common.web.resolver;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.util.List;
public class CustomPageableArgumentResolver extends PageableHandlerMethodArgumentResolver {
private static final List<Integer> ALLOWED_SIZES = List.of(10, 30, 50);
private static final int DEFAULT_SIZE = 10;
@Override
public Pageable resolveArgument(MethodParameter methodParameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
Pageable pageable = super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
if (!ALLOWED_SIZES.contains(pageable.getPageSize())) {
return PageRequest.of(pageable.getPageNumber(), DEFAULT_SIZE, pageable.getSort());
}
return pageable;
}
}
동작 방식은 간단합니다. 부모 클래스의 resolveArgument()를 먼저 호출해서 쿼리 파라미터를 Pageable 객체로 변환한 뒤, size 값이 허용 목록(10, 30, 50)에 없으면 기본값인 10으로 교체합니다. page와 sort는 클라이언트가 요청한 값을 그대로 유지합니다.
허용 목록에 없는 값이 들어왔을 때 예외를 던지는 방법도 있습니다. 하지만 클라이언트가 실수로 잘못된 값을 넘겼을 때 400 에러로 응답하는 것보다, 서버가 정책에 맞는 기본값으로 조용히 보정해주는 방식이 사용자 경험 측면에서 더 자연스럽다고 판단해서 이 방식을 선택했습니다.
WebConfig 등록
작성한 CustomPageableArgumentResolver를 Spring MVC가 인식할 수 있도록 WebMvcConfigurer에 등록합니다.
package org.gupang.common.config;
import org.gupang.common.web.resolver.CustomPageableArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CustomPageableArgumentResolver());
}
}
공통 모듈에 @Configuration 클래스를 작성해두고 Auto-configuration으로 등록해두면, 도메인 서비스는 의존성 선언만으로 이 설정이 자동으로 적용됩니다. 페이징 정책을 바꾸고 싶을 때도 공통 모듈의 ALLOWED_SIZES와 DEFAULT_SIZE만 수정하면 모든 서비스에 일괄 반영됩니다.
한 가지 주의할 점이 있습니다. Spring Boot는 PageableHandlerMethodArgumentResolver를 자동으로 등록하는데, 우리가 직접 CustomPageableArgumentResolver를 addArgumentResolvers()로 추가하면 두 리졸버가 함께 등록됩니다. ArgumentResolver는 목록에서 앞에 등록된 것이 먼저 처리되기 때문에 CustomPageableArgumentResolver가 먼저 매칭되어 정상적으로 동작하지만, 더 명확하게 처리하고 싶다면 @EnableSpringDataWebSupport의 자동 등록을 비활성화하는 방법도 고려해볼 수 있습니다.
도메인 서비스에서의 사용
공통 모듈이 Auto-configuration으로 연동되어 있다면 도메인 서비스에서는 별도 설정 없이 Pageable을 그대로 사용하면 됩니다.
@GetMapping("/items")
public Page<ItemResponse> getItems(Pageable pageable) {
// size=100으로 요청해도 자동으로 10으로 보정됩니다
return itemService.getItems(pageable);
}
허용된 size 값과 그렇지 않은 경우의 동작을 정리하면 다음과 같습니다.
| 요청 파라미터 | 실제 적용 size |
| size=10 | 10 (허용) |
| size=30 | 30 (허용) |
| size=50 | 50 (허용) |
| size=20 | 10 (기본값으로 보정) |
| size=100 | 10 (기본값으로 보정) |
| size 없음 | 10 (Spring 기본값) |
마무리
오늘은 PageableHandlerMethodArgumentResolver를 확장하여 허용된 페이지 크기만 처리하는 CustomPageableArgumentResolver를 공통 모듈에 구축하는 방법을 살펴보았습니다. 구현 자체는 간단하지만, 공통 모듈에 한 번만 정의해두면 모든 서비스에 동일한 페이징 정책이 보장된다는 점에서 꽤 효과적인 방법이었습니다.
다음 시간에는 공통 모듈이 점점 커지면서 자연스럽게 따라오는 고민, 역할별로 모듈을 어떻게 분리할 것인지에 대한 전략을 다루어 보겠습니다.
'Architecture > MSA' 카테고리의 다른 글
| 공통 모듈 적용 - 일관된 전역 예외 처리 로직 구축 (0) | 2026.05.27 |
|---|---|
| 공통 모듈의 역할별 분리 전략 (0) | 2026.05.27 |
| 공통 모듈 의존성 주입 및 연동 가이드 (0) | 2026.05.27 |
| GitHub Packages를 활용한 공통 모듈 배포 파이프라인 구축 (0) | 2026.05.27 |
| 공통 모듈, 무엇을 담고 무엇을 빼야할까? (1) | 2026.05.27 |