9. 페이징과 정렬

민정·2022년 12월 12일
0

Spring Data JPA

목록 보기
9/17
post-thumbnail
post-custom-banner

1. 순수 JPA 페이징과 정렬

setFirstResult(offset), setMaxResult(limit) 사용

📁 MemberJpaRepository

@Repository
public class MemberJpaRepository {

    @PersistenceContext
    private EntityManager em;
    
    
    public List<Member> findByPage(int age, int offset, int limit){
    // 나이 10살, 이름 내림차순 조회, 한 페이지당 3건
    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();
    }
    
    
    // 총 페이지 구하기
    // 성능 최적화를 위해서 sorting 조건 제외
    public long totalCount(int age){
        return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
                .setParameter("age", age)
                .getSingleResult();
    }

}

✔ MemberRepositoryTest

	@Test
    public void paging(){
        // given
        memberJpaRepository.save(new Member("member1", 10));
        memberJpaRepository.save(new Member("member2", 10));
        memberJpaRepository.save(new Member("member3", 10));
        memberJpaRepository.save(new Member("member4", 10));
        memberJpaRepository.save(new Member("member5", 10));

        int age = 10;
        int offset = 1; 
        int limit = 3;

        // when
        List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
        long totalCount = memberJpaRepository.totalCount(age);

        // then
        assertThat(members.size()).isEqualTo(3); // 3개의 데이터 가져옴
        assertThat(totalCount).isEqualTo(5); // 총 5명의 멤버 생성했으니, 총 개수는 5
    }

참고

JPA는 방언 기반으로 동작해서 DB가 변경되더라도 페이징 쿼리가 현재 DB에 맞는 SQL문 짜여져서 실행됨



2. Spring Data JPA 페이징과 정렬

✨ 페이징과 정렬 파라미터

  • 정렬 : org.springframework.data.domain.Sort
  • 페이징 : org.springframework.data.domain.Pageable (내부에 Sort 포함)

Interface

org.springframework.data 패키지안에 Sort와 Pageable 클래스가 존재한다.
즉, 모든 DB에서 공통의 페이징과 정렬을 사용할 수 있음을 의미한다.

✨ 특별한 반환 타입

반환 타입에 따라서 totalCount 쿼리 날릴지 결정됨.
1. Page ⭕
2. Slice ❌
3. List ❌

1. Page

  • org.springframework.data.domain.Page
    • 추가 totalCount 쿼리 결과 포함

2. Slice

  • org.springframework.data.domain.Slice
    • 추가 totalCount 쿼리 없이 다음 페이지만 확인 가능
    • 내부적으로 limit + 1 수행
    • ex) 쭉 내리다가 더보기 버튼 클릭하는 방식 (최근 많이 사용)

3. List

  • List (자바 컬렉션)
    • 추가 totalCount 쿼리 없이 결과만 반환
    • 페이징 기능 없음

🚨 주의! 🚨

page는 1이 아닌 0부터 시작


📁 MemberRepository

    // 페이징과 정렬
    
    Page<Member> findPageByAge(int age, Pageable pageable); // 파라미터 pageable, 반환타입 Page
    
    Slice<Member> findSliceByAge(int age, Pageable pageable); // 파라미터 pageable, 반환타입 Slice
    
    List<Member> findListByAge(int age, Pageable pageable); // 파라미터 pageable, 반환타입 Page

PageRequest

Pageable 인터페이스 구현체를 넘길 때는 PageRequest를 많이 사용

offset, limit, 정렬 기능 포함 가능

// PageRequest.of(offset, limit, Sort.by(오름차순(내림차순), "기준"));
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); 

