본문 바로가기
JPA/Querydsl

Querydsl 15 - data jpa + query dsl 페이징

by 킹차니 2022. 1. 30.

 

이번에는 Query dsl을 사용한 동적쿼리와 스프링 데이터 jpa가 제공하는 편리한 페이징 기능을 함께 사용하는 방법을 알아보자.

 

이전 포스트에서 만든 MemberRepositoryCustom 인터페이스에 아래와 같은 두개의 동적 페이징 메서드를 추가한다.

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    /* 아래 두개 추가 */
    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}

 

그리고 MemberRepositoryImpl에서 아래와 같이 구현해주었다. (스프링 데이터 jpa 포스트는 여기)

@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
    QueryResults<MemberTeamDto> results = queryFactory
            .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()) /*offset*/
            .limit(pageable.getPageSize())/*limit*/
            .fetchResults();/*fetchResults*/

    List<MemberTeamDto> content = results.getResults();
    long total = results.getTotal();

    return new PageImpl<>(content, pageable, total);
}

Querydsl 문법에 offset과 limit, fetchResults를 적용할 수 있다. 그리고 이를 PageImpl타입으로 반환해주면 된다.

위와 같이 fetchResults를 사용하면 알아서 total count쿼리를 날려준다. 이를 테스트해보면 아래와 같다.

@Test
public 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", 10, teamB);
    Member member4 = new Member("member4", 20, teamB);
    Member member5 = new Member("member5", 55, teamB);
    em.persist(member5);
    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);
    List<MemberTeamDto> content = result.getContent();
    for (MemberTeamDto memberTeamDto : content) System.out.println("memberTeamDto = " + memberTeamDto);
}

출력결과:
memberTeamDto = MemberTeamDto(memberId=3, username=member5, age=55, teamId=2, teamName=teamB)
memberTeamDto = MemberTeamDto(memberId=4, username=member1, age=10, teamId=1, teamName=teamA)
memberTeamDto = MemberTeamDto(memberId=5, username=member2, age=20, teamId=1, teamName=teamA)

 

이제 searchPageComplex를 구현해보자. searchPageComplex는 단지 total Count쿼리를 따로 날릴 뿐이다.

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
    List<MemberTeamDto> contents = queryFactory
            .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();/*fetch 사용*/

    /*total Count 쿼리를 직접 날린다.*/
    long total = queryFactory.selectFrom(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .fetchCount();


    return new PageImpl<>(contents, pageable, total);
}

fetchResults를 사용하지 않고, fetch를 사용하면 total Count쿼리를 따로 날릴 수 있다.

 

 

 

 

그런데 이와 같은 Count쿼리를 최적화하는 것이 성능에 영향을 미치는 경우가 있다. 하여 Count쿼리를 최적화하는 방법을 알아보자.

 

Count 쿼리를 생략할 수 있는 경우는 아래와 같은 두 가지 경우가 있다.

1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때

 --> 즉 하나의 페이지에 100개의 컨텐츠를 보여주는데, 총 데이터가 100개가 안되는 경우

2. 마지막 페이지일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구함)

 

하여 위와 같은 두 가지 경우 Count쿼리를 생략해주는 기능이 있다. 위의 searchPageComplex메서드를 아래와 같이 수정한다.

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
    List<MemberTeamDto> contents = queryFactory
            .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();

    /* .fetchCount를 없애고 JpaQuery로 남겨둠 */
    JPAQuery<Member> countQuery = queryFactory.selectFrom(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            );

    return PageableExecutionUtils.getPage(contents, pageable, () -> countQuery.fetchCount());
}

반환타입을 보면 PageableExecutionUtils.getPage를 반환하고 있는 것을 알 수 있다.

또한 countQuery를 날리지 않고 getPage의 인자로 넘겨준다. 이렇게 넘겨주면 countQuery를 날리지 않아도 되는 상황에는 countQuery를 날리지 않는다.

 

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.