본문 바로가기
Spring/SpringBoot

[SpringBoot] Querydsl에서 페이징 처리하기

by J4J 2024. 6. 5.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 querydsl에서 페이징 처리하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

Request 처리 방법

 

querydsl을 사용하는 경우 페이징 처리 하는 방법이 JPA 만을 사용하는 경우보다 자유성이 더 생길 수 있습니다.

 

하지만 일반적으로 querydsl을 사용할 때도 JPA 만을 이용하여 페이징 처리를 할 때와 동일하게 Pageable 인터페이스를 활용합니다.

 

 

 

JPA를 이용하여 데이터 처리를 수행하시는 분들이라면 Pageable 인터페이스가 무엇인지에 대해 대부분 많이 알고 계실 겁니다.

 

Pageable에 대해 간단하게만 얘기해 본다면 spring에서 제공해 주는 페이징 처리를 하기 위해 페이지네이션과 관련된 기본적인 정보들을 담아둔 인터페이스입니다.

 

그래서 Pageable를 사용한다면 개발자는 페이징 처리와 관련된 부가적인 행위를 할 필요 없이 인터페이스의 기능만을 활용하여 페이지네이션 기능을 제공할 수 있습니다.

 

 

반응형

 

 

Pageable을 이용하여 Controller를 간단하게 구성해보면 다음과 같이 작성해볼 수 있습니다.

 

package com.jforj.querydslpagination.controller;

import com.jforj.querydslpagination.StudentService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class StudentController {
    private final StudentService studentService;

    @GetMapping("/students")
    public ResponseEntity<Object> getStudents(Pageable pageable) {
        return ResponseEntity.ok(
                studentService.getStudents(pageable)
        );
    }
}

 

 

 

그리고 API를 요청할 때는 다음과 같이 parameter 값에 page와 size를 넣어주면 됩니다.

 

여기서 page는 사용자가 선택한 페이지의 번호를 의미하며 0번부터 시작합니다.

 

size는 한 개의 페이지에 조회되는 데이터 개수를 의미합니다.

 

포스트맨 API 요청

 

 

 

 

Repository 처리 방법 (1) - PageImpl

 

다음은 repository에서 데이터를 조회 후 page 형태로 데이터를 반환하는 방법입니다.

 

가장 먼저 PageImpl 구현체를 활용 해볼 수 있습니다.

 

 

 

querydsl에서 페이징 처리를 하기 위해서는 데이터를 조회하는 query와 전체 개수를 조회하는 query를 구분하여 모두 작성해야 합니다.

 

그래서 Repository를 간단하게 구성해보면 다음과 같이 작성할 수 있습니다.

 

package com.jforj.querydslpagination.repository;

import com.jforj.querydslpagination.dto.PagingStudent;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.jforj.querydslpagination.entity.QStudentEntity.studentEntity;

@Repository
@RequiredArgsConstructor
public class StudentRepository {
    private final JPAQueryFactory jpaQueryFactory;

    public Page<PagingStudent> getStudents(Pageable pageable) {
        List<PagingStudent> pagingStudents =
                jpaQueryFactory
                        .select(
                                Projections.constructor(
                                        PagingStudent.class,
                                        studentEntity.name,
                                        studentEntity.age
                                )
                        )
                        .from(studentEntity)
                        .offset(pageable.getOffset())
                        .limit(pageable.getPageSize())
                        .fetch();

        Long pagingStudentsCount =
                jpaQueryFactory
                        .select(studentEntity.count())
                        .from(studentEntity)
                        .fetchOne();

        return new PageImpl<>(
                pagingStudents,
                pageable,
                pagingStudentsCount
        );
    }
}

 

 

 

 

Repository 처리 방법 (2) - PageableExecutionUtils

 

다음은 PageableExecutionUtils 클래스를 활용하는 방법입니다.

 

첫 번째 방법인 PageImpl 구현체와 PageableExecutionUtils 클래스의 사용성에서 차이점은 최적화입니다.

 

 

 

PageImpl을 사용하는 경우에는 위에서 얘기한 것처럼 데이터를 조회하는 query와 전체 개수를 조회하는 query를 모두 호출해야 합니다.

 

하지만 PageableExecutionUtils를 사용한다면 전체 개수를 조회하는 query를 필요에 따라서만 호출하도록 도와줍니다.

 

그래서 항상 전체 개수를 조회하는 query가 동작되지 않기 때문에 더 빠른 속도로 비즈니스 로직 처리를 수행할 수 있습니다.

 

 

 

어떤 방식으로 PageableExecutionUtils가 최적화를 할 수 있는지에 대해서 간단히 살펴보겠습니다.

 

클래스 내부 로직을 확인해 보면 다음과 같이 getPage라는 메서드가 존재하는 것을 볼 수 있습니다.

 

public abstract class PageableExecutionUtils {

	private PageableExecutionUtils() {}

