Spring Data JPA는 데이터베이스의 페이징 처리를 추상화하여 개발자가 쉽게 페이징을 구현할 수 있도록 도와준다.
이는 기존에 데이터베이스마다 다르게 적용되던 페이징 처리 방식을 단순화시킨다.
기존 페이징 처리의 어려움
과거에는 데이터베이스별로 페이징 처리 방식이 달랐다.
예를 들어, MySQL은 LIMIT과 OFFSET을 사용했고, Oracle은 ROWNUM을 사용했다.
이러한 방식은 개발자가 직접 계산하고 구현해야 하는 어려움이 있었다.
Spring Data Jpa는 이를 공통으로 추상화시켰다.
Spring Data Jpa의 페이징 처리
Spring Data JPA에서는 Pageable 인터페이스를 사용하여 페이징 처리를 추상화하고 간소화한다.
// MemberRepository
Page<Member> findByAge(int age, Pageable pageable);
// 실제 비즈니스 로직
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
위 예시에서 PageRequest.of() 메소드를 사용하여 페이지 번호, 페이지 크기, 정렬 조건을 설정한다.
Spring Data JPA에서는 페이징 인덱스가 0부터 시작한다.
PageRequest interface
PageRequest는 페이징 처리에 필요한 모든 정보를 담고 있다.
첫 번째 인자는 페이지의 인덱스, 두 번째 인자는 한 페이지당 보여줄 아이템의 수, 세 번째 인자는 정렬 조건을 설정할 수 있다.
위 페이징 코드는 0번째 페이지에서 3개 가져오라는 뜻이고, "username"으로 내림차순한다는 뜻이다.
그렇게 선언한 pageRequest를 인자로 넘기면 끝이다. PageRequest 부모에 Pageable이 있기 때문에 가능한 것이다.
Page interface
// 페이징 후 실제 데이터 꺼내기
List<Member> content = page.getContent();
content.forEach(System.out::println);
/** 출력 결과
* Member(id=9, username=user9, age=10)
* Member(id=8, username=user8, age=10)
* Member(id=7, username=user7, age=10)
*/
int totalPages = page.getTotalPages();
System.out.println("totalPages = " + totalPages); // 3 - 총 페이지 개수
// totalPages = 3
long totalElements = page.getTotalElements();
System.out.println("totalElements = " + totalElements); // 9 - 총 결과 개수
// totalElements = 9
int number = page.getNumber();
System.out.println("number = " + number); // 현재 페이지 수
// number = 0
boolean isFirst = page.isFirst();
System.out.println("isFirst = " + isFirst); // 해당 페이지가 첫번째 페이지인지
// isFirst = true
boolean hasNext = page.hasNext();
System.out.println("hasNext = " + hasNext); // 다음 페이지가 존재하는지
// hasNext = true
위와 같이 사용할 수 있다. 자동으로 페이징처리 계산을 해주는 것이 정말 대단한 것 같다.
page.getTotalPages();
총 페이지를 구하는 함수
page.getTotalElements();
총 결과(Member) 개수
page.getNumber();
현재 페이지 개수(Spring Data Jpa에서 0부터 시작)
page.isFirst();
첫번째 페이지 여부
page.hasNext();
다음 페이지 존재 여부
Slice interface
Repository에는 Page로 두고, 실제 비즈니스 로직에서 다음과 같이 사용한다.
Slice는 Page와 비슷하지만, 전체 페이지 수나 전체 요소 수를 계산하지 않는 차이가 있다.
이는 특히 "더보기" 기능을 구현할 때 유용하다.
// MemberRepository
Slice<Member> findByAge(int age, Pageable pageable);
// 실제 비즈니스 로직
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Direction.DESC, "username"));
Slice<Member> slice = memberRepository.findByAge(age, pageRequest);
Slice는 3개를 인자로 넣으면, 4개를 요청을 해본다. 내부적으로 limit + 1을 하여 조회한다.
요즘 모바일에서 1개더 있는지 확인할 때 이를 사용한다.
아까 말한 더보기를 totalCount가 있는지 조회하고 넘겨주는 것보다, 이를 사용하면 너무나 쉽게 해결할 수 있다.
Pageable + List
Pageable을 사용한 리스트 조회
Pageable 인터페이스를 사용하여 리스트를 직접 반환받는 것도 가능하다. 이 방법은 페이징 쿼리를 실행하면서 결과를 단순히 리스트로 받고 싶을 때 유용하다.
// MemberRepository
List<Member> findByAge(int age, Pageable pageable);
Pageable을 인자로 넘기는데, List로 return 받을 수도 있다.
페이징 쿼리는 상관 없고, 데이터만 10개씩 끊어 갖고올 때 사용한다. 위에 Page interface에서 getContent만 한 것과 같은 결과다.
CountQuery
실무적인 관점으로, totalPage는 그대로 사용하면 안된다.
Spring Data JPA에서 페이징 처리를 할 때, 전체 페이지 수를 계산하는 과정은 성능에 영향을 줄 수 있다.
totalPage 계산을 전체를 카운트하고 원하는 만큼 나누고 갖고오는데까지, 엄청난 자원이 소모가 되고, 성능이 느리다.
엄청나게 많은 조인이 일어난다고 가정할 때, 페이징은 그것을 계산할 필요가 없다. 그래서 CountQuery를 별도로 분리하는 것이 좋다.
Member를 team과 함께 join하여 전체 결과를 조회한다고 해보자.
어차피 Member의 카운트 개수가 team과 join한 카운트의 결과와 같다. 그렇기 때문에 countQuery를 따로 제공한다.
// 기존 Query
Page<Member> findByAge(int age, Pageable pageable);
// CountQuery 추가
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m) from Member m")
Page<Member> findByAgeWithCountQuery(int age, Pageable pageable);
위 예시에서 countQuery는 전체 개수를 계산하는 데 사용되며, 메인 쿼리는 실제 데이터를 조회하는 데 사용된다.
이렇게 분리함으로써, 복잡한 조인이 포함된 메인 쿼리의 성능 저하를 방지할 수 있다.
page.map()
그렇다면 이렇게 반환하면 끝인 걸까?
모든 Api는 Entity를 직접 반환하면 절대 안된다.
Dto로 변환해서 반환해야한다.
정말 마술같은 기능이 또하나 있는데, dto로 즉시 변환할 수 있게 해준다.
Page<MemberDto> memberDtoPage = page.map(member -> new MemberDto(member.getId(), member.getUsername(), "teamName"));
/**
* MemberDto
* Long id;
* String username;
* String teamName;
**/
이 코드에서 map 메소드는 Page에 담긴 각 Member 엔티티를 MemberDto로 변환한다.
이는 API에서 엔티티를 직접 반환하지 않고, DTO를 통해 데이터를 반환하는 데 도움을 준다.
Service혹은 Converter에서 값을 직접 꺼내어 반환하는 것이 아닌, map을 사용하면 정말 간편하게 로직을 작성할 수 있다.
'JPA > Spring Data JPA' 카테고리의 다른 글
[Spring Data JPA] 6. 사용자 정의 Repository. DI 활용 (0) | 2023.11.24 |
---|---|
[Spring Data JPA] 5. JPA Hint & Lock (0) | 2023.11.23 |
[Spring Data JPA] 4. @EntityGraph (0) | 2023.11.23 |
[Spring Data JPA] 3. 벌크성 수정 쿼리 (0) | 2023.11.23 |
[Spring Data JPA] 1. QueryMethod (0) | 2023.11.21 |