QueryDSL의 Paging
Spring Data JPA와 QueryDSL을 활용한 페이징에 대해 작성해보려고 한다.
Spring Data JPA를 사용하여 JPARepository를 상속받으면서, 동시에 사용자 정의 repository를 동시에 상속을 받아야 한다.
Java에서 Class는 다중 상속을 지원하지 않기 때문에, 다중 상속이 가능한 인터페이스를 활용한다.
JPARepository와 사용자 정의 Repository를 작성하는 방법은 아래 글에 자세히 적어놓았으니 참고하면 좋을 것 같다.
2023.11.24 - [JPA/Spring Data JPA] - [Spring Data Jpa] 6. 사용자 정의 리포지토리 구현 (*중요) 추후 queryDSL 활용
[Spring Data Jpa] 6. 사용자 정의 리포지토리 구현 (*중요) 추후 queryDSL 활용
이번 내용은 실무에서 정말 많이 사용한다. querydsl repository를 만들고 활용하기 좋다. 실무에서는 다양한 상황에 맞추어 인터페이스와 메서드를 직접 구현할 필요가 있다. 예를 들어, JPA를 사용하
toychip.tistory.com
Spring Data Page 활용1: Querydsl 페이징 연동
QueryDSL과 스프링 데이터를 결합하여 사용하면, 복잡한 쿼리의 결과를 페이징 처리하는 것이 매우 간단해진다.
스프링 데이터의 Pageable 인터페이스와 QueryDSL의 동적 쿼리 기능을 함께 사용하여, 효율적으로 페이징 처리를 구현할 수 있다.
페이징 처리를 위한 커스텀 리포지토리 인터페이스 먼저, 페이징 처리를 위한 메서드를 포함하는 커스텀 리포지토리 인터페이스를 정의한다.
// 위 글에서 언급한 것처럼 interface의 이름은 어떤것을 해도 무관하다.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
searchPageComplex(): 주어진 조건에 맞는 데이터를 페이징 처리하여 조회하는 메서드이다.
CountQuery 분류 이유와 효과
CountQuery를 따로 작성해야 하는 이유는 아래 글에서 한 번 언급한 적이 있는데, 실무적인 관점으로, totalPage는 그대로 사용하면 안 된다.
전체 페이지 수를 계산하는 과정은 성능에 영향을 줄 수 있다. totalPage 계산을 전체를 카운트하고 원하는 만큼 나누고 갖고 오는 데까지, 엄청난 자원이 소모가 되고, 성능이 느리다.
엄청나게 많은 조인이 일어난다고 가정할 때, 페이징은 그것을 계산할 필요가 없다. 그래서 CountQuery를 별도로 분리하는 것이 좋다.
2023.11.23 - [JPA/Spring Data JPA] - [Spring Data Jpa] 2. Spring Data Jpa 페이징
[Spring Data Jpa] 2. Spring Data Jpa 페이징
Spring Data JPA는 데이터베이스의 페이징 처리를 추상화하여 개발자가 쉽게 페이징을 구현할 수 있도록 도와준다. 이는 기존에 데이터베이스마다 다르게 적용되던 페이징 처리 방식을 단순화시킨
toychip.tistory.com
페이징 처리 구현
searchPageComplex 메서드는 페이징 처리를 위한 핵심 로직을 구현한다.
이 메서드는 조건에 맞는 데이터의 리스트(content)와 총 데이터 수(count)를 조회하여, PageImpl 객체를 생성하여 반환한다.
interface에서는 Page<MemberTeamDto> 형태로 반환하므로 Page의 구현체인 PageImpl 타입으로 반환한다.
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = getSearchPageContent(condition);
Long count = getSearchPageCount(condition);
return new PageImpl<>(content, pageable, count);
}
private List<MemberTeamDto> getSearchPageContent(MemberSearchCondition condition) {
return jpaQueryFactory
.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()),
userAgeGoe(condition.getAgeGoe()),
userAgeLoe(condition.getAgeLoe())
)
.fetch();
}
private Long getSearchPageCount(MemberSearchCondition condition) {
return jpaQueryFactory
.select(Wildcard.count)
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
userAgeGoe(condition.getAgeGoe()),
userAgeLoe(condition.getAgeLoe())
)
.fetchOne();
}
이 구현에서 주목할 점은 getSearchPageCount 메서드이다. 이 메서드는 쿼리의 총 결과 수를 조회하는 데 사용된다.
최신 버전의 QueryDSL에서는 fetchCount() 메서드가 사라지고, 대신 fetchOne() 메서드를 사용하여 결과 수를 조회하며, select(Wildcard.count)를 사용하여 쿼리의 결과 수를 카운트한다.
고도화하여 스프링 데이터 페이징 처리의 성능 최적화에 대해 알아보자.
페이징이 필요하지 않은 경우가 있다면 페이지 카운트 쿼리를 작성하지 않아도 되지 않을까?
스프링 데이터 페이징 활용2:
CountQuery 최적화 페이징 처리를 구현할 때, 특히 데이터가 많은 경우 Count 쿼리의 성능이 전체 응답 시간에 큰 영향을 미칠 수 있다.
Count 쿼리는 전체 데이터 수를 파악하기 위해 실행되지만, 모든 상황에서 필요한 것은 아니다.
아래의 상황을 보자.
1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때:
첫 페이지의 데이터가 페이지 사이즈보다 적다면, 이는 전체 데이터 수가 페이지 사이즈보다 적다는 것을 의미하므로, 추가적인 Count 쿼리 없이도 전체 페이지 수를 알 수 있습니다.
2. 마지막 페이지일 때:
데이터의 총 개수를 정확히 알 필요 없이, 현재 페이지와 페이지 사이즈를 통해 전체 데이터 수를 추정할 수 있다.
Spring Data JPA는 PageableExecutionUtils.getPage() 메서드를 통해, Count 쿼리 실행을 최적화할 수 있는 방법을 제공한다.
구현 방법
PageableExecutionUtils.getPage() 메서드는 위와 같은 조건을 자동으로 검사하여, 필요한 경우에만 Count 쿼리를 실행하게 한다.
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = getSearchPageContent(condition, pageable);
JPAQuery<Long> countQuery = getSearchPageCount(condition);
// Count Query를 자동으로 최적화하여 페이징 처리
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
private JPAQuery<Long> getSearchPageCount(MemberSearchCondition condition) {
return jpaQueryFactory
.select(Wildcard.count)
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
userAgeGoe(condition.getAgeGoe()),
userAgeLoe(condition.getAgeLoe())
);
}
private List<MemberTeamDto> getSearchPageContent(MemberSearchCondition condition, Pageable pageable) {
return jpaQueryFactory
.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()),
userAgeGoe(condition.getAgeGoe()),
userAgeLoe(condition.getAgeLoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
이 방법을 사용하여, 불필요한 Count 쿼리의 실행을 줄이고, 전체적인 애플리케이션의 성능을 향상시킬 수 있다.
특히 대용량 데이터를 다루는 애플리케이션에서는 이러한 최적화가 매우 중요할 수 있다.
'JPA > QueryDSL' 카테고리의 다른 글
[QueryDSL] 6. 동적 쿼리(중요⭐️) - BooleanBuilder, Where 다중 파라미터 사용 (0) | 2023.12.06 |
---|---|
[QueryDSL] 5. QueryDSL의 프로젝션과 결과 반환, @QueryProjection (0) | 2023.12.06 |
[QueryDSL] 4. QueryDSL의 Join과 FetchJoin (0) | 2023.12.06 |
[QueryDSL] 3. 정렬과 페이징, SubQuery (0) | 2023.12.04 |
[QueryDSL] 2. QueryDSL 문법 VS JPQL 문법 비교 (0) | 2023.12.01 |