ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Data JPA에서 페이징 구현하기
    프로젝트/게시판 프로젝트 2022. 7. 1. 13:27

    Spring Data JPA를 활용하여 간편하게 페이징을 구현하여 보겠습니다.

    페이징이란 블로그 서비스나 게시글 서비스에서 흔히 볼 수 있습니다.

     

    게시글이 1000개라면 모든 글들이 한 페이지에 보이는 것이 아니라 1번, 2번.... N번으로 별도의 페이지에 10개씩 들어가는 등 페이징을 통해 사용자에게 보이게 됩니다.

     

    1. 우선 PagingAndSortingRepository를 사용해야 합니다.

    하지만 기존에 JPARepository를 안으로 타고 들어가면 PagingAndSortingRepository를 구현하고 있습니다.

    public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    

     

    PagingAndSortingRepository

    @NoRepositoryBean
    public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    
       /**
        * Returns all entities sorted by the given options.
        *
        * @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
        *          {@literal null}.
        * @return all entities sorted by the given options
        */
       Iterable<T> findAll(Sort sort);
    
       /**
        * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
        *
        * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
        *          {@literal null}.
        * @return a page of entities
        */
       Page<T> findAll(Pageable pageable);
    }
    

     

     

    2. Service 구현

    이미 인터페이스가 구현되어 있기 때문에 이것만 적절히 활용하면 됩니다.

    우선 Pageable에 대해서 조금 알아보겠습니다.

     

    JPA에서는 Pagable이라는 객체를 제공하여 정렬방식, 페이지의 크기, 그리고 몇 번째 페이지의 요청에 따른 정보를 받고, 해당 정보를 통해 DB와 통신하여 내림차순, 1쪽 10개의 글 구성의 3번째 페이지의 정보를 달라는 식으로 요청하게 됩니다.

     

    Pagable은 컨틀롤러에서 아래와 같이 GET 요청을 통해서도 구성할 수 있습니다.

    GET /boards/page?lastName=kim&page=3&size=10&sort=id,DESC

     

    Pagable은 다음과 같은 형식으로 이루어져 있습니다.

    "content":[
    {
        "content": [
            {"id": 1, "username": "User 0", "address": "Korea", "age": 0},
            // 중간 생략
            {"id": 5, "username": "User 4", "address": "Korea", "age": 4}
        ],
        "pageable": {
            "sort": {
                "sorted": false, // 정렬 상태
                "unsorted": true,
                "empty": true
            },
            "pageSize": 5, // 한 페이지에서 나타내는 원소의 수 (게시글 수)
            "pageNumber": 0, // 페이지 번호 (0번 부터 시작)
            "offset": 0, // 해당 페이지에 첫 번째 원소의 수
            "paged": true,
            "unpaged": false
        },
        "totalPages": 20, // 페이지로 제공되는 총 페이지 수
        "totalElements": 100, // 모든 페이지에 존재하는 총 원소 수
        "last": false,
        "number": 0,
        "sort": {
            "sorted": false,
            "unsorted": true,
            "empty": true
        },
        "size": 5,
        "numberOfElements": 5,
        "first": true,
        "empty": false
    }

     

     

    저는 GET 요청을 통해서 인자들을 받지 않고 현재 요청하는 페이지 번호만 받아서 페이징을 구현하여 보겠습니다.

     @Override
        public Page<BoardPagingDTO> paging(int id) {
    
            Pageable curPage = PageRequest.of(id, 10, Sort.by("id")
                                                          .descending());
            Page<Board> result = boardRepository.findAll(curPage);
            Page<BoardPagingDTO> map = result.map(board -> board.toBoardPagingDTO(board));
            return map;
        }

    위의 코드는 페이징을 구현하는 메서드입니다.

    요청정보로 페이징 id값을 받아와서 Pagable 객체인 curPage를 만들어줍니다.

    이때 PageRequest.of(페이지 번호, 몇 개의 글을 가져올 것인지, 정렬 방식)을 지정합니다.

     

    이후에는 위에서 봤던 구현된 인터페이스를 호출하게 되는데 boardRepository.findAll(Pagable 인자) Pagable 인자를 넣어줍니다.

     

    그렇게 되면 제너릭을 Board Entity로 가지는 Page객체가 생성됩니다.

    이후에 컨트롤러에 반환하기 위하여 Board Entity에 toBoardPagingDTO 메서드를 생성하고 map 메서드를 통하여 BoardPagingDTO로 변환하여 반환합니다.

     

    Board Entity의 toBoardPagingDTO 메서드

    public BoardPagingDTO toBoardPagingDTO(Board board) {
        return BoardPagingDTO.builder()
                             .id(board.id)
                             .title(board.title)
                             .content(board.content)
                             .writer(board.writer)
                             .hits(board.hits)
                             .build();
    }

     

    3. 컨트롤러 구현

    @GetMapping({"/page/{pagingid}"})
    public ResponseEntity<BasicResponseDTO> paging(@PathVariable("pagingid") Integer pagingId) {
    
        Page<BoardPagingDTO> resultPage = boardService.paging(pagingId - 1);
    
        if (pagingId > resultPage.getTotalPages()) {
            throw new IllegalStateException();
        }
    
        return ResponseEntity.status(HttpStatus.OK)
                             .body(makeSelectResponseDTO(SUCCESS, resultPage));
    }

    클라이언트로부터 pagingId를 받아서 service레이어의 paging 메서드를 호출하여 Page<BoardPagingDTO> 타입의 resultPage를 반환받습니다.

     

    이때 pagingId -1로 인자를 넘기는 이유는 JPA의 페이징은 0부터 시작하기 때문입니다.

    (클라이언트가 1페이지라는 의미로 1을 넘겨야 1-1 = 0 이 되면서  첫 페이지)

     

    그리고 만약 현재가진 페이지의 수보다 높은 pagingId가 들어온다면 IllegalStateException을 발생시킵니다.

    여기서 만약 -1 같은 음수가 들어오는 경우에도 IllegelStateException이 발생합니다.

     

    이후에는 응답 DTO에 resultPage 객체를 담아서 전송합니다.

     

    4. 테스트 코드 작성

    서비스

    @Test
    void 페이징() {
        //given
        BoardRequestDTO boardRequestDTO = makeBoardRequestDTO();
        for (int i = 0; i < 43; i++) {
            boardService.posting(boardRequestDTO);
        }
    
        //when
        Page<BoardPagingDTO> firstPaging = boardService.paging(0);
    
        Pageable LastPageWithTenElements = PageRequest.of(4, 10);
        Page<BoardPagingDTO> lastPaging = boardService.paging(4);
    
        //then
        Assertions.assertThat(10)
                  .isEqualTo(firstPaging.getContent()
                                        .size());
        Assertions.assertThat(3)
                  .isEqualTo(lastPaging.getContent()
                                       .size());
    }
    
    @Test
    void 페이징음수() {
        //given
        BoardRequestDTO boardRequestDTO = makeBoardRequestDTO();
        boardService.posting(boardRequestDTO);
    
        //then
        assertThrows(IllegalArgumentException.class, () -> {
            //when
            Page<BoardPagingDTO> firstPaging = boardService.paging(-1);
        });
    
    }
    
    @Test
    void 페이징초과(){
        //given
        BoardRequestDTO boardRequestDTO = makeBoardRequestDTO();
        boardService.posting(boardRequestDTO);
    
        //when
        Page<BoardPagingDTO> firstPaging = boardService.paging(100);
    
        //then
        Assertions.assertThat(firstPaging.getContent().size()).isEqualTo(0);
    }

    서비스 레이어에서는 3개의 테스트 코드를 작성해 보았습니다.

    페이징() -> 43개의 게시글을 생성하고 페이징의 개수가 적절하게 이루어지는지 확인

    페이징 음수() -> 페이징에 음수를 넣을 경우 예외가 발생하는지 확인

    페이징 초과() -> 페이징에 초과된 값을 넣으면 예외는 발생하지 않고 사이즈가 0이 출력됨을 확인

     

     

    컨트롤러 테스트 코드 (Rest Docs 적용)

    @Test
    void 게시글_페이징() throws Exception {
        final int pagingId = 1;
        Page<BoardPagingDTO> boardPagingDTO = makePageingDTO();
        given(boardService.paging(any(Integer.class))).willReturn(boardPagingDTO);
    
        //then
        ResultActions resultActions = mvc.perform(get("/boards/page/{pagingid}", pagingId));
    
        resultActions.andExpect(status().isOk())
                     .andDo(document("board-paging-success",
                             preprocessResponse(prettyPrint()),
                             pathParameters(
                                     parameterWithName("pagingid").description("페이징 번호")
                             ),
                             responseFields(
                                     fieldWithPath("message").description("메시지"),
    
                                     fieldWithPath("responseData").description("반환값"),
                                     fieldWithPath("responseData.content.[].id").description("게시글 번호"),
                                     fieldWithPath("responseData.content.[].title").description("제목"),
                                     fieldWithPath("responseData.content.[].content").description("본문"),
                                     fieldWithPath("responseData.content.[].writer").description("작성자"),
                                     fieldWithPath("responseData.content.[].hits").description("조회수"),
    
                                     fieldWithPath("responseData.pageable.sort.sorted").description("정렬 됬는지 여부"),
                                     fieldWithPath("responseData.pageable.sort.unsorted").description("정렬 안됬는지 여부"),
                                     fieldWithPath("responseData.pageable.sort.empty").description("데이터가 비었는지 여부"),
    
                                     fieldWithPath("responseData.pageable.pageNumber").description("현재 페이지 번호"),
                                     fieldWithPath("responseData.pageable.pageSize").description("한 페이지당 조회할 데이터 개수"),
                                     fieldWithPath("responseData.pageable.offset").description("몇번째 데이터인지 (0부터 시작)"),
                                     fieldWithPath("responseData.pageable.paged").description("페이징 정보를 포함하는지 여부"),
                                     fieldWithPath("responseData.pageable.unpaged").description("페이징 정보를 안포함하는지 여부"),
    
                                     fieldWithPath("responseData.last").description("마지막 페이지 인지 여부"),
                                     fieldWithPath("responseData.totalPages").description("전체 페이지 개수"),
                                     fieldWithPath("responseData.totalElements").description("테이블 데이터 총 개수"),
                                     fieldWithPath("responseData.first").description("첫번째 페이지인지 여부"),
                                     fieldWithPath("responseData.numberOfElements").description("요청 페이지에서 조회 된 데이터 개수"),
                                     fieldWithPath("responseData.number").description("현재 페이지 번호"),
                                     fieldWithPath("responseData.size").description("한 페이지당 조회할 데이터 개수"),
    
                                     fieldWithPath("responseData.sort.sorted").description("정렬 됬는지 여부"),
                                     fieldWithPath("responseData.sort.unsorted").description("정렬 안됬는지 여부"),
                                     fieldWithPath("responseData.sort.empty").description("데이터가 비었는지 여부"),
    
                                     fieldWithPath("responseData.empty").description("데이터가 비었는지 여부"),
    
                                     fieldWithPath("errorMessage").description("에러 메시지")
    
                             )
                     ));
    
    
    }
    
    
     private Page<BoardPagingDTO> makePageingDTO() {
    
            List<BoardPagingDTO> data = new ArrayList<>();
            for (long i = 1; i <= 2; i++) {
                data.add(BoardPagingDTO.builder()
                                       .id(i)
                                       .title("본문")
                                       .content("제목")
                                       .writer("작성자")
                                       .hits(i)
                                       .build());
            }
    
    
            Pageable pageable = PageRequest.of(0, 10);
            return new PageImpl<BoardPagingDTO>(data, pageable, 2);
        }

     

     

     

     

    출처

    https://www.baeldung.com/rest-api-pagination-in-spring

     

    REST Pagination in Spring | Baeldung

    Pagination in a Spring REST Service - URI structure and best practice, Page as Resource vs Page as Representation.

    www.baeldung.com

    https://blog.naver.com/PostView.naver?blogId=qjawnswkd&logNo=222394684328&parentCategoryNo=&categoryNo=27&viewDate=&isShowPopularPosts=false&from=postView 

     

    @WebMvcTest 컨트롤러 단위 테스트에서 페이징 테스트 (RestDocs)

    통합 테스트가 아닌 단위 테스트에서 Controller 에서 페이징 처리된 로직을 테스트하려면 PageImpl 을 ...

    blog.naver.com

    https://tecoble.techcourse.co.kr/post/2021-08-15-pageable/

     

    Pageable을 이용한 Pagination을 처리하는 다양한 방법

    Spring Data JPA에서 Pageable 를 활용한 Pagination 의 개념과 방법을 알아본다.

    tecoble.techcourse.co.kr

     

    댓글

Designed by Tistory.