헤르메스 LIFE

[Spring Boot] 게시판 #3 - 게시판 목록 + 페이징처리 본문

Spring Boot Framework

[Spring Boot] 게시판 #3 - 게시판 목록 + 페이징처리

헤르메스의날개 2023. 4. 25. 00:15
728x90

octopus_bbs.zip
1.07MB

게시판을 작성해보려 합니다. 조금씩 살을 붙여나가 보려고 합니다.

게시판 목록의 디자인 및 일부 소스는 도뎡님의 허락을 받아 사용했습니다.

https://congsong.tistory.com/26

 

스프링 부트(Spring Boot) - 페이징(Paging) & 검색(Search) 처리하기 1/2 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBat

본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다. DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기) 화면 처리는 HTML5 기반

congsong.tistory.com


개발환경

STS 4.17.2.RELEASE

OpenJDK Runtime Environment Zulu11.62+17-CA (build 11.0.18+10-LTS)

Spring Boot 2.7.9

lombok

devtools

postgresql 14.1

Gladle

Thymeleaf


BoardController.java

/* 첨부 참조 */
    /**
     * <pre>
     * 게시글 리스트 페이지
     * </pre>
     * 
     * @return
     */
    @GetMapping("/list/page")
    public String findAllOfPage(@ModelAttribute("params") final BoardSearchDto params,
            @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
            Model model) {
        // page : 페이지
        // size : record의 갯수
        model.addAttribute("result", boardService.findAllOfPage(params, pageable));
        
        return "post/list";
    }
    
    /**
     * <pre>
     * 게시글 리스트 페이지
     * </pre>
     * 
     * @return
     */
    @GetMapping("/list/page2")
    public String findAllOfPage2(@ModelAttribute("params") final BoardSearchDto params,
            Model model) {
        
        PageRequest pageRequest = PageRequest.of(params.getPage(), params.getRecordSize(), Sort.by(Sort.Direction.DESC, "id"));
        
        model.addAttribute("result", boardService.findAllOfPage2(params, pageRequest));
        
        return "post/list";
    }

BoardService.java

/* 첨부 참조 */    
    @Transactional(readOnly = true) // 선언적 트랜잭션을 사용
    public PagingListResult<BoardDto> findAllOfPage(BoardSearchDto params, Pageable pageable) {
        log.info("Offset :: {}", pageable.getOffset());
        log.info("PageSize :: {}", pageable.getPageSize());
        log.info("PageNumber :: {}", pageable.getPageNumber());
        
        List<BoardDto> boardList = null;
        Page<TBoardM>  list      = boardRepository.findAll(pageable);
        log.debug("board :: {}", list);
        
        if (list.hasContent()) {
            boardList = list.stream().map(data -> new BoardDto(data))
                    .collect(Collectors.toList());
            
            List<TBoardM> board     = list.getContent();       // 검색된 데이터
            int           totalPage = list.getTotalPages();    // 전체 페이지 수
            boolean       hasNext   = list.hasNext();          // 다음 페이지 존재여부
            int           totalCnt  = (int)list.getTotalElements(); // 검색된 전체 건수
            boolean       isData    = list.hasContent();       // 검색된 자료가 있는가?
            
            log.info("board :: {}", board);
            log.info("totalPage :: {}", totalPage);
            log.info("hasNext :: {}", hasNext);
            log.info("totalCnt :: {}", totalCnt);
            log.info("isData :: {}", isData);
            
            // Pagination 객체를 생성해서 페이지 정보 계산 후 SearchDto 타입의 객체인 params에 계산된 페이지 정보 저장
            Pagination pagination = new Pagination(totalCnt, params);
            params.setPagination(pagination);
            
            return new PagingListResult<>(boardList, pagination);
        } else {
            return new PagingListResult<>(Collections.emptyList(), null);
        }
    }
    
    @Transactional(readOnly = true) // 선언적 트랜잭션을 사용
    public PagingListResult<BoardDto> findAllOfPage2(final BoardSearchDto params, PageRequest pageRequest) {
        log.info("Offset :: {}", pageRequest.getOffset());
        log.info("PageSize :: {}", pageRequest.getPageSize());
        log.info("PageNumber :: {}", pageRequest.getPageNumber());
        
        List<BoardDto> boardList = null;
        Page<TBoardM>  list      = boardRepository.findAll(pageRequest);
        log.debug("board :: {}", list);
        
        if (list.hasContent()) {
            boardList = list.stream().map(data -> new BoardDto(data))
                    .collect(Collectors.toList());
            
            List<TBoardM> board     = list.getContent();       // 검색된 데이터
            int           totalPage = list.getTotalPages();    // 전체 페이지 수
            boolean       hasNext   = list.hasNext();          // 다음 페이지 존재여부
            int           totalCnt  = (int)list.getTotalElements(); // 검색된 전체 건수
            boolean       isData    = list.hasContent();       // 검색된 자료가 있는가?
            
            log.info("board :: {}", board);
            log.info("totalPage :: {}", totalPage);
            log.info("hasNext :: {}", hasNext);
            log.info("totalCnt :: {}", totalCnt);
            log.info("isData :: {}", isData);
            
            // Pagination 객체를 생성해서 페이지 정보 계산 후 SearchDto 타입의 객체인 params에 계산된 페이지 정보 저장
            Pagination pagination = new Pagination(totalCnt, params);
            params.setPagination(pagination);
            
            return new PagingListResult<>(boardList, pagination);
        } else {
            return new PagingListResult<>(Collections.emptyList(), null);
        }
    }

