[Spring] Data JPA 페이징과 정렬

박준형·2023년 11월 9일
0

Spring

목록 보기
5/17
post-thumbnail

📌순수 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();
}
// 전체 회원수를 반환하는 쿼리
public long totalCount(int age) {
        return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
                .setParameter("age", age)
                .getSingleResult();
    }

테스트 코드

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; // 0이면 jpa가 굳이 쿼리에 포함하지 않는다.
        int limit = 3;

        //when
        List<Member> members = memberJpaRepository.findByPage(age, offset, limit); // 페이징 쿼리
        long totalCount = memberJpaRepository.totalCount(age); // 전체 회원수

        //페이지 계산 공식 적용
        // totalPage = totalCount / size => data jpa 기능에서 제공해줌
        // 마지막 페이지, 최초 페이지

        //then
        assertThat(members.size()).isEqualTo(3); // limit을 3으로 해서 총 3개의 데이터만 넘어옴
        assertThat(totalCount).isEqualTo(5); // 전체 회원수 5명 반환
   }

📌data JPA 페이징 정렬

✅반환 타입 Page

쿼리 코드

Page<Member> findByAge(int age, Pageable pageable);

테스트 코드

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;
		
		// page는 0부터 시작 명심하기!, limit은 3, 이름으로 내림차순 정렬
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        //when
        // repository에서 Pageable로 받지만 PageRequest 부모 인터페이스에 Pageable이 있다.
        // 반환 타입이 Page 일 경우에 data JPA가 페이지 계산을 위해서 totalCount 쿼리를 날린다.(전체 데이터수를 알기 위해)
        // 우리가 정렬 조건을 적용해도 totalCount 쿼리는 정렬이 필요 없으므로 포함되지 않는다.
        Page<Member> page = memberRepository.findByAge(age, pageRequest); // 페이징 쿼리

        //then
        List<Member> content = page.getContent(); // 페이징 쿼리 결과 데이터
        long totalElements = page.getTotalElements(); // totalCount 쿼리 결과 즉, 전체 데이터 개수

        assertThat(content.size()).isEqualTo(3); // size를 3으로 설정해서 3개의 데이터
        assertThat(page.getTotalElements()).isEqualTo(5); // 전체 회원수는 5명
        assertThat(page.getNumber()).isEqualTo(0); // 현재 페이지 번호
        assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 개수
        assertThat(page.isFirst()).isTrue(); // 첫 페이지인지
        assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있는지
    }

✅반환 타입 Slice

Slice 타입은 totalCount 쿼리를 생성하지 않아서 전체 데이터 수가 많아 Page의 totalCount 쿼리가 성능에 영향을 끼친다면 유용하게 사용할 수 있다.

Slice 타입의 경우에 내가 요청하는 size 보다 +1 조회를 해서 데이터가 있다면 다음 페이지가 있다는 것으로 간주하고 페이지를 미리 로딩하거나 하는 성능 개선 용도로 사용한다(데이터를 3개씩 한 페이지에 보여준다고 가정하고, 4개를 요청했는데 성공하면 3개 이후에 더 보여줄 데이터가 있다는 뜻이므로 다음 페이지를 미리 로딩한다는 뜻)

쿼리 코드

Slice<Member> findByAge(int age, Pageable pageable);

테스트 코드

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;

		// page는 0부터 시작 명심하기!, limit은 3, 이름으로 내림차순 정렬
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));// page는 0부터 시작

        //when
        // repository에서 Pageable로 받지만 PageRequest 부모 인터페이스에 Pageable이 있다.
        // 반환 타입이 Page 일 경우에 data JPA가 페이지 계산을 위해서 totalCount 쿼리를 날린다.
        // 우리가 정렬 조건을 적용해도 totalCount 쿼리는 정렬이 필요 없으므로 포함되지 않는다.
        // Slice 타입은 size + 1의 결과를 가져온다.
        Slice<Member> page = memberRepository.findByAge(age, pageRequest); // 페이징 쿼리

        //then
        List<Member> content = page.getContent();
        //long totalElements = page.getTotalElements(); // Slice 타입은 전체 데이터 수를 알필요가 없기 때문에 미지원

        assertThat(content.size()).isEqualTo(3); // size를 3으로 설정해서 3 + 1개의 데이터
        //미지원 assertThat(page.getTotalElements()).isEqualTo(5); // 전체 회원수는 5명
        assertThat(page.getNumber()).isEqualTo(0); // 현재 페이지 번호
        //미지원 assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 개수
        assertThat(page.isFirst()).isTrue(); // 첫 페이지인지
        assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있는지
    }

📌페이징 결과 DTO로 매핑하기

API 반환을 할 때 엔티티를 외부에 노출시키면 안 되므로 DTO로 매핑을 해주어야 한다.
data JPA는 map 함수로 엔티티에서 DTO로 쉽게 변환하는 기능을 제공한다.

Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));

Page 타입도 @ResponseBody로 묶어서 반환하게 되면 JSON으로 반환된다.


페이징 할 때 Join을 사용할 경우 주의점❗

countQuery 설정 ❌

쿼리 코드

@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);

이런 식으로 페이지에 담을 데이터를 커스텀 할 때 join을 사용하게 되면 data JPA는 totalCount 쿼리를 날릴 때도 join을 사용하게 되어 데이터 수가 많아지면 성능에 영향을 끼치게 된다.


// data JPA가 날린 totalCount 쿼리
select count(member0_.member_id) as col_0_0_ 
from member member0_ left outer join team team1_ on member0_.team_id=team1_.team_id

countQuery 설정 ⭕

쿼리 코드

@Query(value = "select m from Member m left join m.team t",
            countQuery = "select count(m) from Member m")
Page<Member> findByAge(int age, Pageable pageable);

@Query에 countQuery로 totalCount 쿼리를 따로 지정해주면 불필요한 join 사용을 막아 성능을 개선할 수 있다.


// data JPA가 날린 totalCount 쿼리
select count(member0_.username) as col_0_0_ 
from member member0_

이전 쿼리문과 다르게 join이 빠진걸 볼 수 있다.

profile
으쌰 으쌰

0개의 댓글

관련 채용 정보