[TIL] 23.05.29 Spring Data Jpa(2) 복습 page & sort 처리

hyewon jeong·2023년 5월 29일
0

TIL

목록 보기
126/138

1 . 순수 Jpa 경우 페이징 처리

JPA 페이징 리포지토리 코드

public List<Member> findByPage(int age, int offset, int limit) {
        return em.createQuery("select m from Member m where m.age = :age order by
    m.username desc")
                .setParameter("age", age)
                .setFirstResult(offset)
.setMaxResults(limit)
.getResultList();

//전체 게시물 수를 알아야 추후 페이지 번호를 화면에 나타낼 수 있음 
//하지만 게시물의 수가 많아지면 totalCount로 인한 성능저하가 발생할 수 있다. 고려해야함. 
public long totalCount(int age) {
    return em.createQuery("select count(m) from Member m where m.age = :age",
Long.class)
}
.setParameter("age", age)
.getSingleResult();

2. Spring Data Jpa 페이징 , 정렬 처리

2-1. 페이징과 정렬 파라미터

  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
    Jpa가 아닌 org.springframework.data 의 기능으로 common 공통이기 때문에 mongoDB 등 다른 데이터베이스에서도 공통적으로 사용 가능한 기능이다.

2-2. 특별한 반환 타입

  • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
    - 사용 예 :  한 화면에 출력 될 페이지 번호와 이전 및 다음 페이지를 설정 할 수 있다. 웹에서 주로 사용됨 
    - 추가 count 쿼리가 포함되므로 조인테이블이 있을때 성능저하 우려하여 최적화가 필요 할 수 있다. 
    ⇲참고: count 쿼리 분리(이건 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 됨) 참고: 전체 count 쿼리는 매우 무겁다.
    실무에서 매우 중요!!!
@Query(value = "select m from Member m",
        countQuery = "select count(m) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
  • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1조회)
    • 사용 예 : 모바일에서 페이지처리를 더보기로 할때
    • 내부적으로 쿼리 발생시 limit 보다 1 개 더 조회하여 다음페이지를 확인 할 수 있다. )
  • List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

페이징과 정렬 사용 예제

Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);

다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.

검색 조건: 나이가 10살
정렬 조건: 이름으로 내림차순
페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

public interface MemberRepository extends Repository<Member, Long> {
  Page<Member> findByAge(int age, Pageable pageable);
  }

Page 사용 예제 실행 코드

//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
//given
      memberRepository.save(new Member("member1", 10));
      memberRepository.save(new Member("member2", 10));
      memberRepository.save(new Member("member3", 10));
      memberRepository.save(new Member("member4", 10));
      memberRepository.save(new Member("member5", 10));
//when
      PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
  "username"));
      Page<Member> page = memberRepository.findByAge(10, pageRequest);
//then
List<Member> content = page.getContent(); //조회된 데이터 assertThat(content.size()).isEqualTo(3); //조회된 데이터 수 assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수 assertThat(page.getNumber()).isEqualTo(0); //페이지 번호 assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호 assertThat(page.isFirst()).isTrue(); //첫번째 항목인가? assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}

두 번째 파라미터로 받은 Pageable 은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.
PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.

⚠️주의: Page는 1부터 시작이 아니라 0부터 시작이다.

2-3. Page 인터페이스

public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}

2-4. Slice 인터페이스

public interface Slice<T> extends Streamable<T> {
int getNumber();
int getSize();
int getNumberOfElements();
List<T> getContent();
boolean hasContent();
Sort getSort();
boolean isFirst();
boolean isLast();
boolean hasNext();
boolean hasPrevious();
Pageable getPageable();
Pageable nextPageable();
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}

2-5. 페이지를 유지하면서 엔티티를 DTO로 변환하기

Page<Member> page = memberRepository.findByAge(10, pageRequest);

컨트롤러 반환값으로 Member 엔티티를 직접 노출하면 ! !!! 안된다. !!
엔티티 -> DTO 로 변환하여 반환하기

Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