Pagination.java

package octopus.base.model;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Getter
public class Pagination {
    
    private long    totalRecordCount; // 전체 데이터 수
    private int     totalPageCount;   // 전체 페이지 수
    private int     startPage;        // 첫 페이지 번호
    private int     endPage;          // 끝 페이지 번호
    private int     limitStart;       // LIMIT 시작 위치
    private boolean existPrevPage;    // 이전 페이지 존재 여부
    private boolean existNextPage;    // 다음 페이지 존재 여부
    
    public Pagination(long totalRecordCount, SearchDto params) {
        if (totalRecordCount > 0) {
            this.totalRecordCount = totalRecordCount;
            calculation(params);
        }
    }
    
    private void calculation(SearchDto params) {
        
        // 전체 페이지 수 계산
        totalPageCount = (int) ((totalRecordCount - 1) / params.getRecordSize()) + 1;
        
        log.debug("totalPageCount :: {}", totalPageCount);
        
        // 현재 페이지 번호가 전체 페이지 수보다 큰 경우, 현재 페이지 번호에 전체 페이지 수 저장
        if (params.getPage() > totalPageCount) {
            params.setPage(totalPageCount);
        }
        
        // 첫 페이지 번호 계산
        startPage = ((params.getPage() - 1) / params.getPageSize()) * params.getPageSize() + 1;
        
        // 끝 페이지 번호 계산
        endPage = startPage + params.getPageSize() - 1;
        
        // 끝 페이지가 전체 페이지 수보다 큰 경우, 끝 페이지 전체 페이지 수 저장
        if (endPage > totalPageCount) {
            endPage = totalPageCount;
        }
        
        // LIMIT 시작 위치 계산
        limitStart = (params.getPage() - 1) * params.getRecordSize();
        
        // 이전 페이지 존재 여부 확인
        existPrevPage = startPage != 1;
        
        // 다음 페이지 존재 여부 확인
        existNextPage = (endPage * params.getRecordSize()) < totalRecordCount;
    }
    
}

SearchDto.java

package octopus.base.model;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import lombok.Data;

/**
 * <pre>
 * </pre>
 */
@Data
@MappedSuperclass // BaseEntity를 상속한 Entity들은 아래의 필드들을 컬럼으로 인식한다.
@EntityListeners(AuditingEntityListener.class) // Audting(자동으로 값 Mapping) 기능 추가
public class SearchDto {
    
    protected int        page;       // 현재 페이지 번호
    protected int        recordSize; // 페이지당 출력할 데이터 개수
    protected int        pageSize;   // 화면 하단에 출력할 페이지 사이즈
    protected Pagination pagination; // 페이지네이션 정보
    
    public SearchDto() {
        this.page       = 0;
        this.recordSize = 10;
        this.pageSize   = 10;
    }
    
}

PagingListResult.java

package octopus.base.model;

import java.util.ArrayList;
import java.util.List;

import lombok.Getter;

@Getter
public class PagingListResult<T> {
    
    private List<T>    list = new ArrayList<>();
    private Pagination pagination;
    
    public PagingListResult(List<T> list, Pagination pagination) {
        this.list.addAll(list);
        this.pagination = pagination;
    }
    
}

Spring Boot Start 옵션 : Run Configuration >> Spring Boot App >> Arguments Tab > VM arguments > -Dspring.profiles.active=local

목록 화면

페이지 이동


JPA에서 Page 기능을 사용하면서 알게된 내용이 있습니다.

첫번째. page 는 0 부터 시작합니다. ( 화면에서 Parameter 전달 시 page - 1 해야 합니다. )

두번째. 기본적으로 Count 쿼리를 수행합니다.

INFO  23-04-25 14:22:745[http-nio-9090-exec-1] [▶ p6spy.logSQL ◀][60]: - [statement] | 1 ms | 
    select
        tboardm0_.id as id1_0_,
        tboardm0_.crt_dt as crt_dt2_0_,
        tboardm0_.crt_id as crt_id3_0_,
        tboardm0_.mdf_dt as mdf_dt4_0_,
        tboardm0_.mdf_id as mdf_id5_0_,
        tboardm0_.contents as contents6_0_,
        tboardm0_.notice_yn as notice_y7_0_,
        tboardm0_.read_cnt as read_cnt8_0_,
        tboardm0_.title as title9_0_ 
    from
        t_board_m tboardm0_ 
    order by
        tboardm0_.id desc limit 10 offset 10

	Connection ID:7 | Excution Time:1 ms

	Excution Time:1 ms

	Call Stack :

