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에서 동적 정렬 기능을 추가하는 방법에 대해 설명해보려 한다.