본문 바로가기
Programming/TDD Project

Pull Request 019. 페이징 처리 시 페이지 정보도 Response에 함께 담아주기

by JKROH 2023. 12. 1.
반응형

 기존의 페이징 처리에서 문제점은 페이지 정보가 담기지 않는다는 점이었다. 엔티티 정보는 전부 담기지만, 해당 페이지가 몇 번째 페이지인지, 페이지에는 몇 개의 요소가 있는지, 전체 데이터는 몇 개이고 전체 페이지는 몇 페이진지의 정보가 담기지 않았다.

 

 이렇게 페이징 처리가 진행되면 데이터는 넘어가지만, 실질적인 페이징 처리는 프론트엔드 단에 맡겨야한다. 심지어 이 과정 역시 프론트엔드 단에서 전체 데이터 갯수를 알고 있어야 가능하다. 여러 면에서 문제가 존재한다. 따라서 서버 레벨에서 페이지 정보도 함께 담아줘야만이 진정한 의미로 페이징 처리를 끝냈다고 볼 수 있다.

    @GetMapping
    public ResponseEntity<List<FreeBoardDto.Response>> getFreeBoardPage(@RequestParam(name = "page", defaultValue = "1") int pageNumber) {
        List<FreeBoardDto.Response> responseDtoList = freeBoardService.getFreeBoardPage(pageNumber);
        return new ResponseEntity<>(responseDtoList, HttpStatus.OK);
    }

 

 ResponseEntity는 안에 담긴 객체를 JSON타입으로 바꿔주는 역할을 할 뿐이다. 기존의 방식으로는 페이지 정보를 담아줄 수 없다. 데이터의 리스트만 담긴다. 따라서 페이지 정보까지 함께 ResponseEntity객체에 넣어줘야 한다. 그럼 우리는 ResponseEntity 안에 뭘 넣어주어야 할까? 뭘 넣긴 뭘 넣어, 리스트랑 페이지 정보를 같이 넣어주면 되지, 그런데 어떻게? 바로 새로운 객체를 만들어서 넣어주면 된다.

@Getter
public class PagedResponseDto<T> {
    private final List<T> data;
    private final PageInfo pageInfo;

    public PagedResponseDto(List<T> data, Page page) {
        this.data = data;
        this.pageInfo = new PageInfo(page.getNumber() + 1,
                page.getSize(),
                page.getTotalElements(),
                page.getTotalPages());
    }
}

 

 

 데이터 리스트가 담길 List<T>와 페이지 정보가 담길 커스텀 클래스 PageInfo를 필드에 선언한다. List에는 여러 타입의 객체가 담길 수 있다. 자유 게시글이 될 수도, 바버샵 리스트가 될수도 있다. 따라서 제네릭을 사용한다. ResponseEntity가 내부에 담긴 객체 정보에 접근할 수 있도록 하기 위해 접근자 메서드 역시 정의해준다. 이제 PageInfo를 만들자. page.getNumber()에 1을 더해준 이유는 page의 번호는 index라 0부터 시작하기 때문이다.

@AllArgsConstructor
@Getter
public class PageInfo {
    private int page;
    private int size;
    private long totalElements;
    private int totalPage;
}

 

 간단한 데이터 클래스다. 이제 해당 구조에 맞춰 값을 반환할 수 있게 메서드들을 수정해보자.

// FreeBoardController

    @GetMapping
    public ResponseEntity<MultiResponseDto<FreeBoardDto.Response>> getFreeBoardPage(@RequestParam(name = "page", defaultValue = "1") int pageNumber) {
        PagedResponseDto<FreeBoardDto.Response> responseDtoList = freeBoardService.getFreeBoardPage(pageNumber);
        return new ResponseEntity<>(responseDtoList, HttpStatus.OK);
    }

/////////////////////////

// FreeBoardService

    PagedResponseDto<FreeBoardDto.Response> getFreeBoardPage(int pageNumber);
    
/////////////////////////

// FreeBoardServiceManager

    @Override
    @Transactional(readOnly = true)
    public PagedResponseDto<FreeBoardDto.Response> getFreeBoardPage(int pageNumber) {
        verifyPageNumber(pageNumber);
        int pageIndex = pageNumber - 1;
        Page<FreeBoard> page = freeBoardQueryService.readPagedEntity(pageIndex);
        List<FreeBoardDto.Response> responseList = page.stream()
                .map(freeBoardConverter::convertEntityToResponseDto)
                .collect(Collectors.toList());
        return new PagedResponseDto<>(responseList, page);
    }
    
/////////////////////////

// FreeBoardQueryService

    @Transactional(readOnly = true)
    public Page<FreeBoard> readPagedEntity(int pageIndex) {
        Pageable pageable = PageRequest.ofSize(PAGE_SIZE.getValue()).withPage(pageIndex);
        return freeBoardRepository.findAll(pageable);
    }

 

 하나하나 바뀐 점을 살펴보자.

  • FreeBoardController : ResponseEntity에 List 대신 PagedResponseDto를 담아준다. 해당 객체는 Service 계층에서 넘겨준다.
  • FreeBoardService : getFreeBoardPage의 반환 값을 PagedResponseDto로 바꿨다.
  • FreeBoardServiceManager : 기존에는 List를 받아와서 안에 담긴 값들을 Response DTO로 바꾼 List를 넘겼다. 이제는 Page를 받아오고 해당 Page에서 List를 뽑아쓴다. 그리고 PagedResponseDto에 List와 Page를 둘다 담는다.
  • FreeBoardQueryService : 기존에는 Repository에서 찾아온 Page에서 List를 뽑아서 넘겼다. 이제는 찾아온 Page를 그대로 넘긴다.

 문제는 이렇게 바꾸니 테스트 코드에서 아주 많은 빨간줄들을 띄워줬다는 것이다. 당연하다, 값들이 완전히 뒤집어졌으니까. 테스트 코드도 수정하자.

