PageableSpring Data가 제공하는 페이징 + 정렬 추상화 인터페이스
: 구현체인 PageReauest를 통해 페이징에 필요한 데이터(시작 페이지, 최대 결과 개수, 정렬 조건)를 전달하면 페이징 결과를 Page타입으로 반환한다.
목표 : 모든 게시글을 조회한 결과를 페이징하여 화면에 나타내기
✅ 스프링 부트, 스프링 데이터 JPA, 타임리프 사용
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
@Id @GeneratedValue
@Column(name = "board_id")
private Long id;
private String title;
@Lob
private String content;
private LocalDateTime createDate;
private LocalDateTime updateDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
}
category), 게시글 제목, 작성자 이름(member), 작성일@Data
public class BoardDTO {
private Long id;
private String categoryName;
private String title;
private String userName;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime createDate;
public BoardDTO(Post post) {
this.id = post.getId();
this.categoryName = post.getCategory().getName();
this.title = post.getTitle();
this.userName = post.getMember().getUserName();
this.createDate = post.getCreateDate();
}
}
RepositorySpring Data JPA 사용 : JpaRepository 상속
findBoard(Pageable) : 페치 조인을 통해 연관관계를 가지는 member와 category를 한 번에 조회 + 파라미터로 전달된 pageable를 데이터를 통해 페이징 수행
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("select p from Post p join fetch p.category c join fetch p.member m")
Page<Post> findBoard(Pageable pageable);
}
ServicefindAll(Pagealbe)
findBoard(Pageable)에 pageable 전달page.map() : findBoard()가 반환한 엔티티를 BoardDTO로 변환하여 컨트롤러에 반환@Service
@RequiredArgsConstructor
@Transactional
public class PostService {
private final PostRepository postRepository;
public Page<BoardDTO> findAll(Pageable pageable) {
Page<Post> page = postRepository.findBoard(pageable);
return page.map(board -> new BoardDTO(board));
}
}
Controller요청의 파라미터로 page, size, sort 등의 정보가 넘어오면 Pageable 타입으로 바인딩 → 해당 데이터를 사용하여 PageRequest 객체 생성
@PageableDefault : 페이징 관련 데이터 기본값 설정
size : 최대 결과 개수 (default = 20)page : 조회 페이지 (default = 0)sort + direction : 단일 정렬 조건@SortDefault.SortDefaults({@SortDefault(sort=,direction), ...}) : 다중 정렬 조건
뷰 페이지에 page, pm (하단부 페이징 관련 데이터) 전달
@Controller
@RequestMapping("/boards")
@RequiredArgsConstructor
public class BoardController {
private final PostService postService;
@GetMapping
public String postList(@PageableDefault(size = 2, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
Model model) {
Page<BoardDTO> page = postService.findAll(pageable);
PageMaker pm = new PageMaker(page);
model.addAttribute("pm", pm);
model.addAttribute("page", page);
return "board/postList";
}
}
: /boards 요청 시 실행되는 쿼리 (게시글 전체 조회 + 카운트 쿼리)
PageMaker컨트롤러에서 전달받은 Page를 사용하여 화면의 하단 페이징 영역 관련 데이터 (계산식은 이전 프로젝트에서 사용한 공식 사용..)
page.getNumber() : 현재 요청한 페이지 → 0부터 시작하므로, 1 더한 값 출력page.getTotalPages() : 페이지의 총 개수 @Getter
public class PageMaker {
private int nowPage; // 현재 페이지
private int startPage; // 현재 페이지 블럭 내의 첫번째 페이지
private int endPage; // 현재 페이지 블럭 내의 마지막 페이지
private boolean prev; // 이전 페이지가 있는가?
private boolean next; // 다음 페이지가 있는가?
private int block = 2; // 한 번에 출력할 페이지 개수
public PageMaker(Page<?> page) {
nowPage = page.getNumber() + 1;
endPage = (int)(Math.ceil((double)(nowPage)/block)) * block;
startPage = endPage - block + 1;
endPage = Math.min(endPage, page.getTotalPages());
prev = startPage != 1;
next = ((long)endPage < page.getTotalPages());
}
}
Previous : 해당 페이지 블럭의 prev가 true인 경우에만 출력
/boards?page=0endPage (‹) : /boards?page=startPage-2 페이지 블럭 : startPage ~ endPage 출력 → 출력된 p의 값은 page+1이므로, 클릭 시 /boards?page=p-1로 이동
Next : 해당 페이지 블럭의 next가 true이면서 endPage가 0 이상인 경우에만 출력
startPage (›) : /boards?page=endPageendPage (») : /boards?page=getTotalPages()-1<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center pagination-sm">
<!-- 첫 페이지로 이동 -->
<li class="page-item" th:if="${pm.prev}">
<a class="page-link" th:href="@{/boards(page=0)}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<!-- 첫 페이지 이동 -->
<!-- 이전 페이지 블럭으로 이동 -->
<li class="page-item" th:if="${pm.prev}">
<a class="page-link" th:href="@{/boards(page=${pm.startPage}-2)}" aria-label="Previous">
<span aria-hidden="true">‹</span>
</a>
</li>
<!-- 이전 페이지 블럭으로 이동 -->
<!-- 페이지 블럭 -->
<li th:class="page-item" th:each="p : ${#numbers.sequence(pm.startPage, pm.getEndPage())}"
th:classappend="(${pm.nowPage == p})? 'active' : ''">
<a class="page-link" th:href="@{/boards(page=${p}-1)}"
th:text="${p}"></a>
</li>
<!-- 페이지 블럭 -->
<!-- 다음 페이지 블럭으로 이동 -->
<li class="page-item" th:if="${pm.next && pm.endPage > 0}">
<a class="page-link" th:href="@{/boards(page=${pm.endPage})}" aria-label="Next">
<span aria-hidden="true">›</span>
</a>
</li>
<!-- 다음 페이지 블럭으로 이동 -->
<!-- 마지막 페이지로 이동 -->
<li class="page-item" th:if="${pm.next && pm.endPage > 0}">
<a class="page-link" th:href="@{/boards(page=${page.getTotalPages()}-1)}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
<!-- 마지막 페이지로 이동 -->
</ul>
</nav>
ex0) 테스트 데이터 
ex1) block = 2, size = 2 
ex2) block = 5, size = 2

ex3) block = 5, size = 5

➕ 일단은 Page 타입으로 받아서 일반 게시판 형태로 구현해봤는데, 나중에 시간이 되면 Slice 타입으로 받아서 더보기 형식으로 구현해봐야겠다!