SpringBoot REST API Paging 처리 방법

devdo·2022년 3월 21일
2

SpringBoot

목록 보기
19/35
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


업데이트(Tymeleaf, Vue 단)

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";
    }

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

PageableRequestDto

public class PageableRequestDto extends PageRequestDto {
}

PageRequestDto

@Builder
@AllArgsConstructor
@Data
public class PageRequestDto {

    private int page;
    private int size;
    private String type;
    private String keyword;

    public PageRequestDto() {
        this.page = 1;
        this.size = 10;
    }

    public Pageable getPageable(Sort sort){
        return PageRequest.of(page -1, size, sort);
    }
}

PageResponseDto

@Data
public class PageResponseDto<T> {
    // 페이징 content
    private List<T> content;

    // 총 페이지 번호
    private int totalPage;

    // 현재 페이지 번호
    private int page;
    // 목록 사이즈
    private int size;
    // 게시글 총갯수
    private long totalCount;	

    // 화면에서 시작 페이지 번호, 끝 페이지 번호
    private int start, end;

    // 이전, 다음 링크여부
    private boolean prev, next;

    // 화면에 보여줄 페이지 번호 목록 리스트
    private List<Integer> pageList;

    public PageResponseDto(Pageable pageable, Page<T> pageInfo) {
        content = pageInfo.getContent();
        totalPage = pageInfo.getTotalPages();	   // 페이지 총 갯수
        totalCount = pageInfo.getTotalElements();  // 로우 총 갯수
        makePageList(pageable);
    }

    private void makePageList(Pageable pageable) {
        this.page = pageable.getPageNumber() + 1;                       // 0부터 시작하므로 1을 추가
        this.size = pageable.getPageSize();                             // 한 페이지에 포함될 row 의 최대개수, ex) size = 10, 화면에 보여줄 항목개수가 10개다.

        int tempEnd = (int) (Math.ceil(page / (double) size)) * size;   // 현재 페이지를 기준으로 화면에 출력되어야 하는 마지막 페이지 번호를 우선 처리, ex) size = 5, page = 6이다! => tempEnd = 10 인것!

        start = tempEnd - (size - 1);                                   // start = 6 = 10 - (5 - 1)
        end = Math.min(totalPage, tempEnd);                             // 실제 데이터가 부족한 경우를 위해 마지막 페이지 번호는 전체 데이터의 개수를 이용해서 다시 계산
                                                                        // end = (6, 10) = 6
        
        prev = start > 1;                                               // 6 > 1 => true
        next = totalPage > tempEnd;                                     // 6 > 10 => false
        
        pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());          // 현재 보여줄 목록 ex) 마지막 페이지에 들어갔다. 6
    }

}

PageResponseDto(빌더패턴)

@Slf4j
@Getter
@Builder
@ToString
public class PageResponseDto<T> {
    // 페이징 content
    private List<T> content;

    //현재 페이지 번호
    private int page;
    //목록 사이즈
    private int size;
    // 게시글 총갯수
    private long totalCount;
    //시작 페이지 번호, 끝 페이지 번호
    private int start, end;
    //이전, 다음
    private boolean prev, next;

    public static <T> PageResponseDto<T> of(Page<T> pageInfo) {
        Pageable pageable = pageInfo.getPageable();
        int totalPage = pageInfo.getTotalPages();

        int page = pageable.getPageNumber() + 1;   // 0부터 시작하므로 1을 추가
        int size = pageable.getPageSize();

        int tempEnd = (int) (Math.ceil(page / (double) size)) * size;

        int start = tempEnd - (size - 1);
        int end = Math.min(totalPage, tempEnd);
        boolean prev = start > 1;
        boolean next = totalPage > tempEnd;

        return PageResponseDto.<T>builder()
                .content(pageInfo.getContent())
                .page(page)
                .size(size)
                .totalCount(pageInfo.getTotalElements())
                .start(start)
                .end(end)
                .prev(prev)
                .next(next)
                .build();
    }

}

BoardController

    @GetMapping("/api/board/list-page")
    public PageResponseDto listPage(
             PageRequestDto pageRequestDto
    ) {
        log.info("listPage, pageRequestDto: {}", pageRequestDto);

        // 페이징 조건 결합 전체조회
        PageResponseDto BoardInfoPage = boardService.getAllPage(pageRequestDto);
        log.info("listPage BoardInfoPage: {}", BoardInfoPage);
        return BoardInfoPage;
    }

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개의 댓글