Querydsl - CountQuery 최적화 및 컨트롤러 개발
인프런 실전! Querydsl 강의 내용 정리
CountQuery 최적화
CountQuery를 최적화 하는 방법에 대해 알아보자.
- 스프링 데이터 라이브러리가 제공
- count 쿼리가 생략 가능한 경우 생략해서 처리
- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈 보다 작을 때
- 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> contents = queryFactory
.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()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = queryFactory
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.fetchOne();
}
어제 다음과 같은 페이징 메서드를 만들었다.
이제 스프링이 지원하는 페이징 카운트 최적화 방법을 알아보자.
먼저 카운트 쿼리에 .fetchOne() 을 빼고 다시 변수를 추출 한다.
1
2
3
4
5
6
7
8
9
10
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
그 후 메서드의 리턴을 PageableExecutionUtils.getPage(contents, pagable, 람다식)
이런 형태로 리턴 해주면 된다. 그럼 전체 코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> contents = queryFactory
.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()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.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::fetchOne);
//return new PageImpl<>(contents, pageable, total);
}
이렇게 하면 만약 조건이 성립하면 카운트 쿼리가 안 나가게 된다.
테스트로 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void searchPageTest() throws Exception {
//given
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
MemberSearchCondition condition = new MemberSearchCondition();
PageRequest pageRequest = PageRequest.of(0, 10);
//when
Page<MemberTeamDto> result = memberRepository.searchPageComplex(condition, pageRequest);
//then
assertThat(result.getContent()).extracting("username").containsExactly("member1", "member2", "member3", "member4");
}
다음과 같이 페이지가 0부터 10까지 인데 데이터가 4건이면
테스트를 돌려보면

딱 컨텐츠를 가져오는 쿼리만 나가는 걸 볼 수 있다.
그럼 조건이 안 맞으면? (페이징 0~3, 데이터 4개)

이땐 아까 생략되던 카운트 쿼리가 다시 나오게 된다.
컨트롤러 개발
이제 지금까지 만든 걸 컨트롤러까지 만들어서 이용해 보자.
MemberController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequiredArgsConstructor
public class MemberController {
...
private final MemberRepository memberRepository;
...
@GetMapping("/v2/members")
public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageSimple(condition, pageable);
}
@GetMapping("/v3/members")
public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageComplex(condition, pageable);
}
}
다음과 같이 컨트롤러를 만들어 줬다.

다음과 같이 페이징 정보랑 잘 나온다.
또 v3컨트롤러 같은 경우에는 데이터 보다 페이징 카운트가 크면 카운트 쿼리도 안나간다.
스프링 데이터 정렬(Sort)
스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다. 이 부분은 뒤에 스프링 데이터 JPA가 제공하는 Querydsl 기능에서 살펴 보자.
스프링 데이터 Sort를 Querydsl의 OrderSpecifier 로 변환
1
2
3
4
5
6
7
8
JPAQuery<Member> query = queryFactory.selectFrom(member);
for (Sort.Order o : pageable.getSort()) {
PathBuilder pathBuilder = new PathBuilder(member.getType(), member.getMetadata());
query.orderBy(new OrderSpecifier<Comparable>(o.isAscending() ? Order.ASC : Order.DESC,
pathBuilder.get(o.getProperty())));
}
List<Member> result = query.fetch();
참고 : 정렬(
Sort)은 조건이 조금만 복잡해져도Pageable의Sort기능을 사용하기 어렵다.
루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는Sort를 사용하기 보다는 파라미터를 받아서 직접 쿼리 처리를 하는 것을 권장한다.
댓글남기기