페이지 처리의 필요성
- API 응답으로 모든 쿼리 결과를 주는 것은 비효율적이다.
- 쿼티 엔티티의 개수가 증가함에 따라 오버헤드도 커지게 된다.
- 하지만 웹에서는 API응답으로 온 모든 결과를 보여주려고 하지 않는다.
- 예를 들어 구글에서는 검색결과를 한 화면에 다 보여주지 않고, 특정 개수씩 묶어 검색결과의 일부만 보여주도록 한다.

- 이때 검색결과 쿼리는 전체 검색결과가 아닌 한 페이지에 해당하는 검색결과만 응답으로 받게 된다.
- 다음 페이지를 넘어갈 때는 다음 페이지에 대한 검색결과 쿼리 요청을 보낸다.
- 만약 검색결과를 조금씩 보여주지 않고 한 번에 모든 결과를 보여주려고 했다면 오랜 시간이 걸렸을 것이다.
- 이러한 방식을
Pagintaion
이라고 한다.
- 실제로 구글은 검색결과 요청 시 아래와 같이 쿼리 파라미터 요청을 보낸다.



- 순서대로 1번, 2번 ,3번 페이지를 호출했을 때 바뀌는
start
파라미터 값이다.
- 페이지 사이즈는 10으로, n번째 페이지를 호출할 때 (n-1)부터 (n-1)+10번째 까지의 검색결과를 보여준다.
- 한 페이지의 몇 개의 검색결과를 보여줄지도 파라미터로 넣어 쿼리할 수 있다. 하지만 구글 검색로직에서는 페이지 사이즈는 정해져있는 듯 하다.
Spring Boot Pagintaion - Pageable
Pagination
을 적용한다는 것은 Controller
나 Service
레이어가 아닌 데이터를 fetch해오는 DAO(Repository)
에서 이루어지는 작업이다.
- Client가 요청을 전송할 때 가져올 페이지에 대한 정보를 같이 담아 전송해야한다.
- 서버는 해당 페이지 관련 정보를 받아 쿼리를 작성할 때 사용하게 된다.
Pageable
- Spring Boot는 Pagination을 지원할 수 있도록 하는
Pageable
이라는 인터페이스가 있다.
- 페이지 번호나 포함될 데이터 개수 등과 같은, 페이지를 처리할 때 필요한 정보를 담고 있다.
- 해당 객체를 생성하여 페이지 데이터를 쉽게 관리한다.
Pageable
인터페이스는 아래와 같이 페이지 관련 다양한 메서드 명세를 가지고 있다.
public interface Pageable {
int getPageNumber();
int getPageSize();
long getOffset();
Sort getSort();
Pageable next();
Pageable previousOrFirst();
Pageable first();
Pageable withPage(int pageNumber);
boolean hasPrevious();
}
- 아래 다이어그램은 Spring Data JPA를 사용할 때 pagination을 적용하는 방법이다.
- Spring Data JPA는
Pageable
의 구현체인 PageRequest
를 사용한다.
PageRequest
객체를 사용하여 JPA 메서드에서 쉽게 페이징 처리가 가능해진다.
PageRequest
를 이용해 페이징 처리되어 fetch된 데이터들은 Page
객체로 반환된다.
- 쉽게 말해 페이지를 요청할 때는
PageRequest
, 응답은 Page
가 사용된다고 할 수 있다.
예시 (JPA)

- 쿼리로 page, size를 사용하여 요청한다.
이때 page는 조회할 페이지 번호(0부터 시작), size는 한 페이지 당 결과 개수이다.
localhost:8080/api/v1/results/all?page=2&size=20
이런 url이 되겠다.
쿼리 이름인 page, size를 자동으로 인식하여 controller
레이어에서 Pageable
객체를 파라미터로 받을 수도 있다. (이 경우에는 2단계를 생략한다.)
Controller
혹은 Service
레이어에서 받은 page, size를 인터페이스 Pageable
의 구현체인 PageRequest
로 변환한다.
- 변환한
PageRequest
를 JPARepository
메서드의 파라미터로 전달한다.
- 이에 요청받은 페이지 정보가 담긴
PageRequest
에 따라 알맞은 값이 Page
객체로 return된다.
코드
Controller
@Operation(summary = "모든 로그 조회")
@GetMapping("/all")
public ApiResponse<Page<LogThumbnailResponseDto>> readAllLog(@RequestParam int page,
@RequestParam int size) {
return ApiResponse.onSuccess(logService.readAllLogs(page, size));
}
Service
@Transactional(readOnly = true)
public Page<LogThumbnailResponseDto> readAllLogs(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return logRepository.findAll(pageable)
.map(LogThumbnailResponseDto::of);
}
- 다음과 같이 page, size로
Pageable
이라는 객체를 생성하여 repository로 넘겨준다.
Repository
@Repository
public interface LogRepository extends JpaRepository<Log, Long> {
Page<Log> findAllWithMember(Pageable pageable);
}
- JPA메서드에
Pageable
객체만 파라미터로 넘겨주면 fetch된 데이터를 페이징 처리하여 결과를 Page
로 return할 수 있다.
- Spring Data JPA에서는 모든 메서드에
Pageable
를 적용하여 쉽게 페이징 처리가 가능했지만 QueryDsl같은 동적 쿼리 라이브러리를 사용한다면 페이징 처리는 Spring Data JPA와 달라진다.
- QueryDsl은
Pageable
를 지원하지 않기 때문에 Pageble
를 파라미터로 받되, Pageble
내의 정보를 직접 이용하여 DAO 메소드 내에서 쿼리를 작성해야 한다.
public Page<Log> findAll(Pageable) {
QLog log = QLog.log;
List<Log> fetch = jpaQueryFactory.selectFrom(log)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long count = jpaQueryFactory.select(log.count())
.from(log)
.fetchOne();
return new PageImpl<>(fetch, pageable, count);
}
- QueryDsl은
pageable
을 받아 자동으로 쿼리를 생성해주지 않으니 페이지 정보를 가지고 직접 쿼리를 작성해야한다.
- 다음과 같이
PageImpl
객체를 리턴함으로써 pagination
사용이 가능하다. 여기서 PageImpl
은 Page
의 구현체이다.
- 주목해야할 점은
PageImpl
에 들어가는 파라미터이다.
- 실제 데이터 List를 의미하는
fetch
뿐만 아니라 해당 데이터의 개수를 의미하는 count
까지 쿼리를 사용해 PageImpl
에 값을 전달하고 있다.
추가
fetch join
을 활용한 쿼리에서는 위와 같이 pagination처리가 불가능했다. (데이터 중복 때문)
- 하지만
JPA
의 구현체인 hibernate 6
버전에서부터는 fetch join
과 pagination
을 동시에 사용 가능하게 되었다.
- 하지만
fetch join
은 메모리에 fetch 한 모든 데이터를 로드하기 때문에 부하가 비교적 크다는 단점이 있다.
- 가급적
fetch join
을 사용하지 않는 방향으로 데이터를 가져온 후 pagination
을 진행하는 것이 이상적이다.