본문 바로가기
Back-End/Database

[Spring Data JPA] 오프셋 기반 페이지네이션(Pagenation) | PagingAndSortingRepository

by 달의 조각 2022. 8. 30.

 

페이지네이션

데이터베이스에 회원 정보가 100건이 존재한다고 가정한다. 클라이언트가 이 많은 데이터를 모두 한 번에 요청하는 것이 아니라 특정 개수 만큼 나누어서 요청할 수 있다. 서버에 데이터를 요청할 때 클라이언트의 입장과 서버의 입장에서 특정 정렬 기준에 따라 지정한 개수의 데이터를 요청하고 응답하는 것이 필요하다.

 

오프셋 방식

  • 가져와야 되는 데이터까지 오프셋 개수 만큼 카운팅해서 찾아가는 방식이다.
    • 데이터베이스의 offset 쿼리를 사용하여 페이지 단위로 데이터를 응답한다.
  • 직접 찾아가야 되므로 시간이 걸린다.
    • 이미 읽었던 데이터를 다시 읽는 과정이 포함되어서 데이터가 많아지면 성능상 좋지 않다.
  • 데이터가 100만 건 ~ 200만 건일 때 클라이언트가 심하게 느리다고 할 정도는 아니다.
  • 데이터 중복 문제: 앞쪽에 최신 데이터가 들어오면 데이터가 뒤로 밀리므로 중복이 발생한다.
  • JPA에서는 Pageable로 쉽게 구현할 수 있다.
  • 인덱스 지정을 해 두면 성능이 빨라진다.

 

커서 방식

[Spring Data JPA] 커서 기반 페이지네이션(Pagination)

 

 

PagingAndSortingRepository

 

먼저 Member 클래스가 도메인 엔티티로 존재한다.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    @Id
    private Long memberId;

    private String email;

    private String name;

    private String phone;
}

 

Member에 엑세스 하기 위한 MemberRepository이다.

PagingAndSortingRepository를 상속하면 페이징 및 정렬을 할 수 있는 findAll(Pageable pageable)와 findAll(Sort sort)메서드를 사용할 수 있다.

public interface MemberRepository extends PagingAndSortingRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
}

 

 

PageRequest

 

클라이언트로부터 컨트롤러의 핸들러 메서드가 page와 size 값을 입력받고,
서비스로 전달되도록 한다.

//MemberController.java
@GetMapping
public ResponseEntity getMembers(@Positive @RequestParam int page,
                                 @Positive @RequestParam int size) {

    List<Member> members = memberService.findMembers(page, size);
    List<MemberResponseDto> response = mapper.membersToMemberResponseDtos(members);

    return new ResponseEntity<>(response, HttpStatus.OK);
}

 

 

 

PagingAndSortingRepository의 메서드 findAll(Pageable pageable) 파라미터에 넣을 Pageable를 정의해야 한다. Pageable은 인터페이스로, 구현체로 PageRequest를 갖는다.

PageRequest.of(page, size) 메서드로 생성할 수 있다.
오버로딩을 통해 sort도 함께 사용할 수 있다. 파라미터로 엔티티 이름을 넣는다.

보통 데이터를 조회할 때 최신 데이터부터 필요한 경우가 많으므로 descending()을 적용했다.

  • page: 조회하고자 하는 페이지 번호 (페이지는 0부터 시작하므로 - 1을 해야 한다.
  • size: 페이지 당 로우 개수

 

//MemberService.java
public List<Member> findMembers(int page, int size) {

    PageRequest pr = PageRequest.of(page - 1, size, Sort.by("memberId").descending());
    return memberRepository.findAll(pr).getContent();
}

PageRequest 인스턴스를 PagingAndSortingRepository의 findAll() 메서드의 파라미터로 넣는다.

findAll()은 Page 객체를 리턴하는데, Page는 Slice 인터페이스를 사용하므로 getContent() 메서드를 사용할 수 있다. getContent()를 사용하면 List로 리턴 할 수 있다.

 

 

페이지 정보를 함께 출력하려면 아래 DTO를 만들어서 page 메서드를 사용하면 된다.

public class PageInfoResponseDtos {

    private List data;
    private PageInfo pageInfo;

    public PageInfoResponseDtos(List data, Page page) {
        this.data = data;
        this.pageInfo =
                new PageInfo(page.getNumber() + 1, page.getSize(),
                        page.getTotalElements(), page.getTotalPages());
    }
}
더보기

 

📄 PageInfo

@AllArgsConstructor
@Getter
public class PageInfo {
    private int page; //페이지 번호
    private int size; //한 페이지에 포함되는 데이터 row의 개수
    private long totalElements; //테이블에 저장되어 있는 데이터의 총 개수
    private int totalPages; //총 페이지 수
}

 

📄 MemberController

@GetMapping
public ResponseEntity getMembers(@Positive @RequestParam int page,
                                 @Positive @RequestParam int size) {

    //페이지네이션 적용
    Page<Member> pages = memberService.findMembers(page, size);
    List<MemberResponseDto> response = mapper.membersToMemberResponseDtos(pages.getContent());

    return new ResponseEntity<>(new MemberPageInfoResponseDto(response, pages), HttpStatus.OK);
}

 

📄 MemberService

public Member findMember(long memberId) {
    return findVerifiedMember(memberId);
}

public Page<Member> findMembers(int page, int size) {

    //페이지네이션 적용
    PageRequest pr = PageRequest.of(page - 1, size, Sort.by("memberId").descending());
    return memberRepository.findAll(pr);
}

 

PageImpl

/**
 * PageImpl(List<T> content, Pageable pageable, long total)
 *   -> total: 리턴 하는 데이터 사이즈가 아니라 전체 데이터 사이즈를 넣는다
 *             페이징이란 데이터 일부를 리턴하므로 전체 페이지가 몇 개인지 Client에게 알리는 용도이다
 * PageRequest of(int page, int size, Sort sort)
 *   -> page: 페이지 출력 시작 페이지
 */
return new PageImpl<>(List.of(member1, member2),
        PageRequest.of(0, 10, Sort.by("memberId").descending()), // 정렬 지원 안됨. DB에서 정렬하지 않으므로..
        2);

 

📚 Reference

댓글