0. 관련 포스팅
- QueryDSL 설정하기
- QueryDSL DTO 반환 방법
- QueryDSL 동적 쿼리 적용
- QueryDSL 사용자 정의 레포지토리 적용
- QueryDSL 페이징 적용 <현재 포스팅>
1. 개요
QueryDSL에서 JPA Pageable 객체를 받아 페이징하는 부분에 대한 정리 내용이다.
2. 구현
이번 포스팅에서는 아래 3가지 메소드에 대한 구현체를 작성해 볼 예정이다.
searchPageSimple() : 기본 사용 방법
searchPageComplex() : 카운트 쿼리 별도 설정 방법
searchPageOptimization() : 별도 쿼리 및 PageExecutionUtils 를 이용한 Page 객체 반환 방법
구현체는 이전 포스팅에서 만든 MemberRepositoryImpl 클래스에 만들었고
MemberSearchCondition과 Pageable 을 파라메터로 받는 메소드이다.
a. searchPageSimple()
먼저 기본 페이징 기능을 구현하는 코드는 아래와 같다.
.fetchResults() 로 구해오는 방법이며 JPA 페이징 방식과 동일하게 작동한다.
@Override
public Page<PreMemberDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<PreMemberDto> results = queryFactory
.select(new QPreMemberDto(member.id, member.name, member.age))
.from(member)
.leftJoin(member.boards, board)
.where(
usernameLike(condition.getName())
.and(ageBetween(condition.getAgeGoe(), condition.getAgeLoe()))
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.groupBy(member.id)
.fetchResults();
List<PreMemberDto> contents = results.getResults();
long count = results.getTotal();
return new PageImpl<>(contents,pageable,count);
}
즉, 쿼리에 작성한 부분이 카운트 쿼리에서도 동일하게 적용된다는 뜻이다.
아래 실행 로그를 보면
카운트 쿼리가 먼저 나가고 조회 쿼리가 나가는 모습을 볼수 있는데
카운트 쿼리를 보면 조회에 적용한 left join이 그대로 나가는 모습을 볼수 있다.
필요에 따라서 동일하게 나가야하는 경우도 있겠지만,
그렇지 않아도 되는 경우 불필요한 리소스를 낭비하는 결과를 낳기도 한다.
b. searchPageComplex()
이번에는 조회 쿼리와 카운트 쿼리를 분리한 메소드이다.
아래 코드를 보면
조회 조건을 넣은 쿼리와 count 조건을 넣은 쿼리가 별도로 위치해 있는 것을 볼수 있다.
조회에는 .fetch(), 카운트에는 .fetchCount() 메소드를 사용하였다.
@Override
public Page<PreMemberDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<PreMemberDto> contents = queryFactory
.select(new QPreMemberDto(member.id, member.name, member.age))
.from(member)
.leftJoin(member.boards, board)
.where(
usernameLike(condition.getName())
.and(ageBetween(condition.getAgeGoe(), condition.getAgeLoe()))
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.groupBy(member.id)
.fetch();
long count = queryFactory
.selectFrom(member)
.where(
usernameLike(condition.getName())
.and(ageBetween(condition.getAgeGoe(), condition.getAgeLoe()))
)
.fetchCount();
return new PageImpl<>(contents, pageable, count);
}
아래 실행 로그를 보면
위에서 기본 페이징 기능을 사용할때와 다르게 카운트 쿼리가 별도로 설정한대로 나가는 모습을 볼수 있다.
이처럼 필요에 따라 카운트 쿼리를 별도 작성하여
불필요한 리소스 낭비를 방지할수 있게 된다.
c. searchPageOptimization()
위에서 카운트 쿼리를 별도로 작성한 방법과 비슷하지만
이번에는 PageableExecutionUtils를 이용한 방식이다.
@Override
public Page<PreMemberDto> searchPageOptimization(MemberSearchCondition condition, Pageable pageable) {
List<PreMemberDto> contents = queryFactory
.select(new QPreMemberDto(member.id, member.name, member.age))
.from(member)
.where(
usernameLike(condition.getName())
.and(ageBetween(condition.getAgeGoe(), condition.getAgeLoe()))
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<Long> totalCountQuery = queryFactory
.select(member.count())
.from(member)
.where(
usernameLike(condition.getName())
.and(ageBetween(condition.getAgeGoe(), condition.getAgeLoe()))
);
// 카운트 쿼리가 작동하지 않는 경우 (굳이 작동하지 않아도 되는 경우)
// 첫페이지에서 리밋을 숫자 보다 컨텐츠 갯수가 적은 경우
// 마지막 페이지일 떄
return PageableExecutionUtils.getPage(contents, pageable, totalCountQuery::fetchOne);
}
실제 실행하였을때 큰 차이를 보이지는 않지만 특정 조건에서는 다르게 작동을 하는데
1. 첫 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작았을 때
2. 마지막 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작았을 때
이렇게 두가지 조건에서 카운트 쿼리가 작동하지 않는데
굳이 카운트 쿼리가 수행되지 않아도 되는 경우에는 생략하게끔 설계되어있기 때문이다.
실행 로그를 확인하기 전
현재 Member의 수는 19 row이며 한번 조회에 PageSize를 100으로 설정한 후 실행해보면
이전 페이징 로직과 다르게 카운트 쿼리가 발생하지 않은 것을 볼수 있다.
그 이유는 PageableExecutionUtils 클래스를 보면 알수 있는데
getPage() 메소드 부분을 보면 카운트 쿼리가 들어가는 LongSupplier 가 조건에 따라 동작하기 때문에 위에서 언급한 조건에 따라 실행되지 않고 넘어가는 것을 볼수 있다.
3. 정렬(Sort)
QueryDSL 에서는 OrderSpecifier로 정렬 기능을 사용할수 있는데,
정렬에 대한 조건이 복잡해지면 Pageable의 Sort 기능을 사용하기 어려워져 Sort를 사용하기 보다는 파라메터를 받아서 직접 처리하는 것을 권장하고 있다.
이 부분에 대해서는 추후 직접 구현해보고 포스팅해볼 예정이다.
우선 강의에서 제공해준 OrderSpecifier 로 변환하는 코드를 첨부하였으니 참고 바란다.
JPAQuery<Member> query = queryFactory
.selectFrom(member);
for (Sort.Order o : pageable.getSort()) {
PathBuilder pathBuilder = new PathBuilder(member.getType(),
member.getMetadata());
query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
pathBuilder.get(o.getProperty())));
}
List<Member> result = query.fetch();
4. 마무리
QueryDSL 공부를 그동안 좀 미루고 있다가
'김영한' 강사님의 QueryDSL 강의로 1차적인 공부를 마무리하였다.
JPA 관련 강의 들으면서 QueryDSL 언급을 많이 하셨어서 궁금했었는데 왜 쓰는지 알게되는 강의였다.
물론 JPA를 기반으로 하기 때문에 어느정도 기반 지식이 있어야하는건 어쩔수없으니 앞선 강의부터 차근차근 들어보는걸 추천한다.
필자도 회사에서 SpringBoot를 사용했기 때문에 어느정도 기반 지식이 있었지만 중간에 비어있는 부분이 있는것 같아 복습 겸 러프하게 앞에 부분을 듣고 QueryDSL 강의를 보니 좀 더 이해하기 편했었기에 본인 판단하기에 처음부터 들을지 중간부터 들을지 선택하는것도 나쁘지 않을 듯하다.
QueryDSL 을 사용하지 않더라도 대체 가능한 라이브러리(Jooq 같은)들도 있기에 사용에 있어서는 장단점을 잘 파악한다면 대체 가능하니 다른 라이브러리도 사용해보고 비교해보는것도 좋은 공부가 될것 같다.
(Jooq도 좋긴한데 처음 구동 시, DB 스키마를 보고 처리하는 부분이 있어 협업이나 개발, 상용 서버를 따로 둔다면 중간 처리가 조금 복잡할 수 있다.)
강의로 접한 내용에 대한 포스팅은 여기까지 써볼 생각이지만
중간에 빠트린 내용이나 페이징 관련 기능은 BackEnd Application 에서 많이 들어가는 기능이기에 추가적인 공부 후에 관련 포스팅을 작성해볼 생각이다.
5. GitHub
https://github.com/mk1-p/jpa-sample-code
GitHub - mk1-p/jpa-sample-code: Test for Spring JPA
Test for Spring JPA. Contribute to mk1-p/jpa-sample-code development by creating an account on GitHub.
github.com
'DEV > Spring' 카테고리의 다른 글
[Spring] GlobalExceptionHandler 만들기 (with. Kotlin) (1) | 2024.11.09 |
---|---|
[Spring] Slack 메세지 Custom Annotation 만들기 (0) | 2023.12.15 |
[JPA] QueryDSL 사용자 정의 레포지토리 적용 (0) | 2023.11.06 |
[JPA] QueryDSL 동적 쿼리 적용 (0) | 2023.10.26 |
[JPA] QueryDSL DTO 반환 방법 (0) | 2023.10.23 |