[Spring boot] JPA에서 Paging 동작 내부와 Slice, Page 차이 알아보기

하비·2025년 2월 2일

Spring boot

목록 보기
4/5

저는 페이지네이션을 하면서 page? slice? 오 그거 좋다고? 와 그냥 되네? 싶어서 쓰기만 했는데, 과연 이게 무엇이며, 무한스크롤에는 slice 쓰던데 page로 그냥 통일해서 쓰면 안돼? 라고 생각했던걸 한번 적어보겠습니다.

결론부터 얘기하자면,
pagination은 Page, 무한스크롤은 Slice을 사용하는게 좋습니다!!

Paging 코드 어떻게?

백문이 불여일견! 먼저 코드를 보여드리겠습니다

  • Controller
public BaseResponse<Page<InfouDocument>> RecentInfouList(@PageableDefault(sort="id", direction = Sort.Direction.DESC) Pageable pageable){
    Page<InfouDocument> infouDocuments = infouService.recentInfou(pageable);
    return new BaseResponse(infouDocuments);
}
  • Service
public Page<InfouDocument> recentInfou(Pageable pageable){
    Page<InfouDocument> all = infouRepository.findAll(pageable);
    return all;
}
  • Repository
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
  }
}

이런 식으로 페이징이 돼서 나오게 됩니다.

JPA에서 Paging

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

출처:
https://github.com/spring-projects/spring-data-jpa/blob/main/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

제 프로젝트 코드를 실행시킨다고 하면, 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)
  • spec: 검색 조건을 담은 Specification 객체입니다. 동적 쿼리 조건을 설정하는 데 사용됩니다.
  • countSpec: 페이징을 위한 전체 데이터 개수 조회를 위한 Specification 객체입니다.
  • pageable: 페이징을 위한 Pageable 객체로, page, size와 같은 정보를 담고 있습니다.

TypedQuery<T> query = getQuery(spec, pageable);
return pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) : readPage(query, getDomainClass(), pageable, countSpec);
  • 페이징 없음 (isUnpaged()가 true일 때): query.getResultList()로 전체 데이터를 가져와 PageImpl로 래핑하여 반환.
  • 페이징 있음 (isUnpaged()가 false일 때): readPage()를 호출하여 페이징 처리된 결과와 전체 데이터 개수를 이용해 Page 객체를 반환.

1. getQuery(spec, pageable) -> JPQL 쿼리를 만들고 LIMIT OFFSET 적용

TypedQuery<T> query = getQuery(spec, pageable);
  • getQuery(spec, pageable) 메서드는 주어진 Specification을 사용해 *동적 쿼리를 생성합니다.
  • spec은 Specification 타입으로, 검색 조건을 정의하는 객체입니다.
  • pageable은 페이지에 대한 정보(page, size 등)를 포함하고 있습니다.

[참고]
동적 쿼리 생성 예시

  • spec을 통해 다양한 조건을 AND, OR로 결합하여 SQL 쿼리를 만듭니다.
  • 예를 들어, where name = 'John' and age = 25 같은 조건을 Specification을 사용해 동적으로 생성할 수 있습니다.

2. readPage(query, getDomainClass(), pageable, countSpec)

readPage(query, getDomainClass(), pageable, countSpec);
  • pageable을 사용해 페이징 처리된 데이터를 반환하려면, 전체 데이터 개수도 필요합니다. readPage()는 countSpec을 사용하여 전체 데이터의 개수를 계산하고, 이 값을 바탕으로 전체 페이지 수를 계산합니다.
  • 데이터 조회: query.getResultList()를 사용해 페이지에 맞는 데이터를 가져옵니다.
  • 전체 데이터 개수 조회: countSpec을 사용해 전체 데이터 개수를 계산합니다.
  • 페이지 계산: 전체 데이터 개수와 pageable 정보를 사용하여 전체 페이지 수를 계산하고, Page 객체로 반환합니다.

변환되는 SQL 예시

SELECT * FROM my_table ORDER BY id LIMIT 10 OFFSET 20; 

대략적인 JPA 동작을 알았으니, 이제 관련된 인터페이스에 대해 알아봅시다.

Page, Slice, Pageable

페이징과 관련된 인터페이스들은 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

각 인터페이스에 대해 좀 더 뜯어보겠습니다!

Slice

Page 인터페이스를 알아보기 이전에 Slice 인터페이스에 대해 먼저 알아보도록 하겠습니다.
왜냐 Page가 Slice를 상속받는 형태이기 때문이죠

Slice에는 다양한 메소드들이 있습니다.


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

Page

Page는 앞서 얘기했듯, Slice를 상속받고 있습니다.

즉, Slice에서 제공하는 기능과 추가적인 기능이 더 있다는 것을 의미합니다.

Pageable: Spring Data에서 페이징 정보를 전달하는 인터페이스
추가적인 메소드를 한번 살펴보겠습니다

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

(화질 구린건..죄송합니다..)

이렇게 총 페이지 수를 보여줘야 하는 서비스에서 주로 사용이 됩니다!
이렇기 때문에, Slice에서 count를 한번 더 해주는 작업이 필요해 무한 스크롤 기능에서는 너무 투머치한 인터페이스(와 객체)라고 생각할 수 있습니다.

결론

앞에서 미리 스포를 했습니다.
결국엔 무한스크롤인 경우 slice, 페이지네이션인 경우 page가 좋습니다. 왜냐하면, 무한스크롤인 경우에는 현재 페이지 데이터와 다음 페이지 여부만 있으면 되기 때문입니다! Page를 썼다가는 count 연산까지 하기 때문에 성능이 떨어지게 됩니다

profile
멋진 개발자가 될테야

1개의 댓글

comment-user-thumbnail
2025년 2월 5일

좋은 글이네요.

답글 달기