본문 바로가기
Dev/SpringBoot

JPA paging 처리

by 컴포넌트설계자 2026. 6. 8.

Controller
  └── PageRequest.of(nowPage-1, PAGE_COUNT, Sort.DESC, "bno")  ← Pageable 생성
        └── freeBoardService.selectAll(pageable)
              └── Repository의 join05(pageable)  ← 여기서 페이징 실행
                    └── Page<FreeBoard> 반환

페이징 5개로 처리 
기존 step03_UserBoardReply_ver2 프로젝트 참고
ReplyRepository.java

package web.mvc.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import web.mvc.domain.FreeBoard;

import java.util.List;
import java.util.Optional;

public interface FreeBoardRepository extends JpaRepository<FreeBoard, Long> ,
        QuerydslPredicateExecutor<FreeBoard> {


    /**
     * 연관관계가 있을때  - findAll() 메소드를 사용하면
     * 부모글을 기준으로 댓글정보 가져오면(LAZY 전략인경우)
     * 부모글의 개수만큼 댓글의 select를 계속 실행된다. - 성능이슈가 있다.
     *
     * 그래서, JPQL문법, QueryDSL를 이용해서 직접 fetch조인을 사용한다.
     * */
    @Query("select f from FreeBoard f left join  f.repliesList")
    List<FreeBoard> join00();
//
    @Query("select f from FreeBoard f left join fetch f.repliesList")
    List<FreeBoard> join01();
//
//    /// /fetch join을 EntityGraph///////////////////////////////////
//    /**
//     *  1) attributePaths = "repliesList": FreeBoard.repliesList를 함께 로딩
//     *  2) type:
//     *         - FETCH: 지정한 속성은 즉시 로딩, 나머지는 지연(default 무시)
//     *         -LOAD(자주 씀): 지정한 속성만 즉시 로딩으로 “추가” (기본 페치전략 존중)
//     * */
    //EntityGraph는 JPA가 제공하는 어노테이션 기반의 즉시 로딩 설정 방법
    //@EntityGraph(attributePaths = "repliesList", type = EntityGraph.EntityGraphType.LOAD)
    @EntityGraph(attributePaths = "repliesList")
    @Query("select f from FreeBoard f")
    List<FreeBoard> join02();
//
//
    @Query("select f from FreeBoard f left join fetch f.repliesList")
    Page<FreeBoard> join04(Pageable page);


    @Query(value = "select distinct f from FreeBoard f  left join fetch f.repliesList",
            countQuery = "select count(distinct f.bno) from FreeBoard f left join  f.repliesList" )
    Page<FreeBoard> join05(Pageable page);
//
//
//    /// ////////////////////////////////////
//
//    /**
//     * 상세보기(부모글 + 댓글정보)
//     * */
    @Query("select f from FreeBoard f left join fetch f.repliesList where f.bno=?1")
    Optional<FreeBoard> selectByBnoJoin(Long bno);


    //개인테스트
//    @Query("select f from FreeBoard f left join f.repliesList")
//    List<FreeBoard> join02();
//
//    @Query("select f from FreeBoard f left join fetch f.repliesList")
//    List<FreeBoard> join01();

}

properties에 5개씩 보이게 추가

PAGE_COUNT=5

*UserRepository

package web.mvc.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import web.mvc.domain.User;


public interface UserRepository extends
        JpaRepository<User, String>, QuerydslPredicateExecutor<User> {


}


*ReplyRepository

package web.mvc.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import web.mvc.domain.Reply;

public interface ReplyRepository extends JpaRepository<Reply, Long>
        , QuerydslPredicateExecutor<Reply> {
}

JPA 페이징 처리 핵심 정리


1. Pageable — 페이징 조건 설정

 
 
java
Pageable pageable = PageRequest.of((nowPage-1), PAGE_COUNT, Sort.Direction.DESC, "bno");
  • nowPage-1 : JPA는 페이지 번호가 0부터 시작해서 -1
  • PAGE_COUNT : 한 페이지에 보여줄 게시글 수 (여기선 5)
  • Sort.DESC, "bno" : bno 기준 내림차순 정렬

2. Repository — 페이징 쿼리

 
 
java
@Query(value = "select distinct f from FreeBoard f left join fetch f.repliesList",
       countQuery = "select count(distinct f.bno) from FreeBoard f left join f.repliesList")
