siso 사이드프로젝트를 진행하면서 스프링에서 페이징 처리를 위해 제공하는 Pageable 인터페이스의 예외처리를 적용한 방법을 정리해보자.
커스텀 예외처리를 적용한 이유
- siso는 @RestControllerAdvice 에서 예외를 처리한다. Pageable 기본 예외처리 말고 내가 원하는 예외 타입을 던지고 정해놓은 형식에 맞게 처리되길 원했다. 예를 들어 page size -1 요청에 대해서 IllegalArgumentException: Page size must not be less than one! 가 발생하는데, 나는 내가 정의한 예외 타입 - 예외 코드에 의해 처리되길 바란다.
- Pageable은 page size가 -1 이라던가 하는 일반적으로 잘못된 요청에 대해서는 예외처리를 하지만, 더 다양한 상황에대해 예외처리하고 싶었다. 특히 정렬 방식에 대해서도 예외처리를 하고 싶었다.
- 컨트롤러마다 별도의 예외 처리 로직을 작성하면 중복되기 때문에 중복을 해소하고 싶었다.
해결 방법
@GetMapping("/{encryptedCongressmanId}")
public ResponseEntity<RatingListDTO> ratingList(
@PathVariable String encryptedCongressmanId,
@PageConfig(allowedSorts = {LIKE, DISLIKE, TOPICALITY, REG_DATE},
defaultSort = "topicality, DESC", defaultSize = 20) Pageable pageable,
@Validated @ModelAttribute CountCursor cursor, @LoginId(required = false) Long loginId) {
return ResponseEntity.ok(
ratingService.validateAndGetRecentRatings(encryptedCongressmanId, pageable, cursor, loginId)
);
}
@PageConfig 애너테이션, argumentresolver 를 사용했다.
PageConfig
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface PageConfig {
SortProperty[] allowedSorts() default SortProperty.ID; // 허용된 sort 값
String defaultSort() default "id, DESC"; // 기본 sort
int defaultPage() default 1; // 기본 page
int defaultSize() default 10; // 기본 size
}
페이징이 필요한 컨트롤러는 @PageConfig 가 달린 파라미터를 받도록 했다.
@PageConfig는 허용된 정렬 방식들 allowedSorts, 정렬 방식을 지정하지 않고 요청했을 때 지정할 정렬 방식 defaultSort, 기본 페이지, 기본 페이지 사이즈를 속성으로 갖는다.
컨트롤러에서 애너테이션 속성의 기본값을 설정하지 않을 경우는 @PageConfig 에서 정의한 기본값을 사용한다.
예를 들어 @PageConfig 를 사용할 때 허용 정렬 방식을 따로 지정하지 않으면 id 정렬만 허용하게 된다.
CustomPageableResolver
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(PageConfig.class) && Pageable.class.isAssignableFrom(
parameter.getParameterType());
}
@Override
public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
final PageConfig pageConfig = parameter.getParameterAnnotation(PageConfig.class);
final int page = getIntParam(webRequest, PAGE_PARAM, pageConfig.defaultPage());
final int size = getIntParam(webRequest, SIZE_PARAM, pageConfig.defaultSize());
final String sort = resolveSort(webRequest.getParameter(SORT_PARAM), pageConfig);
validatePageAndSize(page, size);
final String[] sortParts = sort.split(",");
final String property = sortParts[0].trim();
final boolean isDescending = sortParts.length == 2 && "DESC".equalsIgnoreCase(sortParts[1].trim());
// page - 1 ( 요청 : 1페이지 -> 실제 : 0페이지 )
return PageRequest.of(page - 1, size,
isDescending ? Sort.by(property).descending() : Sort.by(property).ascending());
}
... 생략
private void validatePageAndSize(final int page, final int size) {
if (page <= 0) {
throw new CustomException(PageableErrorCode.INVALID_PAGE);
}
if (size < 1) {
throw new CustomException(PageableErrorCode.INVALID_SIZE);
}
}
private boolean isValidSort(final String sort, final SortProperty[] allowedSorts) {
if (sort == null || sort.trim().isEmpty()) {
return true; // sort가 없으면 통과
}
final String[] parts = sort.split(",");
if (parts.length == 1) {
return isAllowedSort(parts[0].trim(), allowedSorts); // 정렬 속성만 있어도 통과
}
if (parts.length == 2) {
final String property = parts[0].trim();
final String direction = parts[1].trim().toUpperCase();
return isAllowedSort(property, allowedSorts) && isAllowedDirection(direction);
}
return false;
}
private static boolean isAllowedDirection(final String direction) {
if ("ASC".equals(direction) || "DESC".equals(direction)) {
return true;
}
throw new CustomException(PageableErrorCode.UNSUPPORTED_SORT_DIRECTION);
}
private boolean isAllowedSort(final String property, final SortProperty[] allowedSorts) {
for (final SortProperty allowedSort : allowedSorts) {
if (allowedSort.getValue().equals(property)) {
return true;
}
}
throw new CustomException(PageableErrorCode.UNSUPPORTED_SORT_PROPERTY);
}
... 생략
PageConfig 애너테이션이 달린 파라미터에 대해 해당 리졸버가 동작하며, 속성들에 대해 검증한다.
검증 실패 시 예외를 던지며, 내가 원하는 예외를 던질 수 있어서 @RestControllerAdvice 에서 공통적으로 처리할 수 있어 편리하다.
결과적으로 Pageable 구현체를 반환하므로 페이징 기능은 그대로 사용할 수 있다.
'스프링' 카테고리의 다른 글
외부 API 예외 처리 전략: 내 서비스와 매핑하는 방법 (0) | 2025.02.03 |
---|---|
트랜잭션 내부에서 외부 api 호출하지 말자 (0) | 2025.02.03 |
spring 요청 Validation 검증, 예외 처리 (0) | 2025.01.16 |
테스트 시 @Value 사용 (0) | 2024.11.04 |
dto를 사용해야 하는 이유 (0) | 2024.09.01 |