SpringBoot REST API Paging 처리 방법

devdo·2022년 3월 21일
2

SpringBoot

목록 보기
19/42
post-thumbnail

SpringBoot에서는 JPA를 통해 페이징 처리를 쉽게 활용할 수 있습니다.

예제 소스는 TodoList-backend 소스를 참고하여 작성하였습니다.


Page 정보를 가지고 있는 Pageable 객체

controller단에서 파라미터로 받는 정보들
: @RequestParam : pageNo(🌟0부터 시작), pageSize(페이징 갯수), sortBy

또는 RequestDto 에서

받아서, Pageable 객체 를 생성해야 합니다.
그리고 이 Pageable 객체를 파라미터로 받아 JpaRepository의 목록 관련 findAll() 과 같은 메서드로
-> Page<Entity> 로 return 됩니다.

Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(sortBy).descending);	

Page<TodoEntity> todoPage = todoRepository.findAll(pageable);
// or
Page<TodoResponse> todoDtoPage = todoRepository.findAll(pageable).map(this::mapToDto);

처리 로직 정리(entity -> Dto)

그리고 Service 내에서 Entity를 Dto로 변환해주어야 합니다.
이것은 stream() 내 map() 메서드와 entityToDto 메서드를 미리 만들어서 처리해주면 됩니다.

findAll(pagable); => Page<Entity> -> getContent(); ->List<Entity> 
-> mapping(stream) -> List<Dto>

✅ Response DTO를 만들어줘야 하는 이유!(front단이 받아야할 Page정보 외에도 많이 있을 수 있기에!)

Page<Entity>content 를 따로 List<PostResponse> 로 처리하는 것을 잊으면 안됩니다!

front단에 보내주는 Dto 필드에 Paging 내용을 넣어줄 것!
예를들어 이런 것들이 있습니다.

totalElements
totalPages
last boolean


✳️ 페이징 용어 정리

  • page - 현재 페이지 넘버

  • size - 페이지 사이즈

  • offset - 최초 시작점

  • limit - size랑 같음

  • totalElements - 데이터 로우 총 갯수

  • totalPages - 페이지들 총 갯수


구현 소스

PageResponse

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PageResponse {

    private List<TodoResponse> content;
    private int pageNo;
    private int pageSize;
    private long totalElements;
    private int totalPages;
    private boolean last;

}

TodoController

    // Paging test
    @GetMapping
    public PageResponse readAllPaging(
            @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo,
            @RequestParam(value = "pageSize", defaultValue = "3", required = false) int pageSize,
            @RequestParam(value = "sortBy", defaultValue = "id", required = false) String sortBy
    ) {
        log.info("Read Paging All");
        return todoService.searchAllPaging(pageNo, pageSize, sortBy);
    }

or

MemoController

    @GetMapping("/memo")
    public ResponseEntity<Page<MemoResponse>> list(
            @PageableDefault(sort = "id", direction = Sort.Direction.DESC, size = 5)
            Pageable pageable)
    {
        Page<MemoResponse> responsePage = memoService.list(pageable);
        return new ResponseEntity<>(responsePage, HttpStatus.OK);
    }

TodoService

    @Override
    public PageResponse searchAllPaging(int pageNo, int pageSize, String sortBy) {

        // create Pageable instance
        Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(sortBy).descending());
        Page<TodoEntity> todoPage = todoRepository.findAll(pageable);
        
		// .map()을 더 추가해서 바로 Page<TodoResponse> 값으로 시작할 수 있어!
		// Page<TodoResponse> todoDtoPage = todoRepository.findAll(pageable).map(this::mapToDto);

        List<TodoEntity> listTodos = todoPage.getContent();

        List<TodoResponse> content = listTodos.stream().map(TodoEntity -> mapToDto(TodoEntity)).collect(Collectors.toList());

        return PageResponse.builder()
                .content(content)	// todoDtoPage.getContent()
                .pageNo(pageNo)
                .pageSize(pageSize)
                .totalElements(todoPage.getTotalElements())
                .totalPages(todoPage.getTotalPages())
                .last(todoPage.isLast())
                .build();

    }

postman 결과 값

1) PageResponse

2) Page<T>

@PageableDefault(sort = "id", direction = Sort.Direction.DESC, size = 5) Pageable pageable