Page<FreeBoard> join05(Pageable page);
  • Pageable을 파라미터로 받으면 JPA가 자동으로 LIMIT, OFFSET 붙여줌
  • countQuery : 전체 페이지 수 계산용 별도 쿼리
  • distinct : 댓글 JOIN 시 중복 행 제거

3. Page<T> — 결과 객체

 
 
java
Page<FreeBoard> pageList = freeBoardService.selectAll(pageable);

pageList 안에 모든 페이징 정보가 담겨있음:

메서드내용
getContent() 현재 페이지 게시글 목록
getTotalPages() 전체 페이지 수
getTotalElements() 전체 게시글 수
getNumber() 현재 페이지 번호 (0부터)
isFirst() / isLast() 첫/마지막 페이지 여부

4. View로 전달

 
 
java
model.addAttribute("pageList", pageList);   // 페이징 정보
model.addAttribute("freeList", freeBoardDTOList); // 게시글 목록
model.addAttribute("startPage", startPage); // 페이지 블록 시작번호
model.addAttribute("nowPage", nowPage);     // 현재 페이지
model.addAttribute("blockCount", BLOCK_COUNT); // 블록당 페이지 수

JSP에서 [1][2][3][4] 같은 페이지 버튼을 만들 때 이 값들을 활용하는 구조

기존 내 springboot에서 변경하기

Boardservice

package web.mvc.service;



import web.mvc.domain.Board;
import web.mvc.dto.BoardReq;
import web.mvc.dto.BoardRes;
import web.mvc.exception.BoardSearchNotException;
import web.mvc.exception.DMLException;

import org.springframework.data.domain.Page;

public interface BoardService {
    Board findBoard(Long id) throws BoardSearchNotException;

    List<BoardRes> findAllBoard()throws BoardSearchNotException ;
     Board addBoard(BoardReq board);

    Board updateBoard(Long id,BoardReq board)throws DMLException; ;

     String deleteBoard(Long id);
}

  Page<BoardRes> findAllBoard(int page, int size) throws BoardSearchNotException ;
     Board addBoard(BoardReq board); 추가

package web.mvc.service;



import web.mvc.domain.Board;
import web.mvc.dto.BoardReq;
import web.mvc.dto.BoardRes;
import web.mvc.exception.BoardSearchNotException;
import web.mvc.exception.DMLException;

import org.springframework.data.domain.Page;

public interface BoardService {
    Board findBoard(Long id) throws BoardSearchNotException;

    Page<BoardRes> findAllBoard(int page, int size) throws BoardSearchNotException ;
     Board addBoard(BoardReq board);

    Board updateBoard(Long id,BoardReq board)throws DMLException; ;

     String deleteBoard(Long id);
}

ServiceImpl변경
기존

	@Transactional(readOnly = true)
	//public List<Board> findAllBoard() throws BoardSearchNotException {
	public List<BoardRes> findAllBoard() throws BoardSearchNotException {
		//List<Board> list = boardRepository.findAll(); //N+1문제가 있다(Board 와 Member연관관계)
		List<Board> list = boardRepository.join();

		//Lis<Board>
		System.out.println("--------------------------------------------");
		if(list ==null || list.isEmpty())
			throw new BoardSearchNotException(ErrorCode.NOTFOUND_NO);

		return list.stream()
				.map(BoardRes::new) //Board --> BoardRes  변환
				.collect(Collectors.toList()); //최종 List<BoardRes> 변환
	}

변경  후

	@Transactional(readOnly = true)
	public Page<BoardRes> findAllBoard(int page, int size) throws BoardSearchNotException {
		Pageable pageable = PageRequest.of(page, size);
		// use repository's pageable join to avoid N+1
		Page<Board> boardPage = boardRepository.join(pageable);

		System.out.println("--------------------------------------------");
		if(boardPage == null || boardPage.isEmpty())
			throw new BoardSearchNotException(ErrorCode.NOTFOUND_NO);

		return boardPage.map(BoardRes::new); // Page<BoardRes>
	}

Controller 기존

	@GetMapping("/boards")
	public ResponseEntity<?> findAll(){
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

변경 후 

@GetMapping("/boards")
	public ResponseEntity<?> findAll(@RequestParam(required = false, defaultValue = "0") int page,
	                                 @RequestParam(required = false, defaultValue = "5") int size){
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();



'Dev > SpringBoot' 카테고리의 다른 글

[SpringSecurity] 실습#2  (0) 2026.05.27
[SpringSecurity] 실습#1  (0) 2026.05.27
JSP/Servlet의 3가지 Scope  (0) 2026.05.26