Java / / 2023. 1. 18. 11:05

Querydsl 페이징 연동 및 최적화 - Querydsl fetchResults() , fetchCount() Deprecated(향후 미지원)

반응형
 
해당 글은 김영한 님의 querydsl을 수강하며 정리하려고 적는 포스팅입니다.

 

먼저 스프링 데이터의 Page, Pageable을 활용해서 페이징 결과를 조회할 것이다.

그리고 전체 카운트를 한 번에 조회하는 단순 방법을 사용해 볼 것이다.

또한 데이터 내용과 전체 카운트를 별도로 조회하는 방법을 사용해 볼 것이다.


💡  참고 : Spring Boot 2.6 이상, Querydsl 5.0 지원 방법

Querydsl fetchResults() , fetchCount() Deprecated(향후 미지원)

Querydsl에서 제공하는 fetchCount()와 fetchResult()는 개발자가 작성한 select 쿼리로 count 쿼리를 내부에서 만들어서 실행시킨다. 그런데 이 기능은 select 구문을 단순히 count 쿼리로 변경하는 정도이다. 따라서 단순한 쿼리에서는 잘 작동하겠지만, 복잡한 쿼리로 넘어가면 잘 작동하지 않고 문제가 발생한다.

 

따라서 Queyrdsl은 향후 fetchCount()와 fetchResult()를 지원하지 않는다고 한다. Querydsl의 변화가 빠르지 않기 때문에 당장에 기능을 제거할 것 같지는 않다. 

 

따라서 count 쿼리가 필요하면 별도로 작성해야 한다.

 

count 쿼리 예제

@Test
public void count() {
    Long totalCount = queryFactory
        //.select(Wildcard.count) //select count(*)
        .select(member.count()) //select count(member.id)
        .from(member)
        .fetchOne();
        
    System.out.println("totalCount = " + totalCount);
}
  • count(*)을 사용하고 싶다면, 위의 주석처럼 WildCard.count를 사용하면 된다.
  • member.count()를 사용하면 count(member.id)로 처리된다.
  • 응답 결과는 숫자 하나 이므로 fetchOne()을 사용한다.

📌 이전 버전으로 예제 작성 - fetchResults() , fetchCount() 사용

 

 

 

사용자 정의 인터페이스에 페이징 두 가지 추가

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}

 

전체 카운드를 한 번에 조회하는 단순 방법

  • searchPageSimple(), fetchResults() 사용
public class MemberRepositoryImpl implements MemberRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> result = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .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())
                .fetchResults();

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

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

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName) : null;
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null ? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.loe(ageLoe) : null;
    }
}
  • Querydsl이 제공하는 fetchResults()를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다. (실제 쿼리는 2번 호출)
  •  fetchResult()는 카운트 쿼리 실행 시 필요 없는 order by를 제거한다.

 

데이터 내용과 전체 카운트를 별도로 조회하는 방법

  • searchPageComplex()
public class MemberRepositoryImpl implements MemberRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .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 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<>(content, pageable, total);
    }

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName) : null;
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null ? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.loe(ageLoe) : null;
    }
}
  • 별도로 나누어 두면 전체 count를 조회하는 방법을 최적화할 수 있으며, 위와 같이 분리하면 된다.
  • content는 쿼리가 복잡하지만 count 쿼리는 join 등이 필요 없어서 불필요한 작업을 더 하는 것을 방지한다.
    • 즉 전체 카운트를 조회할 때, 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.
  • 참고로 코드를 리펙토링해서 내용 쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다. (ctrl + alt + m)

 

 

 

 

💡 Count Query 최적화

JPAQuery<Long> countQuery = queryFactory
        .select(member.count())
        .from(member)
        .leftJoin(member.team, team)
        .where(
                usernameEq(condition.getUsername()),
                teamNameEq(condition.getTeamName()),
                ageGoe(condition.getAgeGoe()),
                ageLoe(condition.getAgeLoe())
        );

//return new PageImpl<>(content, pageable, total);
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
  • 스프링 데이터 라이브러리가 제공한다.
  • count 쿼리가 생략 가능한 경우 생략해서 처리한다. (제일 아래 예제 참고)
    • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈 보다 작을 경우
    • 마지막 페이지 일때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구한다.)
    • countQuery 자체를 처음엔 세팅만 해두고, return 할때 필요하면 query를 날린다고 보면 된다.

