Spring Data JPA + QueryDSL 페이징 처리

이번 포스팅에서는 QueryDSL과 Spring Data JPA를 함께 사용해 페이징 처리하는 방법에 대해 알아보고자 한다.

 

다음은 이번 포스팅에서 사용할 QueryDSL 사용자 정의 레포지토리 인터페이스와 이를 구현한 구현체 이다.

이 내용에 대해 알고 싶으면 아래 링크를 참고 하면 도움이 된다.

2023.07.10 - [오픈소스] - QueryDSL Repository 생성하기 (사용자 정의 리포지토리)

 

public interface MemberRepositoryCustom {
    Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable);
}

 

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
    private final JPAQueryFactory queryFactory;

    @Override
    public Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
		return null;
    }
}

 

이제 이 구현체 레포지토리 MemberRepositoryImpl에 페이징을 위한 코드를 작성해 보도록 하겠다.


페이징 적용

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
    private final JPAQueryFactory queryFactory;

    @Override
    public Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = 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<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(content, pageable, () -> countQuery.fetchOne());
    }

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;
    }
    
    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName) : null;
    }
    
    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe == null ? null : member.age.goe(ageGoe);
    }
    
    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe == null ? null : member.age.loe(ageLoe);
    }
}

조회 데이터와 전체 카운트를 분리하였다.

 

fetchResults() deprecated 

전에는 QueryDSL에서 fetchResults() 메서드를 제공했었는데 이 메서드를 사용해 반환하게 되면 조회 데이터와 쿼리 데이터를 한번에 조회하는 것이였다.

그런데 이 방식이 성능에 문제가 있었는지 현재는 deprecated 되었다.

 

그래서 현재는 위와 같은 방식으로 조회 데이터와 카운트 쿼리를 분리하는 방법을 사용해야 한다.

 

PageableExecutionUtils 

PageableExecutionUtils 를 사용해 반환하면 content(조회 데이터)와 pageable의 total size를 보고

페이지의 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작거나 마지막 페이지 일 때 카운트 쿼리를 카운트 쿼리를 실행하지 않는다. 

따라서 PageableExecutionUtils을 사용해 반환할 시 성능 최적화가 발생한다.


페이징 사용

이제 컨트롤러를 만들어 Postman으로 페이징이 잘 적용되었는지 확인해보자.

(현재 테스트용 데이터를 넣어둔 상태이다.)

 

Controller
    @GetMapping("/v1/members")
    public Page<MemberTeamDto> searchMemberPaging(@ModelAttribute MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPage(condition, pageable);
    }

 

Postman 요청

이제 해당 URL로 GET 요청을 보내보자.

(파라미터 없이 요청하면 기본값으로 0번째 페이지부터 20개의 데이터를 보여준다.)

 

다음은 위 처럼 파라미터를 주어 요청한 결과이다.

첫 번째 페이지부터 3개의 페이지가 보여지는 것을 확인할 수 있다.

그 외에도 pageable이 제공하는 다양한 데이터가 있다.

{
    "content": [
        {
            "memberId": 1,
            "username": "member0",
            "age": 0,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 2,
            "username": "member1",
            "age": 1,
            "teamId": 2,
            "teamName": "teamB"
        },
        {
            "memberId": 3,
            "username": "member2",
            "age": 2,
            "teamId": 1,
            "teamName": "teamA"
        }
    ],
    "pageable": {
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 3,
        "paged": true,
        "unpaged": false
    },
    "totalElements": 100,
    "totalPages": 34,
    "last": false,
    "size": 3,
    "number": 0,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": true,
    "numberOfElements": 3,
    "empty": false
}

 

 


주의사항

주의할점으로, Spring Data JPA의 Sort를 QueryDSL에서 사용할 수 없다.

 

이유는 다음과 같다.

Spring Data JPA의 Sort 기능은 Spring Data JPA 프레임워크에서 제공하는 메서드로 정렬을 처리하는 반면,

QueryDSL은 별도의 쿼리 생성 매커니즘을 사용하여 쿼리를 생성하고 실행할 수 있다.

그렇기 때문에, QueryDSL에서 직접적으로 Spring Data JPA의 Sort 기능을 사용할 수 없고, 이를 직접 변환하여 호환되는 QueryDSL의 OrderSpecifier 클래스로 만들어 적용해야 한다.

 

다음 포스팅에서 JPA의 Sort 객체를 OrderSpecifier 객체로 변환하는 기능을 갖는 메서드를 만들어 QueryDSL에서 동적 정렬 기능을 추가하는 방법에 대해 설명해보려 한다.