fragile and resilient

Spring

[Spring] Pageable 커스텀 예외 처리(feat. @PageableDefault)

Green Lawn 2022. 7. 5. 22:57

Pageable을 사용해 Pagination을 하는 과정에서, 요구 사항에 맞추어 예외를 처리해야 하는 경험을 했습니다.

저희 팀의 요구 사항은 아래와 같습니다.

요구 사항

  • page, size에 값이 모두 없을 경우, default로 page = 0, size = 5를 설정한다.
  • size에 0이 들어오거나 page, size에 문자열, 음수가 들어올 경우 400 에러를 응답한다.

Pageable을 사용하면서, 위 요구 사항에 맞추어 구현한 경험에 대해 공유하고자 합니다.

1) Pageable page, size Default 값 지정

@RestController
public class StudyController {

    //...

    @GetMapping("/api/studies")
    public ResponseEntity<StudiesResponse> getStudies(@PageableDefault(size = 5) Pageable pageable) {
        final StudiesResponse studiesResponse = studyService.getStudies(pageable);
        return ResponseEntity.ok().body(studiesResponse);
    }
}

Pageable 객체 앞에 @PageableDefault 애노테이션을 붙이면 기본값을 설정할 수 있습니다.

여기서 page와 size에 예외 값이 들어왔을 때 어떤 값으로 처리가 되는지 알면 좋을 것 같은데요.
총 4가지의 경우의 예외 예시를 살펴보면 좋을 것 같습니다.

@DisplayName("잘못된 페이징 정보로 목록을 조회시 400에러를 응답한다.")
@ParameterizedTest
@CsvSource({"1,0", "-1,1", "1,two", "two,1"})
public voidresponse400WhenRequestByInvalidPagingInfo(String page, String size) {
    RestAssured.given().log().all()
            .when().log().all()
            .get("/api/studies?page=" + page + "&size=" + size)
            .then().log().all()
            .statusCode(HttpStatus.BAD_REQUEST.value())
            .body("message",not(blankOrNullString()));
}


1) page = 1, size = 0이 들어온 경우 예외
→ page = 1, size = 5로 설정 → size는 1이상이어야 하므로, default로 지정한 5로 설정됩니다.

2) page = -1, size = 1이 들어온 경우 예외
→ page = 0, size = 1 로 지정됩니다. → page는 0이상이어야 하므로, default인 0으로 설정됩니다.

3) page = two, size = 1이 들어온 경우 예외
→ page = 0, size = 1 → page가 문자이므로, default인 0으로 설정됩니다.

4) page = 1, size = two이 들어온 경우 예외
→ page = 1, size = 5 → size가 문자이므로, defaul로 지정한 5로 설정됩니다.

public abstract class AbstractPageRequest implements Pageable, Serializable {

	//..

	private final int page;
	private final int size;

	/**
	 * Creates a new {@link AbstractPageRequest}. Pages are zero indexed, thus providing 0 for {@code page} will return
	 * the first page.
	 *
	 * @param page must not be less than zero.
	 * @param size must not be less than one.
	 */
	public AbstractPageRequest(int page, int size) {

		if (page < 0) {
			throw new IllegalArgumentException("Page index must not be less than zero");
		}

		if (size < 1) {
			throw new IllegalArgumentException("Page size must not be less than one");
		}

		this.page = page;
		this.size = size;
	}
}

AbstractPageRequest 클래스를 확인해 보면, page가 0보다 작은 경우size가 1보다 작을 경우 내부적으로 예외가 발생하는 것을 확인할 수 있습니다.

테스트 코드를 통해 예외 상황이 발생했을 시, 어떤 default 값이 지정되는지 확인해 볼 수 있었습니다.

2) Pageable page, size 예외 처리

저희 팀은 page와 size가 모두 null인 경우에만 default로 값을 지정하고, 그 이외의 문자와 음수 등의 예외 값이 들어올 경우 Exception을 던져 400을 주기로 했는데요.

따라서 Pageable에 값이 들어왔을 때, 값을 확인해서 예외 처리를 해야 했습니다.
이때, 만약 예외 값이 들어와도 정상적인(default) 값으로 변환을 해주기 때문에 외부에서 예외를 처리할 수 없는 문제점이 있었습니다.

이 문제를 해결하기 위해 pageable의 page와 size를 검증하는 Resolver를 따로 생성해서 예외를 처리한 후, PageHandlerMethodArgumentResolver에 넘겨주었습니다.

과정에 대해 살펴보겠습니다.

2.1) PageableResolverConfig로 custom으로 생성한 PageableVerificationArgumentResolver를 빈으로 등록합니다.

@Configuration
public class PageableResolverConfig implements WebMvcConfigurer {

    private final PageableVerificationArgumentResolver pageableVerificationArgumentResolver;

    public PageableResolverConfig(PageableVerificationArgumentResolver pageableVerificationArgumentResolver) {
        this.pageableVerificationArgumentResolver = pageableVerificationArgumentResolver;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(pageableVerificationArgumentResolver);
    }
}

 

2.2) PageableVerificationArgumentResolver의 resolveArgument 메서드에서 필요한 예외를 처리를 하고, 예외가 발생한 경우 400 에러를 던집니다.

@Component
public class PageableVerificationArgumentResolver extends PageableHandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return super.supportsParameter(parameter);
    }

    @Override
    public Pageable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                    NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        
				final String pageText = webRequest.getParameter("page");
        final String sizeText = webRequest.getParameter("size");

        /*
        *
        * 필요한 예외 검증 로직(null, isDigit, minimum value..)
        * 
        * */
        
        return super.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
}

PageableHandlerMethodArgumentResolver의 resolveArgument 메서드를 호출하여 Pageable 객체를 반환합니다.

public class PageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolverSupport
		implements PageableArgumentResolver {

	// ..

	@Override
	public Pageable resolveArgument(MethodParameter methodParameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {

		String page = webRequest.getParameter(getParameterNameToUse(getPageParameterName(), methodParameter));
		String pageSize = webRequest.getParameter(getParameterNameToUse(getSizeParameterName(), methodParameter));

		Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
		Pageable pageable = getPageable(methodParameter, page, pageSize);

		if (sort.isSorted()) {
			return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
		}

		return pageable;
	}
}

이렇게 구현하면, 아래의 두 요구 사항을 만족할 수 있습니다.

  • page, size에 값이 없을 경우 default로 page = 0, size = 5를 설정한다. @PageableDefault
  • size에 0이 들어오거나 page, size에 문자열, 음수가 들어올 경우 400 에러를 응답해야 한다. → PageableVerificationArgumentResolver

 


References

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/web/PageableHandlerMethodArgumentResolver.html
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Pageable.html