20 스프링부트(블로그만들기) 페이징 V3 3.9

윤주헌's avatar
Sep 23, 2024
20 스프링부트(블로그만들기) 페이징
V3 3.9
  1. Spring Data JPA의 PagingAndSortingRepository 사용 PagingAndSortingRepository는 기본적으로 페이징과 정렬 기능을 지원하는 인터페이스입니다. Pageable 객체를 사용해 간단하게 페이징을 처리할 수 있습니다.
코드 예시
public interface UserRepository extends PagingAndSortingRepository<User, Long> { Page<User> findAll(Pageable pageable); }
Pageable pageable = PageRequest.of(0, 10); // 페이지 번호 0, 페이지 당 10개 데이터 Page<User> page = userRepository.findAll(pageable); List<User> users = page.getContent(); // 페이징된 데이터
  1. Spring Data JPA의 JpaRepository 사용 JpaRepository는 더 많은 기능을 제공하는 인터페이스로, 페이징을 포함한 여러 작업을 간편하게 할 수 있습니다. Page나 Slice를 반환하여 페이징된 데이터를 가져올 수 있습니다.
코드 예시
public interface UserRepository extends JpaRepository<User, Long> { Page<User> findByName(String name, Pageable pageable); }
사용 예:
Pageable pageable = PageRequest.of(0, 10); Page<User> page = userRepository.findByName("John", pageable); List<User> users = page.getContent(); Page: 전체 페이지 수, 현재 페이지 번호, 총 데이터 수 등을 제공 Slice: 전체 페이지 수나 총 데이터 수 없이 다음 페이지가 있는지 여부만 제공
  1. JPQL과 EntityManager를 사용한 페이징 처리 직접 JPQL 쿼리를 사용해 페이징 처리를 할 수 있습니다. EntityManager의 createQuery() 메서드를 사용하여 페이징 쿼리를 실행합니다.
String jpql = "SELECT u FROM User u WHERE u.name = :name"; TypedQuery<User> query = entityManager.createQuery(jpql, User.class); query.setParameter("name", "John"); query.setFirstResult(0); // 시작 인덱스 (0부터 시작) query.setMaxResults(10); // 한 번에 가져올 데이터 수 List<User> users = query.getResultList();
  1. Native Query를 사용한 페이징 처리 네이티브 SQL 쿼리를 통해서도 페이징을 구현할 수 있습니다. 이 경우 SQL 문법을 그대로 사용할 수 있습니다.
코드 예시
@Query(value = "SELECT * FROM users WHERE name = :name LIMIT :limit OFFSET :offset", nativeQuery = true) List<User> findUsersByNameWithPaging(@Param("name") String name, @Param("limit") int limit, @Param("offset") int offset);
사용 예
List<User> users = userRepository.findUsersByNameWithPaging("John", 10, 0);
 
 
notion image
 
 
JPQL로 페이징
보드레파지토리
//페이징jpql로 @Query("select b from Board b where b.title like %:title% order by b.id desc") Page<Board> mFindAll(@Param("title") String title, Pageable pageable);
 
 
findAll도 가지고 있다
 
