
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 |