📌 searchPageComplex - Spring Boot 2.6 이상, Querydsl 5.0 ver으로 수정

 

 

 

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
    List<MemberTeamDto> content = queryFactory
            .select(new QMemberTeamDto(
                    member.id.as("memberId"),
                    member.username,
                    member.age,
                    team.id.as("teamId"),
                    team.name.as("teamName")
            ))
            .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();

    JPAQuery<Long> countQuery = queryFactory
            .select(member.count())
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            );

    //return new PageImpl<>(content, pageable, total);
    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
  • PageableExecutionUtils Deprecated(향후 미지원) 패키지 변경
    • import org.springframework.data.support.PageableExecutionUtils;  패키지 변경
    • 사용 패키지 위치가 변경되었습니다. 기존 위치를 신규 위치로 변경해 주시면 문제없이 사용할 수 있습니다.
  • 기존 : org.springframework.data.repository.support.PageableExecutionUtils
  • 신규 : org.springframework.data.support.PageableExecutionUtils

 


📌 컨트롤러 개발

실제 컨트롤러

@RestController
@RequiredArgsConstructor
public class MemberController {

    //private final MemberJpaRepository memberJpaRepository;
    private final MemberRepository memberRepository;

//    @GetMapping("/v1/members")
//    public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
//        return memberJpaRepository.search(condition);
//    }

    @GetMapping("/v2/members")
    public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageSimple(condition, pageable);
    }

    @GetMapping("/v3/members")
    public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageComplex(condition, pageable);
    }
}

 

 

 

 

결과 - postman 사용

  • http://localhost:8080/v2/members?teamName=teamB&ageGoe=31&ageLoe=35&page=0&size=2
  • count 쿼리 생략없이 무조건 조회
2023-01-18T14:40:33.951+09:00 DEBUG 8796 --- [nio-8080-exec-5] org.hibernate.SQL                        : 
    /* select
        count(member1) 
    from
        Member member1   
    left join
        member1.team as team 
    where
        team.name = ?1 
        and member1.age >= ?2 
        and member1.age <= ?3 */ select
            count(m1_0.member_id) 
        from
            member m1_0 
        left join
            team t1_0 
                on t1_0.id=m1_0.team_id 
        where
            t1_0.name=? 
            and m1_0.age>=? 
            and m1_0.age<=?
            
2023-01-18T14:40:33.955+09:00 DEBUG 8796 --- [nio-8080-exec-5] org.hibernate.SQL                        : 
    /* select
        member1.id as memberId,
        member1.username,
        member1.age,
        team.id as teamId,
        team.name as teamName 
    from
        Member member1   
    left join
        member1.team as team 
    where
        team.name = ?1 
        and member1.age >= ?2 
        and member1.age <= ?3 */ select
            m1_0.member_id,
            m1_0.username,
            m1_0.age,
            m1_0.team_id,
            t1_0.name 
        from
            member m1_0 
        left join
            team t1_0 
                on t1_0.id=m1_0.team_id 
        where
            t1_0.name=? 
            and m1_0.age>=? 
            and m1_0.age<=? offset ? rows fetch first ? rows only

 

V3 결과 

  • http://localhost:8080/v3/members?teamName=teamB&ageGoe=31&ageLoe=100&page=0&size=110
  • count 쿼리가 생략 가능한 경우 생략해서 처리한다.
2023-01-18T14:33:46.027+09:00 DEBUG 8796 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    /* select
        member1.id as memberId,
        member1.username,
        member1.age,
        team.id as teamId,
        team.name as teamName 
    from
        Member member1   
    left join
        member1.team as team 
    where
        team.name = ?1 
        and member1.age >= ?2 
        and member1.age <= ?3 */ select
            m1_0.member_id,
            m1_0.username,
            m1_0.age,
            m1_0.team_id,
            t1_0.name 
        from
            member m1_0 
        left join
            team t1_0 
                on t1_0.id=m1_0.team_id 
        where
            t1_0.name=? 
            and m1_0.age>=? 
            and m1_0.age<=? offset ? rows fetch first ? rows only

 


참고 : 스프링 데이터 정렬(sort)

스프링 데이터 JPA는 자신의 Sort를 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다.

 

정렬( Sort )은 조건이 조금만 복잡해져도 Pageable 의 Sort 기능을 사용하기 어렵다.

루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort 를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장한다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유