3-1. 내가 작성한 페이징 처리 코드

controller

  @GetMapping("/check")
  public Result getFaqList(
      @RequestParam(value = "page", required = false, defaultValue = "1") int page,
      @RequestParam(value = "size", required = false, defaultValue = "10") int size,
      @RequestParam(value = "direction", required = false, defaultValue = "DESC") Direction direction,
      @RequestParam(value = "properties", required = false, defaultValue = "createdDate") String properties) {
    return faqService.getFaqList(page, size, direction, properties);
  }

service

  //FAQ 전체 조회 (보기)
  @Override
  public Result getFaqList(int page, int size, Direction direction, String properties) {
    Page<Faq> faqListPage = faqRepository.findAll(
        PageRequest.of(page - 1, size, direction, properties));
    return pageProcessing(faqListPage, page, size);
  }
  --------------------------------
  
  //페이지 처리를 위한 메서드
  public Result pageProcessing(Page<Faq> faqListPage, int page, int size) {
    int totalCount = (int) faqListPage.getTotalElements();
    if (faqListPage.isEmpty()) {
      throw new CustomException(ExceptionStatus.POST_IS_EMPTY);
    }
    List<FaqResponse> faqResponses = faqListPage.stream().map(FaqResponse::new)
        .toList();
    int countList = size;
    int countPage = 5;//todo 리팩토링때  10으로 변경예정
   // int totalPage = totalCount / countList;
    int totalPage = faqListPage.getTotalPages();
    if (totalCount % countList > 0) {
      totalPage ++;
    }
    if (totalPage < page) {
      page = totalPage;
    }
    return new Result(page, totalCount, countPage, totalPage, faqResponses);
  }

  @Getter
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  public static class Result<T> {
    private int page;
    private int totalCount;
    private int countPage;
    private int totalPage;
    private T data;


    public Result(int page, int totalCount, int countPage, int totalPage, T data) {
      this.page = page;
      this.totalCount = totalCount;
      this.countPage = countPage;
      this.totalPage = totalPage;
      this.data = data;
    }
  }  
  

3-1-1. 개선 전) List faqResponses 반환타입일 경우

    List<FaqResponse> faqResponses = faqListPage.stream().map(FaqResponse::new).toList();

  int totalPage = totalCount / countList;
  if (totalCount % countList > 0) {
    totalPage ++;
  }

3-1-2. 개선 후)Page faqResponses 반환타입일 경우

Page<FaqResponse> faqResponses =faqListPage.map(FaqResponse::new);

int countList = size;
int countPage = 5;//todo 리팩토링때  10으로 변경예정  
  
int totalPage = faqListPage.getTotalPages();

Page<> 반환된 페이지 정보를 활용하여 3-1 처럼 계산 및 별도 처리를 할 필요가 없어졌다. >.<ㅎㅎㅎㅎ

3-1-3. 결론

Page<> 인터페이스에 대해서 자세히 공부 하고 나니 , 굳이 Result<> 객체로 페이징에 필요한 정보를 별도로 전달할 필요가 없다는 걸 알 수 있었다.