	/**
	 * Constructs a {@link Page} based on the given {@code content}, {@link Pageable} and {@link LongSupplier} applying
	 * optimizations. The construction of {@link Page} omits a count query if the total can be determined based on the
	 * result size and {@link Pageable}.
	 *
	 * @param content result of a query with applied {@link Pageable}. The list must not be {@literal null} and must
	 *          contain up to {@link Pageable#getPageSize()} items.
	 * @param pageable must not be {@literal null}.
	 * @param totalSupplier must not be {@literal null}.
	 * @return the {@link Page} for {@link List content} and a total size.
	 */
	public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {

		Assert.notNull(content, "Content must not be null");
		Assert.notNull(pageable, "Pageable must not be null");
		Assert.notNull(totalSupplier, "TotalSupplier must not be null");

		if (pageable.isUnpaged() || pageable.getOffset() == 0) {

			if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
				return new PageImpl<>(content, pageable, content.size());
			}

			return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
		}

		if (content.size() != 0 && pageable.getPageSize() > content.size()) {
			return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());
		}

		return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
	}
}

 

 

 

메서드 내부를 살펴보면 처음 방식과 같이 PageImpl을 이용하여 값을 return 하는 것을 볼 수 있습니다.

 

하지만 PageImpl의 3번째 파라미터를 살펴보면 전체 개수 query 조회가 필요한 supplier를 사용하지 않고 pageable의 offset과 조회된 데이터의 크기만을 이용하여 반환하는 것이 확인됩니다.

 

그래서 supplier를 사용하지 않는 분기 처리가 동작이 되면 전체 개수 조회를 위한 query를 수행하지 않기 때문에 최적화가 되는 것입니다.

 

 

 

 

supplier를 사용하지 않는 경우가 어떤 상황인지에 대해 살펴보면 다음과 같습니다.

 

public abstract class PageableExecutionUtils {

        ...
    
	public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {
    
                ...
        
                // (1) pageable이 pagination 정보가 없거나 처음 페이지를 조회하는 경우
		if (pageable.isUnpaged() || pageable.getOffset() == 0) {
        
                        // (1) pageable이 pagination 정보가 없거나 페이지에 보여야 하는 크기가 조회된 데이터 크기보다 큰 경우
			if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
				return new PageImpl<>(content, pageable, content.size());
			}

                ...
            
		}

                // (2) 조회된 데이터가 있고 페이지에 보여야 하는 크기가 조회된 데이터 크기보다 큰 경우
		if (content.size() != 0 && pageable.getPageSize() > content.size()) {
			return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());
		}

		...
        
	}
}

 

 

 

(1)에 해당하는 경우를 정리하면 다음과 같습니다.

 

  • 첫 번째 페이지를 조회
  • 페이지에 보여야 하는 데이터 개수가 조회된 데이터보다 큰 경우

 

 

 

(1)의 경우에 대해 간단하게 예를 들면 다음과 같습니다.

 

  • 첫 번째 페이지에서 조회된 데이터의 개수가 8개
  • 페이지에 보여야 하는 개수는 20개
  • 첫 번째 페이지를 조회할 때 전체 데이터 크기는 8개로 판단할 수 있기 때문에 전체 개수 조회 query 동작하지 않음

 

 

 

다음은 (2)에 해당하는 경우를 정리해 보겠습니다.

 

  • 조회된 데이터가 존재
  • 페이지에 보여야 하는 데이터 개수가 조회된 데이터보다 큰 경우

 

 

 

(2)의 경우에 대해 간단하게 예를 들면 다음과 같습니다.

 

  • 조회할 수 있는 전체 데이터 개수가 48개
  • 페이지에 보여야 하는 개수는 20개
  • 마지막 페이지 (3번째 페이지)를 조회하는 경우 조회된 데이터의 개수는 8개
  • 마지막 페이지를 조회할 때 전체 데이터 크기는 offset 40개 + 조회된 데이터 개수 8개 = 48개로 판단할 수 있기 때문에 전체 개수 조회 query 동작하지 않음

 

 

 

결론적으로 (1)과 (2)를 정리해 보면 첫 번째 페이지와 마지막 페이지를 조회할 때 상황에 따라 전체 개수 조회 query가 동작하지 않습니다.

 

그래서 PageImpl만을 사용할 때와 비교해 보면 최적화된 기능을 제공하는 것으로 볼 수 있습니다.

 

 

 

 

마지막으로 PageImpl에서 작성했던 코드를 PageableExecutionUtils 클래스를 사용하는 것으로 변경하면 다음과 같습니다.

 

package com.jforj.querydslpagination.repository;

import com.jforj.querydslpagination.dto.PagingStudent;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.jforj.querydslpagination.entity.QStudentEntity.studentEntity;

@Repository
@RequiredArgsConstructor
public class StudentRepository {
    private final JPAQueryFactory jpaQueryFactory;

    public Page<PagingStudent> getStudents(Pageable pageable) {
        List<PagingStudent> pagingStudents =
                jpaQueryFactory
                        .select(
                                Projections.constructor(
                                        PagingStudent.class,
                                        studentEntity.name,
                                        studentEntity.age
                                )
                        )
                        .from(studentEntity)
                        .offset(pageable.getOffset())
                        .limit(pageable.getPageSize())
                        .fetch();

        JPAQuery<Long> pagingStudentsCountQuery =
                jpaQueryFactory
                        .select(studentEntity.count())
                        .from(studentEntity);

        return PageableExecutionUtils
                .getPage(
                        pagingStudents,
                        pageable,
                        pagingStudentsCountQuery::fetchOne
                );
    }
}

 

 

 

 

 

 

 

 

이상으로 querydsl에서 페이징 처리하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글