스프링 데이터 JPA와 Querydsl


스프링 데이터 JPA → Querydsl (페이징)


사용자 정의 Repository 생성

방법은 어렵지 않다. 별도의 Repository를 생성해서 스프링 데이터 리포지토리

사용자 정의 인터페이스를 상속하면 된다.

@Repository
public interface MemberRepository 
        extends JpaRepository<Member, Long>, CustomMemberRepository {
    //...
}
public class CustomMemberRepository implements CustomMemberRepository {

    @Override
    public Page<MemberDto> searchSimple(MemberSearchCondidtion condition, Pageable pageable) {
        List<MemberTeamDto> contents = getConten(condition, pageable);
        return new PageImpl<>(contents, pageable, contents.size());
    }

    @Override
    public Page<MemberDto> searchComplex(MemberSearchCondidtion condition, Pageable pageable) {
        List<MemberTeamDto> contents = getConten(condition, pageable);

        int size = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch().size();

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

}
private List<MemberTeamDto> getContent(MemberSearchCondition condition, Pageable pageable) {
    return queryFactory
            .select(new QMemberTeamDto(
                    member.id,
                    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())
            )
            .orderBy(member.id.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();
}


참고로 특정 API나 화면에 의존성이 높은 쿼리의 경우에는 굳이 사용자 정의

인터페이스를 만들 필요는 없다. 그냥 별도의 클래스로 만든 후 스프링 빈으로

등록만 해주면 된다.

@Repository
@RequiredArgsConstructor
public class CustomRepository {

    private final JPAQueryFactory queryFactory;

    //...
}


CountQuery 최적화


CountQuery를 옵션으로!

첫 페이지에 20개의 정보를 표출해야 하는데 데이터가 10개인 경우와, 마지막 페이지인

경우에는 Count쿼리를 수행해야 할까? 스프링 데이터에서 count쿼리 최적화를 위해

PageableExecutionUtils 클래스를 제공해준다.


▷ PageableExecutionUtils

package org.springframework.data.support

아래는 이미지는 PageableExecutionUtils 클래스에 있는 getPage() 메서드다.



우선 시작지점(offset)이 0 또는 페이징 여부를 확인 한다.

if (pageable.isUnpaged() || pageable.getOffset() == 0) {

    if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
        return new PageImpl<>(content, pageable, content.size());
    }

    return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
}


페이징 여부, 페이지 사이즈와 컨텐츠 사이즈를 비교한다.

예를 들면, 한 페이지에 보여줄 데이터는 5개인데 총 데이터는 4개일 경우

count쿼리를 수행하지 않고, 현재 페이지의 데이터 수를 반환한다.

if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
        return new PageImpl<>(content, pageable, content.size());
    }


아래의 현재 페이지가 마지막 페이지인지 아닌지를 확인하는 조건이다.

마지막 페이지라면 검색을 시작한 지점의 수현재 페이지의 데이터 수를 더한다.

if (content.size() != 0 && pageable.getPageSize() > content.size()) {
    return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());
}


최적화 적용 리펙토링

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
    List<MemberTeamDto> contents = getContent(condition, pageable);

    JPAQuery<Member> countQuery = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            );
    return PageableExecutionUtils.getPage(contents, pageable, countSupplier(countQuery));
}
@NotNull
private LongSupplier countSupplier(JPAQuery<Member> countQuery) {
    return () -> countQuery.fetch().size();
}


최적화 적용 확인해보기

▷ Case 1

전체 데이터pagesize(한 페이지 표출 데이터 수)offset
4건05건0
select 
    --.. 
from 
    member member0_ 
left outer join team team1_ 
    on member0_.team_id=team1_.team_id 
order by member0_.member_id desc 
    limit 5;

Count 쿼리는 수행되지 않았다.


▷ Case 2

전체 데이터pagesize(한 페이지 표출 데이터 수)offset
5건12건2
-- 메인 쿼리
select
    --..
from 
    member member0_ 
left outer join team team1_ 
    on member0_.team_id=team1_.team_id 
order by member0_.member_id desc 
limit 2 offset 2;

-- count 쿼리
select
    --..
from
    member member0_ 
inner join
    team team1_ 
        on member0_.team_id=team1_.team_id

메인 쿼리와 count 쿼리 모두 수행된다.


▷ Case 3

전체 데이터pagesize(한 페이지 표출 데이터 수)offset
5건22건4
select 
    --..
from 
    member member0_ 
left outer join team team1_ 
    on member0_.team_id=team1_.team_id 
order by member0_.member_id desc 
limit 2 offset 4;

Count 쿼리는 수행되지 않았다.