본문 바로가기
JPA

Querydsl 실무 활용편 (2)

by Heesu.lee 2021. 5. 3.

Spring Data Paging 활용 - Querydsl 페이징 연동

사용자 정의 인터페이스 추가

public interface MemberRepositorySupport {

    List<MemberTeamDto> search(MemberSearchCondition condition);

    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);

    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}

사용자 정의 인터페이스에 페이징이 추가된 인터페이스를 정의한다.

Simple 의 경우, 앞서 Querydsl 결과 조회에서 배웠던 fetchResult 를 사용하여 페이징 결과를 가져온다.

Complex 의 경우, fetch 로 내용 조회를 가져온 후 별도의 count 쿼리를 다시 요청하여 개수 요청하여 페이징 결과를 가져오는 방식이다.

 

페이징 인터페이스 구현

public class MemberRepositorySupportImpl implements MemberRepositorySupport {

    private final JPAQueryFactory queryFactory;

    public MemberRepositorySupportImpl(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @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())
                .limit(pageable.getPageSize())
                .fetchResults();

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

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

    private BooleanExpression ageLoe(Integer age) {
        return ObjectUtils.isEmpty(age) ? null : member.age.goe(age);
    }

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

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

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

 

Paging 을 요청하기 위해 Spring Data Pageable 을 파라미터로 받고,

원하는 결과 조회는 Querydsl 의 fetchResult 를 사용하여 요청한다. (실제 쿼리 2번 호출)

Querydsl 이 제공하는 fetchResult() 를 사용하면 원하는 내용과 전체 카운트를 한번에 조회할 수 있다.

 

전체 카운트를 별도로 조회

@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();

    long count = queryFactory
            .select(member)
            .from(member)
            .leftJoin(member.team, team)
            .where(usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe()))
            .fetchCount();

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

private BooleanExpression ageLoe(Integer age) {
    return ObjectUtils.isEmpty(age) ? null : member.age.goe(age);
}

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

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

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

전체 카운트를 별도로 작성해주는 경우에는 결과조회를 fetchResult 가 아닌 fetch 로 내용 조회만 한 후,

카운트 쿼리를 추가적으로 작성하여 요청해준다.

 

대게 위처럼 fetchResult 를 사용하지 않고 별도로 분리하는 경우는 복잡한 조인이 들어가거나 데이터 개수가 많을 때 사용한다.

카운트 쿼리 자체에 별다른 조인이 필요없거나 데이터가 무수히 많은 경우에는 해당 쿼리를 튜닝하여 성능 개선을 도모할 수 있다.

단, 데이터가 많지 않은 경우 복잡하게 생각할 필요없이 단순히 fetchResult 사용하여도 무방하다.

 

CountQuery 최적화

Count 쿼리를 페이징 요청 시 따로 분리하는 경우 count 쿼리가 필요하지 않는 경우 별도로 요청하지 않을 수 있도록 할 수 있다.

Count 쿼리 역시 수천만, 수억건의 데이터가 있다면 성능상 이슈를 줄 수 있기 때문에 굳이 필요하지 않다면 사용하지 않는 것이 좋다.

 

Spring Data 라이브러리를 통해 Count 쿼리 생략 가능한 경우 생략하여 처리할 수 있다. - PageableExecutionUtils

  • 첫 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작은 경우
  • 마지막 페이지 일 때 - offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함
@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();

    JPAQuery<Member> countQuery = queryFactory
            .select(member)
            .from(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);
}

Spring Data 가 제공해주는 PageableExecutionUtils 를 사용하여 페이징 요청을 보낸다.

주의해야할 점은 기존 fetchCount 를 직접 요청하지 않고 요청에 대한 행위를 PageableExecutionUtils 메서드에 인자로 넘긴다.

 

참고

  • 해당 게시글은 김영한 님의 "실전! Querydsl" 를 참고하여 작성되었습니다.
  • 더 자세한 해당 강좌를 통해 확인하시기를 적극 추천드립니다.

'JPA' 카테고리의 다른 글

Querydsl 실무 활용편 (1)  (0) 2021.05.02
Querydsl 중급 문법  (0) 2021.04.28
Querydsl 기본 문법  (0) 2021.04.26
Querydsl 사용 개요  (0) 2021.04.25
쿼리 메소드 기능  (0) 2021.01.26

댓글