티스토리 뷰
페이지 처리는 데이터베이스의 종류에 따라서 사용되는 기법이 다른 경우가 많아서 별도의 학습이 필요했다.
예를 들어,
- Oracle → inline view
- MySQL → limit
JPA는 내부적으로 이런 처리를 Dialect
라는 존재를 이용해 처리한다. 때문에 개발자들은 SQL이 아닌 API의 객체와 메서드를 사용하는 형태로 페이징 처리를 할 수 있다.
Goals
PagingAndSortRepository
/Pageable
/PageRequest
- 페이징 처리
- 정렬 조건 추가
- 쿼리 메서드와
Pageable
결합
PagingAndSortRepository
/ Pageable
/ PageRequest
PagingAndSortRepository
인터페이스
Spring Data JPA의 PagingAndSortRepository
인터페이스는 CrudRepository
인터페이스를 상속 받아 CRUD 작업을 지원하며, 추가적으로 페이징과 정렬 기능을 제공한다. 선언은 아래와 같다.
public interface PagingAndSortingRepository<T, ID extends Serializable>
extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort); // 정렬
Page<T> findAll(Pageable pageable); // 페이징 처리
}
페이징 처리와 정렬을 findAll()
메서드로 처리하며, 페이징 처리를 위해서는 Pageable
타입의 객체를 파라미터로 전달해야 한다. 이 때의 리턴 타입은 Page<T>
타입의 객체라는 것도 주의하자.
Pageable
인터페이스
Pageable
인터페이스는 org.springframework.data.domain.Pageable
패키지에 있다. 이 인터페이스는 페이지 처리에 필요한 정보를 전달하는 용도의 타입으로, 인터페이스이기 때문에 실제 객체를 생성할 때는 구현체인 PageRequest
클래스를 사용한다.
선언은 다음과 같다.
public interface Pageable {
static Pageable unpaged()
static Pageable ofSize(int pageSize)
default boolean isPaged()
default boolean isUnpaged()
int getPageNumber();
int getPageSize();
long getOffset();
Sort getSort();
default Sort getSortOr(Sort sort)
Pageable next();
Pageable previousOrFirst();
Pageable first();
Pageable withPage(int pageNumber);
boolean hasPrevious();
default Optional<Pageable> toOptional()
}
각 메서드의 기능을 간단하게 살펴보겠다.
unpaged()
: pagination 설정이 없는Pageable
인터페이스를 반환ofSize(int pageSize)
:pageSize
가 지정된 첫 번째 페이지(페이지 번호 0)에 대한 새로운Pageable
을 반환isPaged()
: 현재Pageable
에 pagination 정보가 포함되어 있는지 여부를 반환isUnpaged()
: 현재Pageable
에 pagination 정보가 포함되어 있지 않은지 여부를 반환getPageNumber()
: 페이지 반환getPageSize()
: 반환할 페이지 내의 항목 수(데이터 개수)를 반환getOffset()
: 기본 페이지 및 페이지 크기에 따라 취할offset
을 반환getSort()
: 정렬 매개변수를 반환next()
: 다음 페이지를 요청하는Pageable
을 반환previousOrFirst()
: 이전Pageable
을 반환하거나, 이미 첫 번째Pageable
인 경우에는 현재Pageable
을 반환first()
: 첫 번째 페이지를 요청하는Pageable
을 반환withPage(int pageNumber)
:pageNumber
가 적용된 새Pageable
을 반환hasPrevious()
: 현재Pageable
에서 접근할 수 있는 이전Pageable
이 있는지 여부를 반환toOptional()
:Optional
로 감싸서 반환
PageRequest
클래스
PageRequest
클래스는 Pageable
인터페이스의 구현체 중 하나로 org.springframework.data.domain.PageRequest
패키지에 속해 있다. PageRequest
클래스는 생성자로 페이지 번호, 페이지 크기 및 정렬 정보를 생성할 수 있다. 하지만 특이하게도 접근 제어자가 protected
로 선언되어 있기 때문에 new
를 이용할 수 없어 of()
메서드를 이용해야 한다.
public class PageRequest extends AbstractPageRequest {
private static final long serialVersionUID = -4541509938956089562L;
private final Sort sort;
protected PageRequest(int page, int size, Sort sort) { }
public static PageRequest of(int page, int size) {}
public static PageRequest of(int page, int size, Sort sort) {}
public static PageRequest of(int page, int size, Direction direction, String... properties) {}
// ...
of(int page, int size)
: 페이지 번호와 페이지 크기를 인자로 받아PageRequest
객체를 생성. 이 때, 정렬은 지정되지 않는다.of(int page, int size, Sort sort)
: 페이지 번호, 페이지 크기, 정렬 방향 및 정렬 속성을 인자로 받아PageRequest
객체를 생성of(int page, int size, Direction direction, String... properties)
: 페이지 번호, 페이지 크기 및 정렬 관련 정보를 담고 있는Sort
객체를 인자로 받아PageRequest
객체를 생성
페이징 처리
실제 페이징 처리를 해보기 위해 다음과 같은 테스트 코드를 작성한다.
- DB : MySQL
- 테이블 명 : member
- 속성 :
- id : 회원 pk
- name : 회원 이름
(이전 [Spring Boot] 입문의 프로젝트를 재 사용하기 때문에, 자세한 설정 및 구성을 알고 싶다면, [Spring Boot] 입문 관련 포스팅을 참고)
// ...
@SpringBootTest
@Transactional
public class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
public void testPageDefault() {
Pageable pageable = PageRequest.of(0, 10);
Page<Member> result = memberRepository.findAll(pageable);
System.out.println(result);
}
}
Pageable pageable = PageRequest.of(0, 10);
: 1 페이지에 데이터를 10개씩 가져온다.Page<Member> result = memberRepository.findAll(pageable);
:member
테이블의 데이터를 모두 조회. 이때pageable
의 페이징 설정 적용
위 로그를 보면, memberRepository.findAll()
코드 상에서는 메서드를 실행했지만, 실제로 실행된 메서드는 spring이 내부적으로 구현한 PagingAndSortingRepository.findAll()
메서드가 실행됐다.
실행 시 생성된 SQL도 확인할 수 있는데, 모든 데이터를 조회하기 때문에 where
조건 없이 member
테이블의 id
와 name 컬럼을 모두 조회한다. 이때 limit
을 사용하는데, 각각의 ?에는 페이징 설정에 따라 0, 10
같은 식으로 대입 될 것이다.
마지막 로그를 보면 Member
도메인(엔티티) 객체에 총 1개의 페이지 중 1번째 페이지가 담겼다는 로그를 볼 수 있다.
이는 사전에 추가해 놓은 데이터가 1개 밖에 없기 때문이다.
데이터를 더 추가한 뒤 다시 테스트 해보자.
@SpringBootTest
@Transactional
public class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
public void testPageDefault() {
Pageable pageable = PageRequest.of(0, 10);
// 100개의 엔티티 객체 추가
for(int i = 0; i < 100; i++){
Member member = new Member();
member.setName("test" + i);
memberRepository.save(member);
}
Page<Member> result = memberRepository.findAll(pageable);
System.out.println(result);
}
}
반복문을 통해 총 100개의 데이터를 추가했다.
마지막 로그를 보니 총 11 페이지로 늘어난 것을 확인할 수 있다.
다만, SQL문이 1개 더 늘어나 버렸다. 두 번째 SQL 문은 count()
를 이용해서 실제 페이지 처리에 필요한 전체 데이터의 개수를 가져오는 쿼리다.
이전에는 데이터가 충분하지 않았기 때문에 실행되지 않은 것이다.
결과 값인 Page<Member>
객체는 쿼리 결과를 사용하기 위한 여러 메서드를 제공한다.
이 중에서, 몇 가지 메서드만 테스트 해보겠다.
// ...
public class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
public void testPageDefault() {
Pageable pageable = PageRequest.of(0, 10);
for(int i = 0; i < 100; i++){
Member member = new Member();
member.setName("test" + i);
memberRepository.save(member);
}
Page<Member> result = memberRepository.findAll(pageable);
System.out.println(result);
// 총 페이지 수
System.out.println("Total Pages: " + result.getTotalPages());
// 전체 데이터(엔티티) 개수
System.out.println("Total Counts: " + result.getTotalElements());
// 현재 페이지 번호(0부터 시작)
System.out.println("Page Nnumber: " + result.getNumber());
// 페이지 당 데이터 개수
System.out.println("Page Size: " + result.getSize());
// 다음 페이지 존재 여부
System.out.println("Has next page?: " + result.hasNext());
// 시작 페이지(0) 여부
System.out.println("Is First page?: " + result.isFirst());
// 데이터 처리를 위한 엔티티 반환
for (Member member : result.getContent()) {
System.out.println("member's id: " + member.getId());
System.out.println("member's name: " + member.getName());
}
}
}
getContent()
메서드는 List<엔티티 타입>
을 반환하는 메서드다.
이를 이용해 위처럼 1 페이지(번호는 0번) 엔티티 10개를 접근할 수 있다.
정렬 조건 추가
PageRequest
에는 정렬과 관련된 org.springframework.data.domain.Sort
타입(이하 Sort
)을 파라미터로 전달할 수 있다. Sort는 한 개 혹은 여러 개의 필드 값을 이용해서 정렬을 지정할 수 있다. SQL의 정렬 방식과 똑같다고 생각하면 된다.
@Test
public void testSort() {
for(int i = 0; i < 100; i++){
Member member = new Member();
member.setName("test" + i);
memberRepository.save(member);
}
// id를 기준으로 역순 정렬
Sort sort1 = Sort.by("id").descending();
Pageable pageable = PageRequest.of(0, 10, sort1);
Page<Member> result = memberRepository.findAll(pageable);
for (Member member : result.getContent()) {
System.out.println("member's id: " + member.getId());
}
}
SQL문을 확인해 보면 order by
가 사용된 것을 알 수 있다.
결과 값 엔티티도 역순으로 출력되었다.
만약 2개 이상의 속성 값을 기준으로 하여 정렬하고 싶다면 and
를 이용하면 된다.
Sort sort1 = Sort.by("id").descending();
Sort sort2 = Sort.by("name").ascending();
Sort sort1AndSort2 = sort1.and(sort2);
(참고로 테스트 마다 계속 데이터를 새로 추가하는 이유는 @Transactional
때문에 테스트가 끝나면 Rollback
해버리기 때문. 그래서 id
값도 테스트를 할 때마다 더 크게 나오는 것)
쿼리 메서드와 Pageable
결합
쿼리 메서드는 메서드의 이름 자체가 쿼리의 구문으로 처리되는 기능이다. 만약 쿼리 메서드에 정렬 조건을 추가하기 위해 OrderBy
키워드를 사용할 수도 있겠지만, 이름도 길어지고 혼동하기가 쉬워진다. 그 대신 Pageable
파라미터를 같이 결합해서 사용할 수 있다.
MemberRepository.java
// ...
List<Member> findByIdBetweenOrderByIdDesc(long from, long to);
Page<Member> findByIdBetween(Long from, Long to, Pageable pageable);
// ...
@Test
public void testQueryMethodWithPageable() {
for(int i = 0; i < 100; i++){
Member member = new Member();
member.setName("test" + i);
memberRepository.save(member);
}
List<Member> result1 = memberRepository.findByIdBetweenOrderByIdDesc(760L, 780L);
result1.forEach(member -> System.out.println("member's id: " + member.getId()));
System.out.println("-------------------------------------------------");
Sort sort1 = Sort.by("id").descending();
Pageable pageable = PageRequest.of(0, 10, sort1);
Page<Member> result2 = memberRepository.findByIdBetween(760L, 780L, pageable);
result2.get().forEach(member -> System.out.println("member's id: " + member.getId()));
}
두 메서드 다 id
값이 760 ~ 780인 데이터를 조회했다.
역순으로 정렬했기 때문에 첫 번째 메서드의 경우 780부터 760까지 모두 출력했고, 두 번째 메서드는 페이징 처리가 되어 771까지 출력한 것을 볼 수 있다.
즉, 두 메서드의 결과 값은 같다는 것이다. 때문에 Pageable
을 활용해서 쿼리 메서드를 작성하는 편이 유지보수에 편하다.
Reference
- 코드로 배우는 스프링 부트 웹 프로젝트 - 구멍가게 코딩단
- Spring Data API 공식문서
'Spring&Spring Boot' 카테고리의 다른 글
[Spring] DTO를 사용하는 이유 (0) | 2023.03.23 |
---|---|
[Spring] Gradle - runtimeOnly, compileOnly, implementation의 차이 (0) | 2023.03.20 |
[Spring Boot] 입문 - Spring Data JPA 맛보기 (0) | 2023.02.25 |
[Spring Boot] 입문 - Spring Boot 프로젝트에 JPA 적용하기 (0) | 2023.02.24 |
[Spring Boot] 입문 - JPA를 사용하는 이유 (0) | 2023.02.23 |
- Total
- Today
- Yesterday
- 쉘 코드
- Computer_Networking_A_Top-Down_Approach
- Spring
- 운영체제 반효경
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- 프로그래머스
- git
- 지옥에서 온 git
- 스프링 mvc
- jsp
- 스프링 테스트
- Gradle
- 파이썬 for Beginner 솔루션
- 쉽게 배우는 운영체제
- Thymeleaf
- JPA
- 선형 회귀
- Python Cookbook
- 생활코딩 javascript
- 패킷 스위칭
- 스프링 컨테이너
- 스프링
- 파이썬 for Beginner 연습문제
- git merge
- Spring Data JPA
- git branch
- 김영환
- Spring Boot
- spring mvc
- 방명록 프로젝트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |