[JPA] 게시판 : Pageable 인터페이스를 활용한 페이징

진예·2024년 2월 6일
0

Code

목록 보기
2/3
post-thumbnail

💡 Pageable

Spring Data가 제공하는 페이징 + 정렬 추상화 인터페이스

: 구현체인 PageReauest를 통해 페이징에 필요한 데이터(시작 페이지, 최대 결과 개수, 정렬 조건)를 전달하면 페이징 결과Page타입으로 반환한다.


📒 게시판 페이징

목표 : 모든 게시글을 조회한 결과를 페이징하여 화면에 나타내기

✅ 스프링 부트, 스프링 데이터 JPA, 타임리프 사용


1. Entity

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

2. BoardDTO

  • 화면에 출력할 정보 : 게시글 번호, 카테고리명(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();
    }
}

2. Repository

  • Spring Data JPA 사용 : JpaRepository 상속

  • findBoard(Pageable) : 페치 조인을 통해 연관관계를 가지는 membercategory를 한 번에 조회 + 파라미터로 전달된 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);
}

3. 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));
    }
}

4. 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 요청 시 실행되는 쿼리 (게시글 전체 조회 + 카운트 쿼리)

5. 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());
    }
}

6. HTML : 하단부 페이징

  • Previous : 해당 페이지 블럭의 prevtrue인 경우에만 출력

    • 페이지 («) : /boards?page=0
    • 이전 페이지 블럭endPage (‹) : /boards?page=startPage-2
  • 페이지 블럭 : startPage ~ endPage 출력 → 출력된 p의 값은 page+1이므로, 클릭/boards?page=p-1로 이동

  • Next : 해당 페이지 블럭의 nexttrue이면서 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">&laquo;</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">&lsaquo;</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">&rsaquo;</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">&raquo;</span>
		</a>
	</li>
    <!-- 마지막 페이지로 이동 --> 
      
    </ul>
</nav>

📝 시연

ex0) 테스트 데이터

ex1) block = 2, size = 2

ex2) block = 5, size = 2

ex3) block = 5, size = 5


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

profile
백엔드 개발자👩🏻‍💻가 되고 싶다

0개의 댓글