[querydsl] Querydsl 페이징 연동

Welcome to Seoyun Dev Log·2023년 5월 19일
0

JPA

목록 보기
15/15

Page Querydsl

  • 사용자 정의 인터페이스에 페이징 메서드 정의
package study.querydslpractice.repository;

import org.springframework.data.domain.Pageable;
import study.querydslpractice.dto.MemberSearchCondition;
import study.querydslpractice.dto.MemberTeamDto;

import java.util.List;

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    List<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    List<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
  • 사용자 정의 인터페이스 구현

    fetchResult를 사용하면 페이징 처리한 데이터와 count를 같이 조회할 수 있다. 하지만 fetchResult를 사용하면 count 쿼리 부분에서 문제가 된다. count에 상관없는 테이블을 조회하는 등의 문제로 성능 이슈가 발생한다. 따라서 현재 Querydsl에서는 fetchResult를 사용하지 않도록 deprecate 된 상태이므로 위에서 작성한 예시 코드처럼 페이징으로 데이터 조회하는 쿼리와 count를 구하는 쿼리를 따로 작성하는 것을 권장한다.

@Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = jpaQueryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername())
                        , teamNameEq(condition.getTeamName())
                        , ageGoe(condition.getAgeGoe())
                        , ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())//몇번부터 시작할 것인지
                .limit(pageable.getPageSize())//몇개까지 가지고 올 것 인가
                .fetch();

        Long totalCount = jpaQueryFactory
                .select(member.count())
                .from(member)
                .where(usernameEq(condition.getUsername())
                        , teamNameEq(condition.getTeamName())
                        , ageGoe(condition.getAgeGoe())
                        , ageLoe(condition.getAgeLoe())
                )
                .fetchOne();
        
        return new PageImpl<>(content, pageable, totalCount);
    }

PageImpl은 Spring Data의 Page의 구현제이다.

  • 테스트 코드
@DisplayName("searchByWhereTest")
    @Test
    void searchPageSimpleTest() {
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        MemberSearchCondition condition = new MemberSearchCondition();
        PageRequest pageRequest = PageRequest.of(0, 3);

        Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);
        assertThat(result.getSize()).isEqualTo(3);
        assertThat(result.getContent()).extracting("username").containsExactly("member1", "member2", "member3");
    }

fetch()로 content와 totalCount를 분리하는 이유는
조회 쿼리보다 totalCount 쿼리가 단순하게 조회가 가능한 경우(join이 없거나) 쿼리를 분리하여 실행하는 것이 좋다.

만약 fetchResult()로 하나의 쿼리에 content와 totalCount를 가지고 올 수 있는 경우에는
totalCount도 join, where절을 모두 타야하기 때문에 카운트 쿼리를 성능 최적화 할 수 없다
따라서 별로로 쿼리를 작성하는 것이 좋다

전체 카운트를 조회 하는 방법을 최적화 할 수 있으면 이렇게 분리하면 된다. (예를 들어서 전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.)
코드를 리펙토링해서 내용 쿼리과 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.

  • 따로 메서드를 분리해서 재사용하는 것도 가능하다
@Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = getContent(condition, pageable)
        Long totalCount = getTotalCount(condition);
        return new PageImpl<>(content, pageable, totalCount);
    }

    private Long getTotalCount(MemberSearchCondition condition) {
        Long totalCount = jpaQueryFactory
                .select(member.count())
                .from(member)
                .where(usernameEq(condition.getUsername())
                        , teamNameEq(condition.getTeamName())
                        , ageGoe(condition.getAgeGoe())
                        , ageLoe(condition.getAgeLoe())
                )
                .fetchOne();
        return totalCount;
    }

    private List<MemberTeamDto> getContent(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = jpaQueryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername())
                        , teamNameEq(condition.getTeamName())
                        , ageGoe(condition.getAgeGoe())
                        , ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())//몇번부터 시작할 것인지
                .limit(pageable.getPageSize())//몇개까지 가지고 올 것 인가
                .fetch();
        return content;
    }

Count 쿼리 최적화

  • PageableExecutionUtils를 사용하면 Count 쿼리 최적화할 수 있다.
  • 내부적으로 Count 쿼리가 필요없으면 조회해오지 않는다
  • 스프링 데이터 라이브러리가 제공
  • 조건 : 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
    마지막 페이지 일 때(offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구함)

조건에 충족하지 않을 경우 getPage가 카운트 쿼리를 호출하지 않는다

@Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = getContent(condition, pageable);
        JPAQuery<Long> totalCountJPAQuery = getTotalCountJPAQuery(condition);
        return PageableExecutionUtils.getPage(content, pageable, () -> totalCountJPAQuery.fetchOne());
    }
    
private JPAQuery<Long> getTotalCountJPAQuery(MemberSearchCondition condition) {
        return jpaQueryFactory
                .select(member.count())
                .from(member)
                .where(usernameEq(condition.getUsername())
                        , teamNameEq(condition.getTeamName())
                        , ageGoe(condition.getAgeGoe())
                        , ageLoe(condition.getAgeLoe())
                );
    }
profile
하루 일지 보단 행동 고찰 과정에 대한 개발 블로그

0개의 댓글