저는 페이지네이션을 하면서 page? slice? 오 그거 좋다고? 와 그냥 되네? 싶어서 쓰기만 했는데, 과연 이게 무엇이며, 무한스크롤에는 slice 쓰던데 page로 그냥 통일해서 쓰면 안돼? 라고 생각했던걸 한번 적어보겠습니다.
결론부터 얘기하자면,
pagination은 Page, 무한스크롤은 Slice을 사용하는게 좋습니다!!
백문이 불여일견! 먼저 코드를 보여드리겠습니다
public BaseResponse<Page<InfouDocument>> RecentInfouList(@PageableDefault(sort="id", direction = Sort.Direction.DESC) Pageable pageable){
Page<InfouDocument> infouDocuments = infouService.recentInfou(pageable);
return new BaseResponse(infouDocuments);
}
public Page<InfouDocument> recentInfou(Pageable pageable){
Page<InfouDocument> all = infouRepository.findAll(pageable);
return all;
}
public interface InfouRepository extends ElasticsearchRepository<InfouDocument, Long>, CrudRepository<InfouDocument, Long> {
Page<InfouDocument> findAll(Pageable pageable);
}
이렇게 하게 되면

{
"isSuccess": true,
"code": 1000,
"message": "성공했습니다.",
"result": {
"content": [
{
"id": 6757567657
},
{
"id": 67676
},
{
"id": 33333
},
{
"id": 6666
},
{
"id": 34
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 5,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"last": true,
"totalPages": 1,
"totalElements": 5,
"size": 5,
"number": 0,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"first": true,
"numberOfElements": 5,
"empty": false
}
}
이런 식으로 페이징이 돼서 나오게 됩니다.
Spring Data JPA에서 Pageable을 처리하는 핵심 클래스인 SimpleJpaRepository의 코드를 한번 살펴보겠습니다!
@Override
public Page<T> findAll(Pageable pageable) {
return findAll((Specification<T>) null, pageable);
}
@Override
public Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable) {
return findAll(spec, spec, pageable);
}
@Override
public Page<T> findAll(@Nullable Specification<T> spec, @Nullable Specification<T> countSpec, Pageable pageable) {
TypedQuery<T> query = getQuery(spec, pageable);
return pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) : readPage(query, getDomainClass(), pageable, countSpec);
}
제 프로젝트 코드를 실행시킨다고 하면, SimpleJpaRepository 코드의 findAll 부분에서 첫번째->두번째->세번째 순서로 실행되게 됩니다.
세번째 메서드를 한번 봐봅시다!
@Override
public Page<T> findAll(@Nullable Specification<T> spec, @Nullable Specification<T> countSpec, Pageable pageable) {
TypedQuery<T> query = getQuery(spec, pageable);
return pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) : readPage(query, getDomainClass(), pageable, countSpec);
}
public Page<T> findAll(@Nullable Specification<T> spec, @Nullable Specification<T> countSpec, Pageable pageable)
TypedQuery<T> query = getQuery(spec, pageable);
return pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) : readPage(query, getDomainClass(), pageable, countSpec);
1. getQuery(spec, pageable) -> JPQL 쿼리를 만들고 LIMIT OFFSET 적용
TypedQuery<T> query = getQuery(spec, pageable);
[참고]
동적 쿼리 생성 예시
- spec을 통해 다양한 조건을 AND, OR로 결합하여 SQL 쿼리를 만듭니다.
- 예를 들어, where name = 'John' and age = 25 같은 조건을 Specification을 사용해 동적으로 생성할 수 있습니다.
2. readPage(query, getDomainClass(), pageable, countSpec)
readPage(query, getDomainClass(), pageable, countSpec);
변환되는 SQL 예시
SELECT * FROM my_table ORDER BY id LIMIT 10 OFFSET 20;
대략적인 JPA 동작을 알았으니, 이제 관련된 인터페이스에 대해 알아봅시다.
페이징과 관련된 인터페이스들은 page, slice, pageable 이렇게 3개를 알면 됩니다. Spring Data JPA가 이러한 인터페이스들을 이용해 페이징을 쉽게 할 수 있도록 구현이 되어 있습니다!
Page, Slice, Pageable는 모두 Spring Data에서 제공하는 인터페이스입니다.
아래 그림을 보면 package 명을 보면 모두 spring data에서 제공하는 인터페이스라는 것을 알 수 있습니다.
출처: https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html
출처: https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Slice.html
출처: https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Pageable.html
각 인터페이스에 대해 좀 더 뜯어보겠습니다!
Page 인터페이스를 알아보기 이전에 Slice 인터페이스에 대해 먼저 알아보도록 하겠습니다.
왜냐 Page가 Slice를 상속받는 형태이기 때문이죠
Slice에는 다양한 메소드들이 있습니다.


이 중에서 무한 스크롤에 관련된 핵심적인 부분만 보자면,
1. hasNext(): 다음 페이지가 존재하는지 여부를 확인하는 메서드입니다.
2. getContent(): 현재 페이지의 데이터를 반환합니다.
Page는 앞서 얘기했듯, Slice를 상속받고 있습니다.

즉, Slice에서 제공하는 기능과 추가적인 기능이 더 있다는 것을 의미합니다.
Pageable: Spring Data에서 페이징 정보를 전달하는 인터페이스
추가적인 메소드를 한번 살펴보겠습니다

핵심적인 부분은 getTotalElements과 getTotalPages입니다.
즉, 총 요소 개수와 총 페이지를 더 받을 수 있다는 것입니다.
그렇기 때문에

(화질 구린건..죄송합니다..)
이렇게 총 페이지 수를 보여줘야 하는 서비스에서 주로 사용이 됩니다!
이렇기 때문에, Slice에서 count를 한번 더 해주는 작업이 필요해 무한 스크롤 기능에서는 너무 투머치한 인터페이스(와 객체)라고 생각할 수 있습니다.
앞에서 미리 스포를 했습니다.
결국엔 무한스크롤인 경우 slice, 페이지네이션인 경우 page가 좋습니다. 왜냐하면, 무한스크롤인 경우에는 현재 페이지 데이터와 다음 페이지 여부만 있으면 되기 때문입니다! Page를 썼다가는 count 연산까지 하기 때문에 성능이 떨어지게 됩니다
좋은 글이네요.