티스토리 뷰

728x90
반응형

페이지 처리는 데이터베이스의 종류에 따라서 사용되는 기법이 다른 경우가 많아서 별도의 학습이 필요했다.

예를 들어,

  • 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()));
}

memberRepository.findByIdBetweenOrderByIdDesc() &nbsp;메서드 실행 결과
memberRepository.findByIdBetween() &nbsp;메서드 실행 결과

두 메서드 다 id 값이 760 ~ 780인 데이터를 조회했다.

역순으로 정렬했기 때문에 첫 번째 메서드의 경우 780부터 760까지 모두 출력했고, 두 번째 메서드는 페이징 처리가 되어 771까지 출력한 것을 볼 수 있다.

즉, 두 메서드의 결과 값은 같다는 것이다. 때문에 Pageable을 활용해서 쿼리 메서드를 작성하는 편이 유지보수에 편하다.

 

Reference

  • 코드로 배우는 스프링 부트 웹 프로젝트 - 구멍가게 코딩단
  • Spring Data API 공식문서
728x90
반응형
댓글