// FreeBoardControllerTest

    @Test
    @DisplayName("게시글 페이지 조회 요청 테스트")
    void getFreeBoardPageTest() throws Exception {
        // given
        ...

        Page<FreeBoard> fakePage = new PageImpl<>(List.of(
                FreeBoard.builder().title("1").content("1").build(),
                FreeBoard.builder().title("2").content("2").build(),
                FreeBoard.builder().title("3").content("3").build(),
                FreeBoard.builder().title("4").content("4").build(),
                FreeBoard.builder().title("5").content("5").build(),
                FreeBoard.builder().title("6").content("6").build(),
                FreeBoard.builder().title("6").content("6").build()
        ));
        
        ...
        
        given(freeBoardService.getFreeBoardPage(anyInt())).willReturn(new PagedResponseDto<>(fakeResponseList, fakePage));

        // when // then
        for (int i = 0; i < fakeResponseList.size(); i++) {
            mockMvc.perform(
                            get("/free-board?page={page}", fakePageNumber)
                                    .accept(MediaType.APPLICATION_JSON)
                                    .contentType(MediaType.APPLICATION_JSON)
                    ).andExpect(jsonPath("data", hasSize(fakeResponseList.size())))
                    .andExpect(jsonPath("data[%d].boardId", i).value(fakeResponseList.get(i).boardId))
                    .andExpect(jsonPath("data[%d].title", i).value(fakeResponseList.get(i).title))
                    .andExpect(jsonPath("data[%d].content", i).value(fakeResponseList.get(i).content))
                    .andExpect(jsonPath("data[%d].viewCount", i).value(fakeResponseList.get(i).viewCount))
                    .andExpect(jsonPath("data[%d].createdAt", i).value(fakeResponseList.get(i).createdAt))
                    .andExpect(jsonPath("data[%d].modifiedAt", i).value(fakeResponseList.get(i).modifiedAt));
        }

        mockMvc.perform(
                        ...
                        
                        responseFields(
                                List.of(
                                        fieldWithPath("data[].boardId").type(JsonFieldType.NUMBER).description("게시글 식별자"),
                                        fieldWithPath("data[].title").type(JsonFieldType.STRING).description("제목"),
                                        fieldWithPath("data[].content").type(JsonFieldType.STRING).description("내용"),
                                        fieldWithPath("data[].viewCount").type(JsonFieldType.NUMBER).description("조회수"),
                                        fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("작성일"),
                                        fieldWithPath("data[].modifiedAt").type(JsonFieldType.STRING).description("수정일"),
                                        fieldWithPath("pageInfo.page").type(JsonFieldType.NUMBER).description("페이지 번호"),
                                        fieldWithPath("pageInfo.size").type(JsonFieldType.NUMBER).description("해당 페이지에 담긴 게시글 수"),
                                        fieldWithPath("pageInfo.totalElements").type(JsonFieldType.NUMBER).description("전체 게시글 수"),
                                        fieldWithPath("pageInfo.totalPage").type(JsonFieldType.NUMBER).description("전체 페이지 수")
                                )
                        )
                ));
    }

////////////////////

// FreeBoardQueryServiceTest

    @Test
    @DisplayName("여러 개의 자유 게시글을 읽어온다.")
    void testReadPagedEntity() {
    
        ...

        //when
        Page<FreeBoard> foundPage = freeBoardQueryService.readPagedEntity(fakePageNumber);

        //then
        assertThat(foundPage).isEqualTo(fakePage);
    }

 

 기존과 같은 부분은 지우고 새롭게 수정된 부분만 남겼다.

  • FreeBoardControllerTest : getFreeBoardPage()의 반환 값에 맞게 새롭게 Page를 정의했다. 또한 반환되는 ResponseEntity의 JSON 형식에 맞게 값들을 수정했다.
  • FreeBoardQueryService : readPagedEntity를 통해 Page를 받아오게 수정했고, 검증 단계에서 Page간 비교기 때문에 불필요한 getContent()를 제거했다.

 마지막으로 Postman을 돌려서 원하는 결과가 잘 나오는지 확인해보자.

{
    "data": [
        {
            "boardId": 1,
            "title": "title",
            "content": "s123",
            "viewCount": 0,
            "createdAt": "2023. 12. 1. 오후 4:29:39",
            "modifiedAt": "2023. 12. 1. 오후 4:29:39"
        },
        {
            "boardId": 2,
            "title": "title",
            "content": "s123",
            "viewCount": 0,
            "createdAt": "2023. 12. 1. 오후 4:29:40",
            "modifiedAt": "2023. 12. 1. 오후 4:29:40"
        },
        
       ...
       
    ],
    "pageInfo": {
        "page": 1,
        "size": 20,
        "totalElements": 5,
        "totalPage": 1
    }
}

 

 야무지게 잘 나오는 모습을 확인할 수 있다.

 

 이렇게 페이징 처리를 끝냈다. 다음으로 어떤 기능을 건드려야할지 고민이 된다. 로그인 쪽을 먼저 해볼지, 다른 엔티티들의 구현을 마쳐야 할지, 그것도 아니면 외부 API와의 연동을 먼저 해봐야할지, 아니면 기존의 테스트 코드를 더 좋은 테스트 코드로 만들어봐야할지 순서를 잘 모르겠다. 어짜피 다 해야 하긴 하지만... 그럼에도 일의 순서를 찰지게 정해놓는게 더 좋을 것 같아서, 고민을 좀 해봐야겠다.

 

 전체 코드는 링크에서 확인할 수 있다.

반응형

댓글