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();
org.springframework.data
의 기능으로 common 공통이기 때문에 mongoDB 등 다른 데이터베이스에서도 공통적으로 사용 가능한 기능이다. - 사용 예 : 한 화면에 출력 될 페이지 번호와 이전 및 다음 페이지를 설정 할 수 있다. 웹에서 주로 사용됨
- 추가 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);
페이징과 정렬 사용 예제
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부터 시작이다.
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
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); //변환기
}
Page<Member> page = memberRepository.findByAge(10, pageRequest);
컨트롤러 반환값으로 Member 엔티티를 직접 노출하면 ! !!! 안된다. !!
엔티티 -> DTO 로 변환하여 반환하기
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
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;
}
}
List<FaqResponse> faqResponses = faqListPage.stream().map(FaqResponse::new).toList();
int totalPage = totalCount / countList;
if (totalCount % countList > 0) {
totalPage ++;
}
Page<FaqResponse> faqResponses =faqListPage.map(FaqResponse::new);
int countList = size;
int countPage = 5;//todo 리팩토링때 10으로 변경예정
int totalPage = faqListPage.getTotalPages();
Page<> 반환된 페이지 정보를 활용하여 3-1 처럼 계산 및 별도 처리를 할 필요가 없어졌다. >.<ㅎㅎㅎㅎ
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)
}
});
}
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 문제