또는, @PageableDefault 어노테이션으로 페이징Request를 처리할 수 있다.

Tymeleaf (MVC)

// view에 보낼 페이징 코드 
// @PageableDefault 로 파라미터 정리
    @GetMapping("/list")
    public String list(Model model,
                       @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable                       
    ) {
        Page<Board> list = null;

        int nowPage = list.getPageable().getPageNumber() + 1;
        int startPage = Math.max(nowPage - 4, 1);
        int endPage = Math.min(nowPage + 5, list.getTotalPages());

        model.addAttribute("list", list);
        model.addAttribute("nowPage", nowPage);
        model.addAttribute("startPage", startPage);
        model.addAttribute("endPage", endPage);

        return "board/list";
    }

service

    @Transactional(readOnly = true)
    @Override
    public Page<PostResponse> listPage1(Pageable pageable) {
        return postRepository.findAll(pageable)
                .map(PostResponse::toDto);
    }

🌟업데이트(DTO로 커스터마이징)

DTO 내용에 Page 정보를 계산하는 방정식도 기입

PageRequestDTO

@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {

    @Builder.Default
    private int page = 1;

    @Builder.Default
    private int size = 10;

    @Builder.Default
    private String sort = "desc";

}

PageResponseDTO

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResponseDTO<E> {

    private List<E> dtoList;

    private List<Integer> pageNumList;

    private PageRequestDTO pageRequestDTO;

    private boolean prev, next;

    private int totalCount, prevPage, nextPage, totalPage, current;

    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(List<E> dtoList, PageRequestDTO pageRequestDTO, long totalCount) {

        this.dtoList = dtoList;
        this.pageRequestDTO = pageRequestDTO;
        this.totalCount = (int)totalCount;

        int end =   (int)(Math.ceil( pageRequestDTO.getPage() / 10.0 )) *  10;

        int start = end - 9;

        int last =  (int)(Math.ceil((totalCount/(double)pageRequestDTO.getSize())));

        end =  end > last ? last: end;

        this.prev = start > 1;


        this.next =  totalCount > end * pageRequestDTO.getSize();

        this.pageNumList = IntStream.rangeClosed(start,end).boxed().collect(Collectors.toList());

        if(prev) {
            this.prevPage = start -1;
        }

        if(next) {
            this.nextPage = end + 1;
        }

        this.totalPage = this.pageNumList.size();

        this.current = pageRequestDTO.getPage();

    }
}

Service

    @Override
    public PageResponseDTO<PostResponse> listPage2(PageRequestDTO requestDTO) {

        Pageable pageable = PageRequest.of(
                requestDTO.getPage() - 1,  //페이지 시작 번호가 0부터 시작하므로
                requestDTO.getSize(),
                "asc".equals(requestDTO.getSort()) ?  // 정렬 조건
                        Sort.by("id").ascending() : Sort.by("id").descending()
        );

        Page<Post> result = postRepository.findAll(pageable);

        return PageResponseDTO.<PostResponse>withAll()
                .dtoList(result.stream().map(PostResponse::toDto).toList())
                .totalCount(result.getTotalElements())
                .pageRequestDTO(requestDTO)
                .build();
    }

Controller

    @GetMapping("/list/page2")
    public Response<PageResponseDTO<PostResponse>> listPage2(
            PageRequestDTO requestDTO
    ){
        return Response.success(postService.listPage2(requestDTO));
    }

PageableRequestDto

public class PageableRequestDto extends PageRequestDto {
}

QueryDSL 내로 처리할려면?

✅ BoardRepsitory(QuerydslPredicateExecutor<T> 상속)
booleanbuilder 파라미터를 받은 페이징 타입 메서드를 사용할 수 있다.

BoardRepository

public interface BoardRepository extends JpaRepository<Board, Long>, 
QuerydslPredicateExecutor<Board> {
    Optional<Board> findByIdAndMemberId(Long boardId, int memberId);
    List<Board> findByMemberId(int memberId);
}

