JPA Pagination 처리 방법
실무에서 게시글 같은 화면 구성요소를 보았을떄, 페이지와 무한스크롤이 되는 경우로 나뉘어져 구현하게 됩니다. 소규모 프로젝트에서는 전체 게시글 수와 페이지의 각 번호가 있는 Page 많이 사용하게 되고, 데이터가 많거나 실시간 앱 화면 같은 경우는 무한 스크롤인 Slice를 통해 페이지를 구현하게 됩니다. JPA에서는 어떻게 일반적인 페이징을 처리하고, 무한 스크롤을 구현하는지 알아보겠습니다.
Pageable이란?
Pageable은 Spring에서 제공하는 Pagination을 위한 인터페이스 입니다. 이 Pageable을 활용하여 Page 또는 Slice의 요청, 응답에 대한 전달을 합니다.
PageRequest 활용
PageRequest은 Pageable의 페이지 정보를 생성하는 클래스입니다. 이를 통해 Pageable 인터페이스를 구현할 수 있으며 페이지 번호, 페이지 크기, 정렬기준에 대한 값을 받아 구현하게 됩니다.
//pageRequest 객체 생성 예시
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by("id").ascending());
Page, Slice Interface
각 인터페이스의 구성요소는 다음과 같습니다.
- Page Interface
public interface Page<T> extends Slice<T> {
int getTotalPages(); //총 페이지 수
long getTotalElements(); //총 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); // 변환
}
- Slice Interface
public interface Slice<T> extends Streamable<T> {
int getNumber(); // 현재 페이지
int getSize(); // 페이지 크기
int getNumberOfelements(); // 현재 페이지에 나올 데이터 수
List<T> getContent(); // 조회된 데이터
boolean hasContent(); // 조회된 데이터 존재 여부
Sort getSort(); // 정렬 정보
boolean isFirst(); // 현재 페이지가 첫 번째 페이지인지 여부
boolean isLast(); // 현재 페이지가 마지막 페이지인지 여부
boolean hasNext(); // 다음 페이지 여부
boolean hasPrevious(); // 이전 페이지 여부
Pageable getPageable(); // 페이지 요청 정보
Pageable nextPageable(); // 다음 페이지 객체
Pageable previousPageable(); // 이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> convert); // 변환
}
Page 구현 예제
offset, limit, sort를 전달받아 전체 게시글 수와 페이지의 각 번호가 있는 Page를 반환하게 됩니다. Page는 일반적인 게시판 형태의 페이징에서 주로 사용됩니다. 또한 Pageable을 파라미터로 두어서 쿼리 메서드로 만들거나, QueryDSL로 구현 할 수 있습니다.
//쿼리 메서드를 활용한 Page
public Page<Post> findAll(Pageable pageable);
//QueryDSL Page
@Override
public Page<PostResBody> findAllByPost(Pageable pageable) {
List<PostResBody> list = jpaQueryFactory.select(PostResBody.qBean(post))
.from(post)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long count = jpaQueryFactory.select(post.count())
.from(post)
.fetchOne();
return new PageImpl<>(list, pageable, count);
}
- offset과 limit을 활용하여 전체 카운트가 있는 Page를 반환하게 됩니다.
- QueryDSL에서 카운트 쿼리는 조건을 대상으로 위의 코드와 같이 총 데이터 수를 반환합니다.
Slice 구현 예제
데이터가 많거나 실시간 앱 화면 같은 경우는 Slice를 통해 무한 스크롤 형태를 구현하게 됩니다. 전체 데이터 건수가 필요하지 않기 때문에 추가적인 count 쿼리가 실행되지 않으며, limit와 offset-Id로 마지막 번호 기준으로 데이터를 가져오게 되고, 첫 페이지와 마지막 페이지의 존재 여부를 데이터와 함께 반환해 줍니다.
//쿼리 메서드를 활용한 Slice
public Page<Post> findAll(Pageable pageable);
//QueryDSL Slice
@Override
public Slice<PostResBody> findAllBySlice(Long offsetId, Pageable pageable) {
List<PostResBody> list = jpaQueryFactory.select(PostResBody.qBean(post))
.from(post)
.where(post.id.lt(offsetId)) //무한 스크롤 구현 시 pk.id < offsetId
.limit(pageable.getPageSize() + 1) //다음 페이지 여부를 알기 위함
.orderBy(post.id.desc()) // id desc
.fetch();
boolean hasNextPage = hasNextPage(list, pageable.getPageSize());
return new SliceImpl<>(list, pageable, hasNextPage);
}
//다음 페이지가 있는지에 대한 여부
private boolean hasNextPage(List<PostResBody> body, int pageSize) {
if (body.size() > pageSize) { // body.size가 11 / page.size가 10 이라면,
body.remove(pageSize); // 다음 페이지가 존재함 return true;
return true;
}
return false; // 작다면 다음 페이지가 없음 return false;
}
- 무한 스크롤 구현 시 노출된 대상은 다시 스크롤에 나오지 않기 위해서 id < offset-Id로 처리하게 되며 id를 기준으로 내림차순 정렬해줍니다.
- 무한 스크롤 구현시 정렬 기준에 따라 Order by Case가 변경될 수 있습니다.
- 다음 페이지 여부를 알기 위해 받아온 출력 페이지 수에 대하여 +1을 해주게 됩니다.
- 다음 페이지 여부와 함께 Slice를 생성하여 데이터를 반환합니다.
이번 글에는 JPA에서의 Page와 Slice에 대해 알아보았습니다. 개발은 항상 이해하고 적용하는 것보다 사용해보고 동작 원리를 파악하는 것이 머리에 잘 들어온다고 생각합니다. JPA에서 처음 Page와 Slice를 사용하게 되었을 때, 이 글을 보고 가벼운 개념과 사용 방법에 대해 알아가셨으면 좋겠습니다. 읽어주셔서 감사합니다.
'Spring' 카테고리의 다른 글
[JPA] ManyToMany를 사용하지 않는다면? (0) | 2024.03.02 |
---|---|
[JPA] Batch Insert로 대량의 데이터 처리하기 (0) | 2024.02.27 |
[JPA] JPQL개념과 사용법 패턴 정리 (0) | 2024.02.22 |
[JPA] QueryDSL의 페이징 카테시안 곱 문제 해결 방법 (1:N Pagination) (0) | 2024.02.16 |
[JPA] QueryDSL에서 JPA N+1 문제 해결하기 (0) | 2024.02.13 |