오늘은 게시판에 필수인 Pagination 페이징처리를 Spring Data Jpa에서 어떻게 하는지와 관련 클래스를 알아보고 간단하게 만들어도 볼까합니다.
Pagination
- 데이터베이스에서 데이터를 가져올 때 한번에 전부 가져오는 것이 아니라 정해진 Size만큼 페이징해서 가져오면서 서버의 부화를 줄여주는 기술
- 다른 언어들의 일반적인 페이징로직에 비해 Spring Data JPA를 쓰면 동적 페이징 쿼리가 쉽습니다.
순수 JPA에서의 Pagination
먼저 간략히 순수 JPA에서의 Pagination에 대해 알아보시죠
데이터베이스에 따라 Pagination처리를 하는 방법은 다양합니다. Mysql은 좀 간단한 편이고, Oracle은 좀 복잡합니다.
JPA는 이런 여러 DB 방언(dialect)을 추상화하여 하나의 방법으로 페이지네이션을 구현할 수 있도록 제공합니다.
솔직히 스프링 사용하면서 가장 좋았던 부분중에 하나입니다.
MySQL | JPA |
---|---|
offset | setFirstResult() |
limit | setMaxResults() |
이렇게 JPA가 추상화를 해준 덕분에 DB에 상관없이 Pagination을 손쉽게 구현가능 합니다.
그럼 오늘의 주제인 Spring Data JPA에서의 Pagination에 대해 알아 보겠습니다.
JPA에서도 Pagination에 선 계산되어야 하는 불편함등이 있는데 이런 불편함을 Spring Data JPA는 페이지 크기와 페이지 순서만 전달하면, 데이터베이스에서 해당 페이지에 해당하는 데이터만 가져올 수 있습니다.
PageRequest 생성
아래와 같이 정적 팩토리 메소드를 사용하여 PageRequest
를 생성할 수 있다.
PageRequest page = PageRequest.of(0, 10);
이때 첫번째 파라미터는 Page, Size를 의미합니다. 페이지 순서는 0부터 시작한다고 말씀드렸죠.
Pageable
// Slice.java (interface)
default Pageable getPageable() {
return PageRequest.of(getNumber(), getSize(), getSort());
}
//PageRequest.java
public static PageRequest of(int page, int size, Sort sort) {
return new PageRequest(page, size, sort);
}
- Pageable은 인터페이스이므로 실제로 사용할 때에는 구현체
PageRequest
객체를 사용합니다. - 위에 보시듯 파라미터로 Page, size, sort정보를 사용합니다.
- Page Index는 0부터 시작합니다.
- Repository에 Pageable을 전달하면, 반환 타입으로 Slice, Page를 받습니다.
Pagination을 통한 조회 결과를 저장하는 역활을 하고 Page는 Slice를 상속 받습니다.
Slice
- 전체 페이지 개수를 필요하지 않는 경우에 사용하시면 됩니다.
- 즉 count 쿼리가 발생하지 않으니 성능 낭비가 없습니다.
- 모바일 Infinity Scrolle을 위한 페이징에 사용하기 좋습니다.
Slice가 다음 Slice의 존재 유무를 판단하는 방법
boolean isFirst();
/**
* Returns whether the current {@link Slice} is the last one.
*
* @return
*/
boolean isLast();
/**
* Returns if there is a next {@link Slice}.
*
* @return if there is a next {@link Slice}.
*/
boolean hasNext();
/**
* Returns if there is a previous {@link Slice}.
*
* @return if there is a previous {@link Slice}.
*/
boolean hasPrevious();
//AdminBookmarkRepository.kt
PageRequest pageRequest = PageRequest.of(0, 10);
val bookMarkPage: Slice<BookMark> = AdminBookmarkRepository.findSliceBybookmark(pageRequest);
PageRequest.of(0, 10)인 Pageable을 전달하면, Spring Data JPA는 전달된 페이지 사이즈에 1을 더한 값으로 쿼리를 실행합니다. 즉, 총 11개의 데이터를 쿼리를 날립니다.
분명 페이지 사이즈로 10을 전달했는데, 실제 실행된 쿼리를 보면 limit 11 가 되어 있을 것입니다.
위 쿼리를 통해 불러온 데이터가 실제로 11개라면, 다음 슬라이스가 있다고 판단합니다.
Page
- count쿼리가 날라갑니다.
전체 데이터 개수 / 단일 페이지의 크기
로 계산할 수 있습니다.- PC 게시판 Api를 구현할 때 적합합니다.
PagingAndSortingRepository
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
/**
* Returns all entities sorted by the given options.
*
* @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return all entities sorted by the given options
*/
Iterable<T> findAll(Sort sort);
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}.
* @return a page of entities
*/
Page<T> findAll(Pageable pageable);
}
여기에 보면 findAll 메서드의 반환타입을 보시죠
-
org.springframework.data.domain.Pageable
- 페이징을 제공하는 인터페이스입니다.
-
org.springframework.data.domain.Page
- 페이징의 findAll() 의 기본적인 반환 메서드로 여러 반환 타입 중 하나입니다.
JpaRepository<>
를 사용할 때, findAll()
메서드를 Pageable
인터페이스로 파라미터를 넘기면 페이징을 사용할 수 있게되는 것입니다.
그럼 사용예제로 한번 보겠습니다.
//AdminBookmarkController.kt
@RestController
@RequestMapping("/admin/{adminId}/bookmarks")
class AdminBookmarkController(
private val adminBookmarkService: AdminBookmarkService
) {
@GetMapping
fun getBookmarkAll(
@RequestParam(required = false) query: String?,
@PathVariable adminId: Long,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int
): Page<AdminBookmarkRes> = adminBookmarkService.getBookmarkAll(
query,
PageRequest.of(page, size)
).map(BookmarkTranslator::toAdminBookmarkRes)
}
위에 DI된 AdminBookmarkService.getBookmarkAll
메서드를 보시면 PageRequest
파라미터를 전달받고 있는 것을 보실 수 있습니다.
PageRequest 메서드 정의로 가서 확인 해 보겠습니다.
//PageRequest.java
public class PageRequest extends AbstractPageRequest {
}
//AbstractPageRequest.java
public abstract class AbstractPageRequest implements Pageable, Serializable {
}
PageRequest는 AbstractPageRequest Interface를 상속받는 것을 보실 수 있습니다.
Creates a new AbstractPageRequest. Pages are zero indexed, thus providing 0 for page will return the first page.
Params:
page – must not be less than zero.size – must not be less than one.
파라미터로 page, size 정보를 받습니다. 위에 예에서 보시면 PageRequest.of(page, size) 이렇습니다.
// AdminBookmarkService.kt
@Service
class AdminBookmarkService(
private val adminBookmarkRepository: AdminBookmarkRepository
) {
fun getBookmarkAll(
query: String?,
pageable: Pageable
): Page<AdminBookmark> = if (query != null) {
adminBookmarkRepository.SearchBookmark(
query,
pageable
)
}
}
// AdminBookmarkRepository.kt
interface AdminBookmarkRepository: JpaRepository<AdminBookmark, Long> {
@Query("""
SELECT *
FROM bookmark
WHERE shop_id = :query OR product_id = :query
OR customer_id = :query OR story_id = :query
OR portfolio_id = :query
ORDER BY id DESC
""",
nativeQuery = true)
fun SearchBookmark(
query: String,
pageable: Pageable
): Page<AdminBookmark>
}
PostMan으로 Api호출해서 쿼리문 확인
- PageRequest.of 파라미터를 보내지 않은 경우
limit
부분을 보면page
값이 없는 것을 확인 하실 수 있습니다.
Parameter값을 확인 할 수 있게 환경설정을 해야 하는데 안해서 ?로만 보입니다.
Hibernate:
SELECT
*
FROM
bookmark
ORDER BY
id DESC limit ?
Hibernate:
select
count(*)
FROM
bookmark
- PageRequest.of 파라미터를 보낸 경우
limit
부분을 보면page
값을 확인 하실 수 있습니다.
Hibernate:
SELECT
*
FROM
bookmark
ORDER BY
id DESC limit ?,
?
Hibernate:
select
count(*)
FROM
bookmark
정리
오늘은 프로젝트를 할때 꼭 해야하는 Paging처리 그 중에 JPA가 빛나는 이유에 대해 알아 봤습니다.
참고로 저는 자바개발자가 아니여서 다른 언어에서의 데이터베이스의 페이징처리는 귀찮았었습니다. 근데 스프링에 Paging처리는 그저 감사할 따름이었습니다.
'IT > JPA' 카테고리의 다른 글
JPA Kotlin으로 Entity설계시 고려할 부분을 알아보자 (0) | 2023.04.24 |
---|---|
JPA BatchSize에 대해 알아보자 (0) | 2023.04.18 |
JPA의 낙관적 락과 비관적 락을 통해 엔티티에 대한 동시성 제어에 대해 알아보자 (0) | 2023.04.16 |
JPA EntityManager 라이프사이클에 대해 알아보자 (1) | 2023.04.14 |
JPA EntityGraph에 대해 알아보자 (0) | 2023.04.11 |