BoardService

    @Transactional(readOnly = true)
    @Override
    public PageResponseDto getAllPage(PageRequestDto pageRequestDto) {

        Pageable pageable = pageRequestDto.getPageable(Sort.by("id").descending());
        BooleanBuilder booleanBuilder = getSearch(pageRequestDto);

        Page<BoardInfo> boardInfoPage = boardRepository.findAll(booleanBuilder, pageable)
                .map(BoardInfo::toInfo);

        log.info("getAll boardInfoPage: {}", boardInfoPage);
        return new PageResponseDto(pageable, boardInfoPage);
    }

    /**
     * 검색 조건 서칭 - querydsl 사용, 페이징과는 상관없음.
     * @return Boolean 조건 집합
     */
    private BooleanBuilder getSearch(PageRequestDto pageRequestDto) {
        String type = pageRequestDto.getType();
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        QBoard qBoard = QBoard.board;
        String keyword = pageRequestDto.getKeyword();
        BooleanExpression expression = qBoard.id.gt(0L); // id > 0 조건만
        booleanBuilder.and(expression);

        // 검색 조건 없는 경우
        if (StringUtils.isEmpty(type)) {
            return booleanBuilder;
        }

        // 검색 조건이 있는 경우
        BooleanBuilder conditionBuilder = new BooleanBuilder();

        if(type.contains("t")){
            conditionBuilder.or(qBoard.title.contains(keyword));
        }
        if(type.contains("c")){
            conditionBuilder.or(qBoard.content.contains(keyword));
        }
        if(type.contains("w")){
            conditionBuilder.or(qBoard.writer.contains(keyword));
        }

        //모든 조건 통합
        booleanBuilder.and(conditionBuilder);
        return booleanBuilder;
    }

vue

  <!--  pagination  -->
  <nav aria-label="Page navigation example">
    <ul class="pagination justify-content-center">
      <template v-if="prev">
        <li class="page-item">
        <a class="page-link" href="javascript:;" @click="fnPage(1)" >&lt;&lt;</a> &nbsp;
        <a class="page-link" href="javascript:;" @click="fnPage(`${start-1}`)">&lt;</a>
        </li>
      </template>
      &nbsp;
      <template v-for="(n,index) in pageList" :key=index>
        <template v-if="page==n">
          <li class="page-item">
          <span class="page-link" :key="index">{{ n }}</span>
          </li>
        </template>
        <template v-else>
          <li class="page-item">
          <a class="page-link" href="javascript:;" @click="fnPage(`${n}`)" :key="index">{{ n }}</a>
          </li>
        </template>
      </template>
      &nbsp;
      <template v-if="next">
        <li class="page-item">
          <a class="page-link" href="javascript:;" @click="fnPage(`${end+1}`)">&gt;</a> &nbsp;
          <a class="page-link" href="javascript:;" @click="fnPage(`${totalPage}`)">&gt;&gt;</a>
        </li>
      </template>
      </ul>
  </nav>

..
          
<script>
import axios from "axios";
export default {
  data() { // 동적 바인딩 data
    return {
      requestBody: {},  // 리스트 페이지 데이터전송
      list: null,
      type: "",
      keyword: "",
      // page
      totalCount: 0,
      prev: false,
      next: false,
      page: 1,
      pageList: [],
      size: 10,
      start: 1,
      end: 0,
      totalPage: 0,
    }
  },
  mounted() {
    this.fnGetList();
  },
  methods: {
    detail(id) {
      this.$router.push({
        path: `/board/detail/${id}`,
        query: this.requestBody
      });
    },

    fnGetList() {
      this.requestBody = { // 데이터 전송
        type: this.type,
        keyword: this.keyword,
        page: this.page,
        size: this.size
      }
      axios.get("/api/board/list-page", {
        params: this.requestBody
      }).then((res) => {
        console.log(res);
        this.list = res.data.content;
        // page
        this.totalCount = res.data.totalCount;
        this.prev = res.data.prev;
        this.next = res.data.next;
        this.page = res.data.page;
        this.pageList = res.data.pageList;
        this.size = res.data.size;
        this.start = res.data.start;
        this.end = res.data.end;
        this.totalPage = res.data.totalPage;
      }).catch((err) => {
        console.log(err);
      })
    },
    fnPage(n) {
      if (this.page !== n) {
        this.page = n;
      }
      this.fnGetList()
    },
  },
}
</script>          


참고

소스출처

profile
배운 것을 기록합니다.

0개의 댓글