Pageable
Spring 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();
}
}
Repository
Spring 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);
}
Service
findAll(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=0
endPage
(‹) : /boards?page=startPage-2
페이지 블럭 : startPage ~ endPage
출력 → 출력된 p
의 값은 page+1
이므로, 클릭 시 /boards?page=p-1
로 이동
Next
: 해당 페이지 블럭의 next
가 true
이면서 endPage
가 0 이상인 경우에만 출력
startPage
(›) : /boards?page=endPage
endPage
(») : /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
타입으로 받아서 더보기 형식으로 구현해봐야겠다!