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 |
댓글