공부함

Pageable 본문

스프링

Pageable

찌땀 2023. 12. 8. 22:49

 

프로그레인저 프로젝트에서 페이지네이선에 Pageable 객체를 사용하도록 전체적인 수정사항이 있었다.

수정 예시

Pageable을 사용하는 편이 통일성도 있고, 코드도 간결해지고.. 여러 장점이 있는 것 같다.

다만 내가 구현한지 좀 지난 기능들이고 그때는 구현이 급해서 참고한 내용들을 따로 정리를 못했다.

그래서 관련 내용들이 기억이 잘 안난다..

내용을 복습하고 정리해보자. 

 

https://tecoble.techcourse.co.kr/post/2021-08-15-pageable/

 

Pageable을 이용한 Pagination을 처리하는 다양한 방법

Spring Data JPA에서 Pageable 를 활용한 Pagination 의 개념과 방법을 알아본다. Pageable을 활용한 Pagination…

tecoble.techcourse.co.kr

 

Pageable이란?

Pageable이전에 Pagination이 뭘까?

Pagination(페이지네이션)은 정렬 방식, 페이지의 크기, 몇 번째 페이지인지의 요청에 따라 정보를 전달해주는 것이다. 

게시판처럼 페이지 별로 나눠서 응답할 때 많이 사용한다.

개발자가 직접 Pagination 기능을 구현해서 사용할 수 있다.

하지만 JPA에서는 Pagination을 편하게 사용할 수 있는 Pageable 객체를 제공한다. 

쿼리파라미터로 요청을 하면 원하는 형식의 데이터를 제공한다. 

 

JPA에서 Pageable 활용

Spring Data JPA를 활용하면서 Pageable을 어떻게 활용할까?

@Repository
public interface SolutionRepository extends JpaRepository<Solution, Long> {
	public List<Solution> findByMember(Membe member, Pageable pageable);
    ...
}

@Controller
public class SolutionController{
	@GetMapping("/solutions/{id}")
    public List<SolutionResponse> findByMember(@PathVariable Long memberId, Pageable pageable){
    	...
    }
    ...
}

레포지토리에서 Pageable을 인자로 받으면 JpaRepository가 해당하는 형태의 데이터를 반환해준다. 

GET/solutions/10?page=3&size=10&sort=id,DESC

위와 같은 url 요청이 오면 컨트롤러에서 page=3&size=10&sort=id,DESC에 해당하는 Pageagble 객체를 자동으로 만든다. 이 객체를 리포지토리에 그대로 넘겨주면 된다. 

 

	
    public List<Solution> findByMember(Membe member, Pageable pageable);
    
    public Page<Solution> findByMember(Membe member, Pageable pageable);
    
    public Slice<Solution> findByMember(Membe member, Pageable pageable);

리포지토리에 Pageable을 전달해서 얻는 반환값의 형태는 List, Page, Slice가 있다. 

Page는 전체 페이지를 알아야 한다. 따라서 count 쿼리가 별도 실행된다.

Slice는 전후의 Slice가 존재하는지 여부에 대한 정보를 갖고 있다. 

 

Pageable 객체가 생성되는 원리

 

컨트롤러에서 어떻게 GET/solutions/10?page=3&size=10&sort=id,DESC 형태가 Pageable 객체로 변한 것일까?

ArgumentResolver인 PageableHandlerMethodArgumentResolver에 의해 가능하다. 엥?무? 기억을 되살려 보자....

스프링 MVC 동작원리 (출처: 김영한 님 강의)

HTTP 요청이 들어오면 먼저 핸들러 매핑 정보를 통해 어떤 핸들러를 호출할지 정한다.핸들러를 그냥 호출하는 것이 아니라 알맞은 핸들러 어댑터를 통해 호출해야 한다. 따라서 핸들러 어댑터 목록에서 알맞은 핸들러어댑터를 찾는다. 그리고 그 핸들러 어댑터가 핸들러를 호출한다. 

출처: 김영한님 강의

@RequestMapping(애노테이션 기반 컨트롤러)을 처리하는 핸들러 어댑터가 핸들러를 호출할 때, ArgumentResolver가 핸들러(컨트롤러)가 필요로 하는 파라미터를 생성한다. 다양한 종류의 ArgumentResolver가 있다. 원한다면 HandlerMethodArgumentResolver 인터페이스를 구현해서 사용할 수 있다. 

 

