Spring Boot Pageable (페이지 처리)
API 응답으로 모든 쿼리 결과를 주는 것은 비효율적이다. 쿼티 엔티티의 개수가 증가함에 따라 오버헤드도 커지게 된다.
Spring Boot Pageable (페이지 처리)
페이지 처리의 필요성
- 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인터페이스는 아래와 같이 페이지 관련 다양한 메서드 명세를 가지고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
- 쿼리로 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
1
2
3
4
5
6
@Operation(summary = "모든 로그 조회")
@GetMapping("/all")
public ApiResponse<Page<LogThumbnailResponseDto>> readAllLog(@RequestParam int page,
@RequestParam int size) {
return ApiResponse.onSuccess(logService.readAllLogs(page, size));
}
Service
1
2
3
4
5
6
@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
1
2
3
4
@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 메소드 내에서 쿼리를 작성해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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을 진행하는 것이 이상적이다.
원문: Velog
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.