front code

 .ajax(settings)
                        .done(function (response) {
                            console.log(response);
                            let rows = response.data
                            for (let i = 0; i < rows.length; i++) {
                                let faqId = rows[i]['id']
                                let number = faqId
                                let question = rows[i]['question']
                                let createdDate = rows[i]['createdDate']
                                let temp_html = `<tr>
                                                    <td>${number}</td>
                                                    <td><a href ="contactDe-faq.html?${faqId}">${question}</td>
                                                    <th>${createdDate}</th> 
                                                </tr>`
                                $('#faqList').append(temp_html)


                            }
                            let page = response.page
                            let totalCount = response.totalCount
                            let countPage = response.countPage
                            let startPage = ((page - 1) / 10) * 10 + 1;
                            let endPage = startPage + countPage - 1;
                            let totalPage = response.totalPage;
                            if (totalPage < page) {
                                page = totalPage;
                            }
                            if (endPage > totalPage) {
                                endPage = totalPage
                            }

                            for (let i = startPage; i <= endPage; i++) {
                                let number = i
                                let temp_html = `<a href="contactPageMove-faq.html?${number}">${number}</a>`

                                $('#faqPaging').append(temp_html);
                            }
                        }).fail(function (response,status) {
                            console.log(response.responseJSON);
                            if (response.responseJSON.statusCode === 403) {
                                alert(response.responseJSON.message)
                            }
                            if (response.responseJSON.statusCode === 404) {
                                alert(response.responseJSON.message)
                            }
                        });

                }

3-2. Result객체 없이 page<>객체로 반환하여 불필요한 코드 줄이기

3-2-1. Page<> 객체 반환시 페이징 처리에 필요한 속성들도 함께 반환된다.

  • offset: 현재페이지에서 첫번째 게시물

  • pageNumber : 현재 페이지 번호 ( page-1 )

  • pageSize : 한 페이지에 출력되는 게시물의 크기

  • totalPage : 총 페이지수

  • totalElement: 총 게시물의 갯수

  • last : 마지막 페이지인지 아닌지 유무

  • first : 첫번째 페이지인지 아닌지 유무

  • numberOfElements : 현재 화면에 보여지는 게시물의 수

    ItemController

  @GetMapping("")
  public Page<ItemResponse> getRegisteredItems(@AuthenticationPrincipal UserDetailsImpl userDetails,
      @RequestParam(value = "page", required = false, defaultValue = "1") int page,
      @RequestParam(value = "size", required = false, defaultValue = "5") int size,
      @RequestParam(value = "direction", required = false, defaultValue = "DESC") Direction direction,
      @RequestParam(value = "properties", required = false, defaultValue = "createdDate") String properties) {
    return itemService.getRegisteredItems(page, size, direction, properties,userDetails.getUser().getId());
  }

ItemService

  public Page<ItemResponse> getRegisteredItems(int page, int size, Direction direction, String properties,Long sellerId) {
    Page<Item> itemList = itemRepository.findAllBySellerId(
        PageRequest.of(page - 1, size, direction, properties),sellerId);
     Page<ItemResponse> itemPageList = itemList.map(o->new ItemResponse(o));
     return itemPageList;
  }

ItemRepository

public interface ItemRepository extends JpaRepository<Item, Long> {

  Optional<Item> findBySellerIdAndId(Long sellerId,Long id);



  Page<Item> findAllBySellerId(PageRequest of, Long sellerId);
  List<Item> findAllBySellerId(Long sellerId);
}

ItemResponseDto

package study.wonyshop.item.dto;


@Getter
public class ItemResponse {
  private final String name; //상품명
  private final int price; // 가격
  private final int stockQuantity; // 재고 수량

  //  private final List<Category> categories = new ArrayList<>();
  private final String image;
  private final String itemDetail;
  @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm")
  private final LocalDateTime createdDate;

  public ItemResponse(Item item) {
    this.name = item.getName();
    this.price = item.getPrice();
    this.stockQuantity = item.getStockQuantity();
    this.image = item.getImage();
    this.itemDetail = item.getItemDetail();
    this.createdDate = item.getCreatedDate();
  }
}

참고 : 김영한 강의 Spring Data Jpa
page: https://ibks-platform.tistory.com/277 (페이지를 이용한 REST API 응답을 보여주는 예제입니다.)
slice: https://devjem.tistory.com/74 (Slice는 Page의 상위 타입으로, 링크의 예처럼 모바일 환경에서 많이 사용하는 무한 스크롤링 예제에 사용하기 좋은 것 같습니다. )
JPA Pagination, 그리고 N + 1 문제

profile
개발자꿈나무

0개의 댓글