https://velog.io/@jidam03/HTTP-%EB%A9%94%EC%84%B8%EC%A7%80-%EC%BB%A8%EB%B2%84%ED%84%B0-4231l13d#-argumentresolver

 

HTTP 메세지 컨버터

출처 : 김영한 님 강의 스프링 MVC는 다음 경우에 HTTP 메세지 컨버터를 적용한다 HTTP 요청 : @RequestBody,HttpEntity(RequestEntity) HTTP 응답 : @ResponseBody, HttpEntity(ResponseEntity)

velog.io

(ArgumentResolver에 대한 자세한 내용은 위 링크를 참고하자)

 

다시 Pageable로 돌아와서..

우리가 사용하는 컨트롤러는 애노테이션 기반 컨트롤러이므로, ArgumentResolver 중 하나인 PageableHandlerMethodArgumentResolver 컨트롤러의 파라미터인 Pageable이 생성된다는 것이다! 

 

 

Pageable 기본값 설정

@PageDefault 애노테이션 

GET/solutions/10

 

위와 같이 Pageable에 해당하는 쿼리파라미터가 없는 요청에 대해서는 어떻게 동작할까? 

기본 설정에 의해 정렬되지 않은 size=20인 페이지 중 첫 페이지를 반환한다. 

@PageDefault 애노테이션을 활용해서 기본 설정을 바꿀 수 있다. 

 

@Controller
public class SolutionController{
	@GetMapping("/solutions/{id}")
    public List<SolutionResponse> findByMember(@PathVariable Long memberId, 
    	@PageDefault(size=10, sort="id", direction = Sort.Direction.DESC) Pageable pageable){
    	...
    }
    ...
}

 

위와 같이 @PageDefault 애노테이션을 활용해 기본값을 지정해줄 수 있다. 

그리고 @PageDefault도 기본 값이 있다!

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

 

PageableDefault (Spring Data Core 3.2.0 API)

Since: 1.6 Author: Oliver Gierke, Mark Paluch Optional Element Summary Optional Elements The direction to sort by. int The default page number the injected Pageable should use if no corresponding parameter defined in request (default is 0). int The default

docs.spring.io

 

Pageable의 속성값

공식문서를 확인해보면 page의 default값은 0, size의 default값은 10이다. 

즉 @PageDefault을 안 붙인 Pageable의 default값은 page 0, size 20이며 @PageDefault를 붙이면 default값은 page 0, size 20이 된다.


 

PageableHandlerArgumentResolver에 의해 컨트롤러에게 전달할 Pageable 객체가 생성된다. 

PageableHandlerArgumentResolver는 어떻게 스프링 빈으로 등록될까?

Configuration 파일인(빈 등록 파일) SpringDataWebConfiguration에서 빈으로 등록한다. spring-boot-starter-web의 의존성을 추가하면 자동으로 등록된다.

@Configuration(proxyBeanMethods=false)
public class SpringDataWebConfiguration {
  private @Autowired Optional<PageableHandlerMethodArgumentResolverCustomizer> pageableResolverCustomizer;

  @Bean
  public PageableHandlerMethodArgumentResolver pageableResolver() {

    PageableHandlerMethodArgumentResolver pageableResolver = new PageableHandlerMethodArgumentResolver(sortResolver.get());
    customizePageableResolver(pageableResolver);
    return pageableResolver;
  }

  protected void customizePageableResolver(PageableHandlerMethodArgumentResolver pageableResolver) {
    pageableResolverCustomizer.ifPresent(c -> c.customize(pageableResolver));
  }    
}

SpringDataWebConfiguration의 코드는 위와 같다. 

PageableHandlerMethodArgumentResolverCustomizer가 존재하면 적용하고 아닐 경우 기본 PageableHandlerMethodArgumentResolver로 적용한다. 

 

application.properties

PageableHandlerMethodArgumentResolverCustomizer Bean도 빈으로 등록이 될텐데 어디서 등록이 되는지 보자.

@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());
    };
  }    
}

 

Configuration인 SpringDataWebAutoConfiguration에서 등록한다. 