--------------------------------------
INFO  23-04-25 14:22:749[http-nio-9090-exec-1] [▶ p6spy.logSQL ◀][60]: - [statement] | 0 ms | 
    select
        count(tboardm0_.id) as col_0_0_ 
    from
        t_board_m tboardm0_

	Connection ID:7 | Excution Time:0 ms

	Excution Time:0 ms

	Call Stack :​

세번째. Pageable 을 사용하면 Controller 에서 Parameter 로 받을 수 있습니다.

/**
 * # 1 페이지 조회를 요청합니다.
 * http://localhost:9090/bbs/list/page?page=1
 * # 2 페이지, 10개 Record 조회를 요청합니다.
 * http://localhost:9090/bbs/list/page?page=1&size=10
 * # 3 페이지, 10개 Record, id를 DESC 조회를 요청합니다.
 * http://localhost:9090/bbs/list/page?page=1&size=10&sort=id.DESC
 */

    @GetMapping("/list/page")
    public String findAllOfPage(@ModelAttribute("params") final BoardSearchDto params,
            @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
            Model model) {
        // page : 페이지
        // size : record의 갯수
        model.addAttribute("result", boardService.findAllOfPage(params, pageable));
        
        return "post/list";
    }

네번째. 개발자에 의해 Pagination을 정의하기에는 Pageable 보다는 PageReqeust 를 사용하는 것이 좋을 것으로 보여집니다.

    @GetMapping("/list/page2")
    public String findAllOfPage2(@ModelAttribute("params") final BoardSearchDto params,
            Model model) {
        
        PageRequest pageRequest = PageRequest.of(params.getPage(), params.getRecordSize(), Sort.by(Sort.Direction.DESC, "id"));
        
        model.addAttribute("result", boardService.findAllOfPage2(params, pageRequest));
        
        return "post/list";
    }​

참고1. 페이지를 유지하면서 엔티티를 DTO 로 변환하기

Page<TBoardM> page = boardRepository.findAll(pageRequest);
Page<BoardDto> dtoPage = page.map(m -> new BoardDto());​

참고2. Page의 구현체 PageImpl

public Page<BoardDto> findAllOfPage(Pageable pageable) {

    Page<TBoardM> list = boardRepository.findAll(pageable);
    List<BoardDto> boardList = new ArrayList<>();

	BoardDto dto = null;
    for (TBoardM board : list) {
        dto = new BoardDto(board);
        boardList.add(dto);
    }

    return new PageImpl<>(boardList, pageable, list.getTotalElements());
}

REST API URI 명명 규칙에는 맞지 않지만, 일단 테스트라는 명목으로 적당히 무시해서 URI를 정의했습니다.

https://dzone.com/articles/7-rules-for-rest-api-uri-design-1

 

7 Rules for REST API URI Design - DZone

URIs, or Uniform Resource Identifiers, should be designed to be readable and clearly communicate the API resource model. These rules will help you succeed.

dzone.com

https://dkrnfls.tistory.com/218?category=1026591 

 

REST API URI를 설계하는 7가지 규칙

최근 백엔드를 구현하던 중 uri를 규칙없이 작성하는 것 같아 여러글을 찾아보다가 잘 쓰여진 글이 있는 것 같아 가져가고 싶었습니다. 해당 포스트는 아래에 첨부된 사이트를 번역한 글입니다.

dkrnfls.tistory.com


https://congsong.tistory.com/26

 

스프링 부트(Spring Boot) - 페이징(Paging) & 검색(Search) 처리하기 1/2 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBat

본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다. DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기) 화면 처리는 HTML5 기반

congsong.tistory.com

https://hermeslog.tistory.com/704

 

[Spring Boot] 게시판 #1 - 개발환경

게시판을 작성해보려 합니다. 조금씩 살을 붙여나가 보려고 합니다. 개발환경 STS 4.17.2.RELEASE OpenJDK Runtime Environment Zulu11.62+17-CA (build 11.0.18+10-LTS) Spring Boot 2.7.9 lombok devtools postgresql 14.1 Gladle Thymeleaf

hermeslog.tistory.com

https://hermeslog.tistory.com/706

 

[Spring Boot] 게시판 #2 - 간단한 게시판 CRUD 개발

게시판을 작성해보려 합니다. 조금씩 살을 붙여나가 보려고 합니다. https://hermeslog.tistory.com/704 [Spring Boot] 게시판 #1 - 개발환경 게시판을 작성해보려 합니다. 조금씩 살을 붙여나가 보려고 합니다

hermeslog.tistory.com

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

 

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

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

tecoble.techcourse.co.kr

 

728x90