✔ MemberRepositoryTest

  	@Test
    public void paging(){
        // 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));

        int age = 10;
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); 
        // PageRequest(구현체)의 부모를 타고가다보면 Pageable interface가 있음.
        // WARN) page 1이 아니라 0부터 시작!!

        // when
        Page<Member> page = memberRepository.findPageByAge(age, pageRequest);
        Slice<Member> slice = memberRepository.findSliceByAge(age, pageRequest);
        List<Member> list = memberRepository.findListByAge(age, pageRequest);

        // then
        // 1) 반환 타입 Page - totalCount쿼리 날라감
        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();// 다음 페이지 있는지 여부



        // 2. 반환 타입 Slice
        // Slice는 total 관련 함수들 지원 X
        List<Member> content_slice = slice.getContent();

        assertThat(content_slice.size()).isEqualTo(3);
//        assertThat(slice.getTotalElements()).isEqualTo(5);
        assertThat(slice.getNumber()).isEqualTo(0);
//        assertThat(slice.getTotalPages()).isEqualTo(2);
        assertThat(slice.isFirst()).isTrue();
        assertThat(slice.hasNext()).isTrue();



        // 3. 반환 타입 List
        // 위의 함수들 동작 안함

        // 실무 팁! Entity는 외부로 절대 반환 금지!! DTO로 변환해서 반환!!
        Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null)); 
        // Page유지하면서 DTO로 변경 : api로 반환 가능
        // Page 유지하면서 JSON생성될 때, totalPage, totalElement 등이 JSON으로 반환됨
    }
  • 반환 타입에 따라서 사용가능한 함수가 다르다!
    Slice의 경우 : TotalCount 관련 함수들은 지원하지 않는다.
    List의 경우 : 페이징 관련 함수를 모두 지원하지 않는다.

  • Slice 반환타입의 경우
    생성된 쿼리 확인해보면 limit 3이 아닌, limit 4
    즉, 내부적으로 limit + 1해서 쿼리를 생성

장점

Page 형식 사용하다가 totalCount 쿼리까지 날라가서 성능이 안나오니까 Slice 형식으로 변경하자! 했을 때, Spring Data JPA를 사용하면 반환 타입만 변경하면 해결!

✨ 주요 메서드

Page만 사용가능

  • getTotalPages() : 총 페이지 수
  • getTotalElements() : 전체 개수

Page, Slice 모두 사용 가능

  • getNumber() : 현재 페이지 번호
  • getSize() : 페이지 당 데이터 개수
  • getNumberOfElements() : 페이지 당 실제 데이터 개수
  • hasNext(): 다음 페이지 존재 여부
  • hasPrevious(): 이전 페이지 존재 여부
  • isFirst(): 시작 페이지 여부
  • getContent(): 실제 컨텐츠 가져오는 메서드. (반환 타입 : List<Entity>)
  • get(): 실제 컨텐츠 가져오는 메서드. (반환 타입 : Stream<Entity>)

getSize vs getNumberOfElements

repository.findAll(new PageRequest(0, 30)); 일 때, 30개의 데이터를 요청했지만, 실제로는 10개의 데이터만 들어있다고 가정하자.

getSize() = 30
getNumberOfElements() = 10



✨ TotalCount Query 최적화

다대일 left outer join을 할 때, 데이터를 가져올 때는 join을 해야하지만, totalCount 쿼리에서는 join을 하지 않아도 count 값은 같다.

데이터를 가져오는 쿼리가 복잡하면, count 쿼리도 복잡해져서 성능 최적화가 필요할 때가 있다.

@Query(value = "데이터 가져오는 쿼리", countQuery = "count 쿼리")

// totalCount 성능 최적화를 위한, 쿼리 분리(값 가져오는 쿼리, totalCount 쿼리)
@Query(value = "select m from Member m left join m.team t", countQuery = "select count(m.username) from Member m") 
Page<Member> findMemberAllBy(Pageable pageable); // 파라미터 pageable, 반환타입 Page

참고

Sorting도 복잡해지면 PageRequest안에서 해결하지 말고, 직접 @Query에 Sorting 쿼리를 적어주는 것이 편하다!



출처

김영한 강사님 - 인프런 실전! 스프링 데이터 JPA

https://ojt90902.tistory.com/716

post-custom-banner

0개의 댓글