Backend/spring boot

spring boot JPA : 페이징(paging)

attlet 2023. 5. 25. 01:13

보통 게시판 화면을 보면 게시글이 특정 개수 단위로만 띄워진다. 모든 게시글을 한눈에 보이도록 제공하지는 않는다. 그렇게 하면 서버 부하도 심할 것이고, 보는 사용자 입장에서도 가시성이 떨어질 것이다. 그래서 특정 단위로 쪼개서 데이터를 페이지 구분을 하는 방법을 사용하는데, 이것을 페이징이라고 이야기한다.

 

 

DBMS를 이용해 쿼리를 할 때 개수를 나눠서 쿼리하는 페이지네이션 방법은 다 다르다. 예로 mysql은 offset, limit를 사용하면 가능하다. 문제는 이건 DBMS마다 다르기에, 페이지네이션을 하는 다양한 방법들을 하나의 방법으로 통합해서 추상화하여 제공하는 것을 필요로 한다. 이것을 JPA에서는 dialect(방언) 설정을 통해 처리한다. 이 덕에 개발자들은 SQL을 사용하지 않고 API를 통해 쿼리를 할 수 있게 된다. 

 

 

 

Pageable, PageRequest


 

일단 알아야할 것은 Pageable은 인터페이스라는 점이다. 

 

Pageable 인터페이스

 

인터페이스라면 implements 한 구현체가 존재할 것 같다. 구현체는 PageRequest가 존재한다. 

 

Pageable 구현체 PageRequest

 

 

 

구조를 대략적으로 보면 이렇다!

 

PageRequest는 정적 팩터리 메서드 of를 통해 PageRequest 인스턴스를 생성할 수 있다. 생성할 때 매개변수에 따라 기능이 달라진다.

 

정적 팩터리 메서드 of의 오버로딩 형태

 

3가지의 of를 사용할 수 있다. 공통적으로 시작 페이지 번호, 페이지 당 데이터 크기는 무조건 들어가고, 추가로 정렬 조건이나 속성을 추가한다. 중요한 점은 페이지 번호는 1이 아니라 0부터 시작한다는 점이다. 우리가 커뮤니티를 이용할 때는 주로 1페이지부터 시작한다는 것을 생각하면 이를 계산해서 반영해야 한다.

 

 

 

PageRequest의 내부를 보면 Sort 객체가 존재한다. 객체 생성 시 받은 Sort 객체로 초기화되고, 이를 이용할 수 있는 것이다. 

 

 

Page 리턴타입에서 중요한 건 내부적으로 count쿼리를 실행한다는 점이다. 페이징에서 같이 거론되는 Slice, List와의 차별점이 바로 이것이다. count는 왜 필요할까?

 

 

 

count는 쿼리문에서 데이터의 개수를 세는데 사용된다. 데이터 개수를 아는 것이 페이징에서 중요한 이유는 사용자가 몇 페이지를 서칭할 지 모르기 때문이다. 총 데이터 개수를 알아야 페이징이 가능하며, 이 때문에 count가 필요하다.

 

 

문제는 이 count가 full scan을 하는 방식이기에 시간이 걸리는 요인 중 하나이고, 이 때문에 이를 최적화하는 전략들이 존재한다. 이는 나중에 작성하려한다.

 

 

다음은 PageRequest객체를 생성하는 간단한 예시이다.

 

PageRequest pageReq = PageRequest.of(0, 2);

 

 

repository에서 페이징을 사용할 때 매개변수로 Pageable을 전달하고, 반환받는 타입을 Page로 설정하면 준비는 끝난다.

 

 

 

Page<ResponsePostDto> getUserPostList(Long id, Pageable pageable); //page 리턴

Slice<ResponsePostDto> getUserPostList(Long id, Pageable pageable); //Slice 리턴

 

 

Pageable매개변수를 실제로 사용할 때는 구현체인 PageRequest를 생성해서 넣어주면 된다. 이것이 성립하는 것은 다형성을 응용한 것이다. 리턴 타입에 따라 차이점이 존재하는데, 이는 다음과 같다.

 

Page : 조회하는 쿼리 후, 전체 데이터 개수를 세는 count 쿼리를 실행. 비용이 많이 들어감.

 

Slice : 조회하는 쿼리 후, count 실행 안 함. 그 다음 Slice가 존재하는 지, 사용 가능한 지 여부만 체크. 데이터 양이 많을 수 록 slice가 유리할 수 있다.

 

 

@PageableDefault


이 어노테이션을 이용하면 pageable 객체의 디폴트 값을 지정할 수 있다.  정확히는 page, size, sort의 디폴트 값을 설정할 수 있다.

 

 - page :  페이지 번호를 생각하면 된다. 설정하지 않으면 기본값은 0으로 지정된다.

 

 - size : 한 페이지 당 데이터 갯수를 의미한다. 기본값은 20이다. 

 

 - sort : 가져온 데이터를 어떻게 정렬할 지 설정한다. 

 

 

 

 

실제 사용 예시


프로젝트를 통해 실제로 사용한 예시이다.

 

 

게시글의 댓글들을 페이징해서 가져오는 로직을 예시로 사용한다.

 

 

1. comment Entity 

 

 

@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "comment")
public class Comment extends BaseEntity {

    @Column(nullable = false)
    private String text;
    @ManyToOne
    @JoinColumn(name = "user_id")       //N : 1
    private User user;
    @ManyToOne
    @JoinColumn(name = "board_id")       //N : 1
    private Board board;

    public void updateComment(CommentDto commentDto){
        this.text = commentDto.getText();
        this.user = commentDto.getUser();
        this.board = commentDto.getBoard();
    }
}

 

 

2. comment service

 

@Override
public List<ResponseCommentDto> getCommentsInBoard(Pageable pageable, Long board_id) {

    //page로 반환하는 repository 메서드
    Page<Comment> commentPage =  commentRepository.findCommentsByBoardId(pageable, board_id);
   
    // getContent를 통해 리스트로 변환
    List<Comment> commentList = commentPage.getContent();                  
    List<ResponseCommentDto> responseCommentDtoList = new ArrayList<>();

    for(Comment comment : commentList){
        ResponseCommentDto responseCommentDto = new ResponseCommentDto(comment);
        responseCommentDtoList.add(responseCommentDto);
    }

    return responseCommentDtoList;
}

 

comment service의 메서드 중 하나로 한 게시글의 모든 댓글을 가져오는 로직이다. 여기서 페이징이 적용된 모습을 볼 수 있다. 

 

 

3. comment repository

 

@Override
public Page<Comment> findCommentsByBoardId(Pageable pageable, Long board_id) {
    List<Comment> content = jpaQueryFactory.selectFrom(qComment)
            .where(qComment.board.id.eq(board_id))
            .orderBy(qComment.id.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

    JPAQuery<Long> count = getCount(board_id);
    return PageableExecutionUtils.getPage(content, pageable, () -> count.fetchOne());
}

 

repository의 메서드로, 게시글의 id를 통해 그 게시글과 연관된 모든 댓글 데이터를 쿼리하는 메서드이다.

 

pageable의 getOffset과 getPageSize를 이용하는 모습이다. 여기서 offset은 페이징을 할 때, 어디에서 조회를 시작하는 지 나타내는 것이다. limit는 그 offset에서 몇 개를 가져오는 지 나타낸다. 

 

즉 offset과 limit를 조합해서 페이징을 구현하는 것이라 할 수 있다.