Spring Boot Pageable (페이지 처리)

임원재·2025년 1월 28일
0

SpringBoot

목록 보기
12/18
post-thumbnail

페이지 처리의 필요성

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

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

  • 순서대로 1번, 2번 ,3번 페이지를 호출했을 때 바뀌는 start파라미터 값이다.
  • 페이지 사이즈는 10으로, n번째 페이지를 호출할 때 (n-1)부터 (n-1)+10번째 까지의 검색결과를 보여준다.
  • 한 페이지의 몇 개의 검색결과를 보여줄지도 파라미터로 넣어 쿼리할 수 있다. 하지만 구글 검색로직에서는 페이지 사이즈는 정해져있는 듯 하다.

Spring Boot Pagintaion - Pageable

  • Pagination을 적용한다는 것은 ControllerService레이어가 아닌 데이터를 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();  
}

JPA에서의 pagination - PageRequest & Page

  • 아래 다이어그램은 Spring Data JPA를 사용할 때 pagination을 적용하는 방법이다.
  • Spring Data JPA는 Pageable의 구현체인 PageRequest를 사용한다.
  • PageRequest객체를 사용하여 JPA 메서드에서 쉽게 페이징 처리가 가능해진다.
  • PageRequest를 이용해 페이징 처리되어 fetch된 데이터들은 Page객체로 반환된다.
  • 쉽게 말해 페이지를 요청할 때는 PageRequest, 응답은 Page가 사용된다고 할 수 있다.

예시 (JPA)

  1. 쿼리로 page, size를 사용하여 요청한다.
    이때 page는 조회할 페이지 번호(0부터 시작), size는 한 페이지 당 결과 개수이다.
    localhost:8080/api/v1/results/all?page=2&size=20 이런 url이 되겠다.
    쿼리 이름인 page, size를 자동으로 인식하여 controller레이어에서 Pageable객체를 파라미터로 받을 수도 있다. (이 경우에는 2단계를 생략한다.)
  2. Controller혹은 Service레이어에서 받은 page, size를 인터페이스 Pageable의 구현체인 PageRequest로 변환한다.
  3. 변환한 PageRequestJPARepository 메서드의 파라미터로 전달한다.
  4. 이에 요청받은 페이지 정보가 담긴 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할 수 있다.

QueryDsl에서의 pagination

  • 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사용이 가능하다. 여기서 PageImplPage의 구현체이다.
  • 주목해야할 점은 PageImpl에 들어가는 파라미터이다.
  • 실제 데이터 List를 의미하는 fetch뿐만 아니라 해당 데이터의 개수를 의미하는 count까지 쿼리를 사용해 PageImpl에 값을 전달하고 있다.

추가

  • fetch join을 활용한 쿼리에서는 위와 같이 pagination처리가 불가능했다. (데이터 중복 때문)
  • 하지만 JPA의 구현체인 hibernate 6버전에서부터는 fetch joinpagination을 동시에 사용 가능하게 되었다.
  • 하지만 fetch join은 메모리에 fetch 한 모든 데이터를 로드하기 때문에 부하가 비교적 크다는 단점이 있다.
  • 가급적 fetch join을 사용하지 않는 방향으로 데이터를 가져온 후 pagination을 진행하는 것이 이상적이다.

0개의 댓글