@ConditionalOnMissingBean은 Bean이 정의되지 않은 경우에만 등록한다. 즉 직접 Configuration 파일을 만들어서 (바로 아래에서 소개한다) PageableHandlerMethodArgumentResolverCustomizer Bean을 등록해놓은게 아닌 경우에만 위 설정 파일에 의해 등록된다.

위 설정 파일에 등록할 내용을 작성하는 방법은 application.properties에 ‘spring.data.web.pageable.default-page-size=100’과 같은 설정을 추가해주면 된다.

CustomPageableConfiguration

@Configuration
public class CustomPageableConfiguration {
    @Bean
    public PageableHandlerMethodArgumentResolverCustomizer customize() {
        return p -> p.setFallbackPageable(PageRequest.of(0, Integer.MAX_VALUE));
    }
}

그리고 위와 같이 Configuration 파일을 만들어서 PageableHandlerMethodArgumentResolverCustomizer Bean을 직접 등록할 수 있다.  사용자가 미리 등록한 빈이 있으므로 @ConditonalOnMissingBean에 의해 application.properties에 작성한 설정대로 빈이 생성되지 않는다. 

 

정리하자면 Pageable 기본값을 설정하는 방법은 3가지가 있다. 

1. 컨트롤러에서 @PageDefault 애노테이션 사용 

2. application.properties에서 ‘spring.data.web.pageable.default-page-size=100’과 같은 설정 추가 

3. CustomPageableConfiguration 파일을 만들어서 직접 커스텀 빈 등록

우선순위 : 1>3>2

 

Querydsl에서 Pageable 사용

https://jddng.tistory.com/345

 

Querydsl - Spring Data JPA에서 제공하는 페이징 활용

Spring Data JPA에서 제공하는 페이징 활용 QueryDSl에서 페이징 사용 Count 쿼리 최적화 Controller 개발 QueryDSL에서 페이징 사용 1. 커스텀 인터페이스에 메서드 추가 public interface MemberRepositoryCustom { List sea

jddng.tistory.com

SpringDataJpa에서는 인자로 Pageable을 넘겨주면 알아서 조건에 맞는 데이터를 제공한다.

동적 쿼리를 작성하기 위해 사용하는 Querydsl에서도 Pageable을 사용할 수 있다.

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
    List<MemberTeamDto> content = getMemberTeamDtos(condition, pageable);

    Long count = getCount(condition);

    return new PageImpl<>(content, pageable, count);
}

private Long getCount(MemberSearchCondition condition) {
    Long count = queryFactory
            .select(member.count())
            .from(member)
//                .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .fetchOne();
    return count;
}

private List<MemberTeamDto> getMemberTeamDtos(MemberSearchCondition condition, Pageable pageable) {
    List<MemberTeamDto> content = queryFactory
            .select(new QMemberTeamDto(
                    member.id,
                    member.username,
                    member.age,
                    team.id,
                    team.name))
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .offset(pageable.getOffset())   // 페이지 번호
            .limit(pageable.getPageSize())  // 페이지 사이즈
            .fetch();
    return content;
}
출처: https://jddng.tistory.com/345 [IT 개발자들의 울타리:티스토리]

 

offset(pageable.getOffset()), limit(pageable.getPageSize())와 같이 페이지 번호와 페이지 사이즈를 전달해서 원하는 데이터를 조회할 수 있다. 이 때 fetch()가 아닌 fetchResult()를 사용하면 데이터와 count를 같이 조회할 수 있다.

하지만 count에 상관없는 테이블을 조회하는 등의 성능 문제가 발생할 수 있어서 count 쿼리를 따로 작성하는 것이 좋다.

(fetchResult는 현재 deprecate 되었다.)

Page의 구현체인 PageImpl을 생성해서 반환하면 된다. 

Slice로 반환하고 싶으면 SliceImpl을 반환하면 된다.

 

Pageable 예외처리

https://jaeseo.tistory.com/entry/Pageable-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%ACfeat-PageableDefault

 

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

Pageable을 사용해 Pagination을 하는 과정에서, 요구 사항에 맞추어 예외를 처리해야 하는 경험을 했습니다. 저희 팀의 요구 사항은 아래와 같습니다. 요구 사항 page, size에 값이 모두 없을 경우, defaul

jaeseo.tistory.com

 

작성 예정 ..