보드레파지토리 테스트 이동
@Test public void mFindAll_test(){ //given String title = "제목"; //when Pageable pageable = PageRequest.of(0,3); Page<Board> boardPG = boardRepository.mFindAll(title, pageable); }
 
이까지 우리가 짠 것
notion image
  • 알아서 해준 것
 
pageable때문에 한 것
notion image
 
페이지 계산해주려고
notion image
 
 
전체
notion image
 
 
boardPG 안 내용이 궁금하잖아 이 때는 가장쉬운방법이 json으로 만들어 뿌려보는것!
 
예가 java을 json으로 json을 java로 바꿔주는 애 @Requestbody할 때 알아서 예가 발동 하나더 있었는데 뭐였지
ObjectMapper om = new ObjectMapper();
 
@Test public void mFindAll_test() throws JsonProcessingException { //given String title = "제목"; //when Pageable pageable = PageRequest.of(0,3); Page<Board> boardPG = boardRepository.mFindAll(title, pageable); //eye ObjectMapper om = new ObjectMapper(); //쓰로우 해주기 String responseBody = om.writeValueAsString(boardPG); //om.readValue(responseBody, (타입 /클래스) Board.class); 이게 뭐하는 거지? }
이러면 터짐
 
새로운 쿼리 발동
유저를 조회하는 쿼리 발생
notion image

ByteBuddy 오류 잘 기억하자!!!!

notion image
왜 터졌을까?
비어있는것을 getter때렸는데 lazy로딩걸리는데 받기 전에 json으로 보내려고 하는 것!!
select하기 전 이기때문 id만 가지고 있고 데이터는 가지고 있지 않아서
 
→ try catch로 잡을 수 있다 아니면 애초에 dto로 옮겨버린 후 json으로 컨버팅하면 이런 일 없다
 
notion image
친절하게 알려준다
비어있는 객체때문에 실패하는 것을 disable해라!
 

프로퍼티에 넣어주자(해결)

spring.jackson.serialization.fail-on-empty-beans=false
 
오류발생
하이버네이트 초기화발생 @JsonIgnore
이거 보드에 reply, user위에 넣으니까 해결됨
 
→완전한 해결방법 DTO로 바꾸면 된다
package org.example.springv3.board; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.example.springv3.reply.Reply; import org.example.springv3.user.User; import org.hibernate.annotations.CreationTimestamp; import java.sql.Timestamp; import java.util.List; @NoArgsConstructor // 빈 생성자 (하이버네이트가 om 할때 필요) @Setter @Getter @Table(name = "board_tb") @Entity // DB에서 조회하면 자동 매핑이됨 public class Board { @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_incremnt 설정, 시퀀스 설정 @Id // PK 설정 private Integer id; @Column(nullable = false) private String title; @Column(nullable = false) private String content; @CreationTimestamp private Timestamp createdAt; // fk //@JsonIgnore @ManyToOne(fetch = FetchType.LAZY) private User user; // @JsonManagedReference //@JsonIgnore //@JsonIgnoreProperties({"board", "createdAt"}) properties는 내부의 json을 무시하는 것 @OneToMany(mappedBy = "board") //게시글 조회할 때 댓글여러개 필요해서 적어준 것이다, 포린키의 주인이 누군지 알려줘야 한다! 나는 아니야(안적으면 컬럼 생성한다) private List<Reply> replies; @Builder public Board(Integer id, String title, String content, Timestamp createdAt, User user) { this.id = id; this.title = title; this.content = content; this.createdAt = createdAt; this.user = user; } }
 
눈으로 보기 위해 사용하는 거고 다 테스트 하고 나면은 @JsonIgnore 지우기!!
{"content":[{"id":5,"title":"제목5","content":"내용5","createdAt":1726102168605},{"id":4,"title":"제목4","content":"내용4","createdAt":1726102168605},{"id":3,"title":"제목3","content":"내용3","createdAt":1726102168605}],"pageable":{"pageNumber":0,"pageSize":3,"sort":{"empty":true,"unsorted":true,"sorted":false},"offset":0,"unpaged":false,"paged":true},"totalElements":5,"totalPages":2,"last":false,"size":3,"number":0,"sort":{"empty":true,"unsorted":true,"sorted":false},"first":true,"numberOfElements":3,"empty":false}
이 값을 여기 사이트 text에넣고 view를 눌러보자 이쁘게 나온다!
notion image
 
number이 현제 페이지
번호 붙이고 싶으면 totalPages만큼 for문돌리면 된다!!
 
페이지에 보드 타입이라 .getContent하면 안의 정보 다 있다 이제는 List가 아니니까
 
투 스트링을 유저, 보드, 리플라이에도 넣는다
보드 객체 꺼내서 getContent해서 syout하면 어떻게 될까? 터진다!
왜 터질까?
stachOverFlower로 메모리 터짐
계속 타고 타고 타고 들어가서 터진다! toString으로 무한 참조해서 터진다

테스트 코드

@Test public void mFindAllV2_test() throws JsonProcessingException { // given String title = "제목"; // when Pageable pageable = PageRequest.of(0, 3); Page<Board> boardPG = boardRepository.mFindAll(title, pageable); // eye System.out.println(boardPG.getContent()); }
 
notion image
해결하려면
→ reply에서 보드 넣는것 toString만 지우면 끝난다!
notion image
 
@DATA안에 getter, setter, toString 등 여러가지가 자동호출돼서 초보자 때는 잘 쓰지않는다 제어할 수 없으니
 

리팩토링 보드서비스 게시글 목록보기

원래 코드
public List<Board> 게시글목록보기(String title) { //전체 결과 if(title == null){ //Pageable pg = PageRequest.of(0, 3, Sort.Direction.DESC, "id"); // 게시글 순서 거꾸로 만드려고 Sort sort = Sort.by(Sort.Direction.DESC, "id"); List<Board> boardList = boardRepository.findAll(sort); return boardList; // 검색된 결과 }else { List<Board> boardList = boardRepository.mFindAll(title); return boardList; } }
변한 것
public Page<Board> 게시글목록보기(String title, int page) { //전체 결과 Pageable pageable = PageRequest.of(page*3, 3, Sort.Direction.DESC, "id"); if(title == null){ // 게시글 순서 거꾸로 만드려고 Sort sort = Sort.by(Sort.Direction.DESC, "id"); Page<Board> boardList = boardRepository.findAll(pageable); return boardList; // 검색된 결과 }else { Page<Board> boardList = boardRepository.mFindAll(title, pageable); return boardList; } }
 
DTO로 해주는게 좋다 하지만 여기서는 직접 머스테치에 선별에서 줄꺼니까 안터짐

보드 컨트롤러 이동

//localhost:8080?title=제목 이러면? requestParam 생략 가능 // 이유는 적어주면 requestParam에서(defaultValue ="", name="title") 이렇게 쓸 수 있다 쿼리 안에 title이 없으면 터지는데 (파싱 못하니까) 그래서 공백으로 넣어라 할 수 있다.! @GetMapping("/") public String list(@RequestParam(value = "title", required = false) String title, @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, HttpServletRequest request) { //쿼리스트링으로 page넣을 거임 // 디폴트 값 넣을 수 있다. /* if(page == null) { page = 0; } 이렇게 넘길 수 있다. /호출하면 넣어줄 수 있다. defaultValue는 쿼리스트링으로 받는 거라서 숫자 0으로는 안됨 문자열 0으로 해야함 그래서 defaultValue하면 if 위에거 사용 안해도 됨 */ Page<Board> boardPG = boardService.게시글목록보기(title,page); //가방에 담고 list에 있는 것만 꺼낼꺼니까 DTO안만들어도 됨 request.setAttribute("model", boardList); return "board/list"; }
이제 객체니까 models가 아닌 model로 바꿔야 한다
boardList를 boardPG로 명확하게 바꿔주자

list 이동

왜 model.content인가 content안에 내용들 다 들어가 있으니까 아까 봤잖아 json으로
 
원래는 json으로 값 넣으면 너무 많은 정보들이 있어서 다 넣으니까 터져서 DTO를 만들어야하는데
머스테치에서는 내가 원하는 값만 넣을거여서 터지지 않는다!
 
모델.content아래 넣어주기
<ul class="pagination d-flex justify-content-center"> <li class="page-item disabled"><a class="page-link" href="#">Previous</a></li> <li class="page-item"><a class="page-link" href="#">Next</a></li> </ul>
위치
notion image
결과
notion image
 
 

게시글 목록보기

*3 없애기
 

list로

disable지워야 한다 연습하려면
<h1>{{model.number}}</h1> <ul class="pagination d-flex justify-content-center"> <li class="page-item "><a class="page-link" href="?page=0">Previous</a></li> <li class="page-item"><a class="page-link" href="?page={{model.number+1}}">Next</a></li> </ul>
model.number하면 현 번호가 있는데
+1 -1 이렇게 하면 안됨! 머스테치에서는 연산이 불가해서
 

보드 컨트롤러

@GetMapping("/") public String list(@RequestParam(value = "title", required = false) String title, @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, HttpServletRequest request) { //쿼리스트링으로 page넣을 거임 // 디폴트 값 넣을 수 있다. /* if(page == null) { page = 0; } 이렇게 넘길 수 있다. /호출하면 넣어줄 수 있다. defaultValue는 쿼리스트링으로 받는 거라서 숫자 0으로는 안됨 문자열 0으로 해야함 그래서 defaultValue하면 if 위에거 사용 안해도 됨 */ Page<Board> boardPG = boardService.게시글목록보기(title,page); //가방에 담고 list에 있는 것만 꺼낼꺼니까 DTO안만들어도 됨 request.setAttribute("model", boardPG); request.setAttribute("prev", boardPG.getNumber()-1); request.setAttribute("next", boardPG.getNumber()+1); return "board/list"; }
그래서 컨트롤러에서 넣어주면 되기는 한다
하지만 서비스에서 DTO를 만들어서 해줘야 한다
 

list

<h1>{{model.number}}</h1> <ul class="pagination d-flex justify-content-center"> <li class="page-item "><a class="page-link" href="?page={{prev}}">Previous</a></li> <li class="page-item"><a class="page-link" href="?page={{next}}">Next</a></li> </ul>

숙제

검색은 일단 버리고
보드컨트롤러의
request.setAttribute("model", boardPG); request.setAttribute("prev", boardPG.getNumber()-1); request.setAttribute("next", boardPG.getNumber()+1);
게시글목록보기에서 한번에 만들어라
 
하나의 객체로 DTO로 만들어서 한번에 응답 받아라
즉 게시글 목록보기DTO만들어라
 
이런게 뭔데? 많은 데이터들이 있는데 머스테치에서 뽑아써서?
이런거는 서버사이드랜더링에서는 동작하지만
resAPI로 넘어가면 문제가 된다!

숙제 내가 만들어본 것 DTO만들기

 
BoardResponse
package org.example.springv3.board; import lombok.Data; import org.example.springv3.reply.Reply; import org.example.springv3.user.User; import org.springframework.data.domain.Page; import java.util.ArrayList; import java.util.List; public class BoardResponse { @Data public static class DetailDTO { private Integer id; private String title; private String content; private Boolean isOwner; // private Integer userId; private String username; //리플라이 엔티티 집이 넣으면 안된다 레이지 로딩 되니까! 똑같이 생긴 DTO만들면 된다 비영속객체 만들어서 응답하게 하는게 좋다! private List<ReplyDTO> replies = new ArrayList<>(); public DetailDTO(Board board, User sessionUser) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isOwner = false; if (sessionUser != null) { if (board.getUser().getId() == sessionUser.getId()) { isOwner = true; // 권한체크 } } // this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); for(Reply reply : board.getReplies()) { replies.add(new ReplyDTO(reply,sessionUser)); } } } @Data public static class ReplyDTO{ private Integer id; private String comment; private String username; private Boolean isOwner; public ReplyDTO(Reply reply, User sessionUser) { this.id = reply.getId(); this.comment = reply.getComment(); this.username = reply.getUser().getUsername(); this.isOwner = false; if (sessionUser != null) { if (reply.getUser().getId() == sessionUser.getId()) { isOwner = true; // 권한체크 } } } } @Data public static class DTO { private Integer id; private String title; private String content; public DTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); } } @Data public static class PageDTO { private Integer number; // 현재페이지 private Integer totalPage; // 전체페이지 개수 private Integer size; // 한페이지에 아이템 개수 private Boolean first; private Boolean last; private Integer prev; // 현재페이지 -1 private Integer next; // 현재페이지 +1 private List<Content> contents = new ArrayList<>(); public PageDTO(Page<Board> boardPage) { this.number = boardPage.getNumber(); this.totalPage = boardPage.getTotalPages(); this.size = boardPage.getSize(); this.first = boardPage.isFirst(); this.last = boardPage.isLast(); this.prev = boardPage.getNumber()-1; this.next = boardPage.getNumber()+1; //for로 id title만 있으면 확인이 가능하다 //어디서 값을 가지고 와야 하지? for(Board board : boardPage.getContent()) { contents.add(new Content(board)); } } //생성자를 만들어서 @Data class Content { private Integer id; private String title; public Content(Board board) { this.id = board.getId(); this.title = board.getTitle(); } } } }
 
boardService
public BoardResponse.PageDTO 게시글목록보기(String title, int page) { //전체 결과 Pageable pageable = PageRequest.of(page, 3, Sort.Direction.DESC, "id"); Page<Board> boardList; if (title == null) { // 게시글 순서 거꾸로 만드려고 Sort sort = Sort.by(Sort.Direction.DESC, "id"); boardList = boardRepository.findAll(pageable); // 검색된 결과 } else { boardList = boardRepository.mFindAll(title, pageable); } return new BoardResponse.PageDTO(boardList); }
BoardController
@GetMapping("/") public String list(@RequestParam(value = "title", required = false) String title, @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, HttpServletRequest request) { BoardResponse.PageDTO boardPG = boardService.게시글목록보기(title,page); request.setAttribute("model", boardPG); return "board/list"; }
List.mustache
{{>layout/header}} <form action="/text/form" method="post"> <input type="text" name="username"> <button>고고</button> </form> <div class="container p-5"> <div class="d-flex justify-content-end mb-2"> <form action="/" method="get" class="d-flex col-md-3"> <input class="form-control me-2" type="text" placeholder="Search" name="title"> <button class="btn btn-primary">Search</button> </form> </div> {{#model.contents}} <div class="card mb-3"> <div class="card-body"> <h4 class="card-title mb-3">{{title}}</h4> <a href="/board/{{id}}" class="btn btn-primary">상세보기</a> </div> </div> {{/model.contents}} <ul class="pagination d-flex justify-content-center"> <li class="page-item "><a class="page-link" href="?page={{model.prev}}">Previous</a></li> <li class="page-item"><a class="page-link" href="?page={{model.next}}">Next</a></li> </ul> </div> {{>layout/footer}}
 
 

강사님 페이징

 
BoardResponse
package org.example.springv3.board; import lombok.Data; import org.example.springv3.reply.Reply; import org.example.springv3.user.User; import org.springframework.data.domain.Page; import java.util.ArrayList; import java.util.List; public class BoardResponse { @Data public static class DetailDTO { private Integer id; private String title; private String content; private Boolean isOwner; // private Integer userId; private String username; //리플라이 엔티티 집이 넣으면 안된다 레이지 로딩 되니까! 똑같이 생긴 DTO만들면 된다 비영속객체 만들어서 응답하게 하는게 좋다! private List<ReplyDTO> replies = new ArrayList<>(); public DetailDTO(Board board, User sessionUser) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isOwner = false; if (sessionUser != null) { if (board.getUser().getId() == sessionUser.getId()) { isOwner = true; // 권한체크 } } // this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); for(Reply reply : board.getReplies()) { replies.add(new ReplyDTO(reply,sessionUser)); } } } @Data public static class ReplyDTO{ private Integer id; private String comment; private String username; private Boolean isOwner; public ReplyDTO(Reply reply, User sessionUser) { this.id = reply.getId(); this.comment = reply.getComment(); this.username = reply.getUser().getUsername(); this.isOwner = false; if (sessionUser != null) { if (reply.getUser().getId() == sessionUser.getId()) { isOwner = true; // 권한체크 } } } } @Data public static class DTO { private Integer id; private String title; private String content; public DTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); } } @Data public static class PageDTO { private Integer number; // 현재페이지 private Integer totalPage; // 전체페이지 개수 private Integer size; // 한페이지에 아이템 개수 private Boolean first; private Boolean last; private Integer prev; // 현재페이지 -1 private Integer next; // 현재페이지 +1 private List<Content> contents = new ArrayList<>(); private List<Integer> numbers = new ArrayList<>(); public PageDTO(Page<Board> boardPage) { this.number = boardPage.getNumber(); this.totalPage = boardPage.getTotalPages(); this.size = boardPage.getSize(); this.first = boardPage.isFirst(); this.last = boardPage.isLast(); this.prev = boardPage.getNumber()-1; this.next = boardPage.getNumber()+1; int temp = (number / 3)*3; // 0-> 0, 3->3, 6->6 for(int i = temp; i<temp+2; i++){ this.numbers.add(i); } //for로 id title만 있으면 확인이 가능하다 //어디서 값을 가지고 와야 하지? for(Board board : boardPage.getContent()) { contents.add(new Content(board)); } } //생성자를 만들어서 @Data class Content { private Integer id; private String title; public Content(Board board) { this.id = board.getId(); this.title = board.getTitle(); } } } }
List.mustache
{{#model.contents}} <div class="card mb-3"> <div class="card-body"> <h4 class="card-title mb-3">{{title}}</h4> <a href="/board/{{id}}" class="btn btn-primary">상세보기</a> </div> </div> {{/model.contents}} <ul class="pagination d-flex justify-content-center"> <li class="page-item "><a class="page-link" href="?page={{model.prev}}">Previous</a></li> {{#model.numbers}} <li class="page-item"><a class="page-link" href="?page={{.}}">{{.}}</a></li> {{/model.numbers}} <li class="page-item"><a class="page-link" href="?page={{model.next}}">Next</a></li> </ul>
변경 사항은 이 두개 말고는 없다
Share article

code-sudal