inblog logo
|
code-sudal
    blog

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

    윤주헌's avatar
    윤주헌
    Sep 23, 2024
    20 스프링부트(블로그만들기) 페이징
V3 3.9
    Contents
    ByteBuddy 오류 잘 기억하자!!!!프로퍼티에 넣어주자(해결)리팩토링 보드서비스 게시글 목록보기보드 컨트롤러 이동list 이동게시글 목록보기list로보드 컨트롤러list 숙제숙제 내가 만들어본 것 DTO만들기강사님 페이징
    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를 눌러보자 이쁘게 나온다!
    Online JSON Viewer and Formatter
    JSON Viewer and Formatter - Convert JSON Strings to a Friendly Readable Format
    Online JSON Viewer and Formatter
    https://jsonviewer.stack.hu/
    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

    RSS·Powered by Inblog