Paging, Iterating Large Results, Sorting & Limiting

2024. 10. 20. 11:54Spring Boot/Spring Data JPA

이 내용은 Spring Data JPA에서 쿼리 메서드에 Paging, Sorting, Limiting 등의 기능을 적용하는 방법을 설명하고 있습니다. 구체적으로 Pageable, Sort, Limit와 같은 특정 타입의 파라미터를 사용하여 쿼리 결과를 동적으로 처리하는 방법을 다룹니다.

1. 페이징, 정렬, 제한을 적용한 쿼리 메서드 사용 예시

다음은 Pageable, Slice, Sort, Limit 등을 활용한 쿼리 메서드 예시입니다:

Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Sort sort, Limit limit);
List<User> findByLastname(String lastname, Pageable pageable);

2. 특정 파라미터 설명

  • Pageable: org.springframework.data.domain.Pageable 인스턴스를 사용해 쿼리에 페이징을 동적으로 추가할 수 있습니다. Page 객체는 전체 엘리먼트 수와 페이지 수에 대한 정보를 포함하고 있어, 결과에 대한 총 개수를 알려줍니다. 하지만, 총 개수를 계산하기 위해서는 별도의 count 쿼리가 실행되며, 데이터 저장소에 따라 이 작업이 비싼 연산이 될 수 있습니다.
  • Slice: Slice는 결과 세트에서 다음 슬라이스(Slice)가 있는지 여부만 알 수 있습니다. 대규모 데이터 셋을 다룰 때는 Page보다 성능 면에서 유리할 수 있습니다.
  • Sort: 쿼리 메서드에서 정렬 옵션을 적용하고자 할 때 사용합니다. 정렬이 필요할 때 Sort 파라미터를 메서드에 추가하여 동적으로 처리할 수 있습니다.
  • Limit: 쿼리에서 조회할 결과의 수를 제한하고자 할 때 사용됩니다. LimitList 결과를 반환할 때 쿼리가 조회하는 엔티티 수를 제한합니다.

3. 중요 사항

  • Null 값 허용되지 않음: Sort, Pageable, Limit을 파라미터로 사용한 쿼리 메서드에게 아규먼트로 null 값을 전달해서는 안 됩니다. 만약 페이징, 정렬 또는 제한을 적용하고 싶지 않다면 Sort.unsorted(), Pageable.unpaged(), Limit.unlimited()를 사용해야 합니다.
  • 카운트 쿼리: 전체 쿼리의 페이지 수를 알아내기 위해서는 추가적인 count 쿼리를 실행해야 합니다. 이 쿼리는 실제 트리거된 쿼리에서 자동으로 유도되지만, 성능에 영향을 미칠 수 있습니다.

4. 파라미터 사용 규칙

  • Pageable과 Sort는 함께 사용할 수 없습니다. 이는 Pageable이 이미 정렬(Sort)을 정의하고 있기 때문입니다.
  • Pageable과 Limit도 함께 사용할 수 없습니다. Pageable이 이미 제한(Limit) 값을 정의하고 있기 때문입니다.
Parameters Example Reason
Pageable and Sort findBy…​(Pageable page, Sort sort) Pageable already defines Sort
Pageable and Limit findBy…​(Pageable page, Limit limit) Pageable already defines a limit.

 

5. Top 키워드와 Pageable의 조합

Top 키워드는 쿼리 결과의 최대 개수를 제한하는데 사용되며, 이를 Pageable과 함께 사용할 수 있습니다. 이때 Top은 결과의 최대 수를 정의하고, Pageable 파라미터는 이 수를 더 줄일 수 있습니다.

 

이렇게 페이징, 정렬, 제한 기능을 사용하면 대규모 데이터 처리에서 쿼리 성능을 최적화할 수 있으며, 비즈니스 로직에 필요한 형태로 결과를 동적으로 구성할 수 있습니다.

Which Method is Appropriate?

이 내용은 Spring Data JPA에서 쿼리 메서드의 결과를 처리하는 다양한 방법을 설명하며, 각각의 메서드가 어떤 상황에 적합한지에 대한 정보를 제공합니다. 각 메서드의 특성, 데이터 처리 방식, 제약 사항을 기반으로 대용량 쿼리 결과를 처리하는 적절한 방법을 선택할 수 있습니다.

1. List

  • 데이터 처리 방식: 모든 결과를 한 번에 가져옵니다.
  • 쿼리 구조: 단일 쿼리로 데이터를 조회합니다.
  • 제약 사항:
    • 메모리가 충분하지 않을 경우 쿼리 결과가 메모리를 모두 소진할 수 있습니다.
    • 대량의 데이터를 한 번에 처리할 경우 시간이 많이 소요될 수 있습니다.

2. Streamable

  • 데이터 처리 방식: 모든 결과를 한 번에 가져옵니다.
  • 쿼리 구조: 단일 쿼리로 데이터를 조회합니다.
  • 제약 사항:
    • List<T>와 동일하게 메모리 소모가 큽니다.
    • 대량의 데이터를 처리하는 데 시간이 많이 소요될 수 있습니다.
    • Streamable은 스트림처럼 데이터를 편리하게 처리할 수 있지만 메모리 상에서 모든 결과를 보유하므로 큰 데이터 셋에서는 주의가 필요합니다.

3. Stream

  • 데이터 처리 방식: 데이터를 하나씩 또는 배치로 처리하며, Stream을 소비하는 방식에 따라 달라집니다.
  • 쿼리 구조: 주로 커서를 사용하여 단일 쿼리로 데이터를 가져옵니다.
  • 제약 사항:
    • 스트림을 사용한 후에는 반드시 close()를 호출하여 자원 누수를 방지해야 합니다. 그렇지 않으면 메모리 누수 등의 문제가 발생할 수 있습니다.
    • try-with-resources 블록을 사용해 스트림을 자동으로 닫는 것이 일반적입니다.

4. Flux

  • 데이터 처리 방식: 데이터를 하나씩 또는 배치로 처리하며, Flux를 소비하는 방식에 따라 달라집니다.
  • 쿼리 구조: 주로 커서를 사용하여 단일 쿼리로 데이터를 가져옵니다.
  • 제약 사항:
    • Reactive Infrastructure를 제공하는 스토어 모듈이 필요합니다. 즉, 데이터베이스나 시스템이 리액티브 프로그래밍을 지원해야 합니다.
    • 리액티브 프로그래밍을 통해 효율적인 비동기 처리가 가능하지만, 모든 시스템에서 지원되는 것은 아니므로 환경에 맞게 사용해야 합니다.

5. Slice

  • 데이터 처리 방식: Pageable.getOffset()에서 시작해 Pageable.getPageSize()보다 하나 더 많은 데이터를 가져옵니다.
  • 쿼리 구조: 데이터를 나누어 페이징하며, 여러 번의 쿼리를 통해 데이터를 조회할 수 있습니다.
  • 제약 사항:
    • 한 번에 다음 슬라이스(Slice)로만 이동할 수 있습니다. 즉, 뒤로 돌아가거나 이전 페이지로 이동하는 기능이 없습니다.
    • 결과에 더 가져올 데이터가 있는지 여부를 알려줍니다.
    • Offset 기반 쿼리는 오프셋 값이 커질수록 비효율적일 수 있습니다. 데이터베이스가 전체 결과를 물리적으로 처리해야 하기 때문입니다.

6. Page

  • 데이터 처리 방식: Pageable.getOffset()에서 시작해 Pageable.getPageSize()만큼의 데이터를 가져옵니다.
  • 쿼리 구조: 페이징을 통해 여러 번의 쿼리를 수행하며, 총 데이터 수를 계산하기 위해 추가로 COUNT(…) 쿼리가 필요할 수 있습니다.
  • 제약 사항:
    • COUNT(…) 쿼리가 자주 필요한 경우 성능에 부정적인 영향을 줄 수 있습니다.
    • Offset 기반 쿼리가 오프셋 값이 커질수록 비효율적으로 변합니다. 데이터베이스가 전체 결과를 물리적으로 처리해야 하기 때문입니다.

Consuming Large Query Results

Method Amount of Data Fetched Query Structure Constraints
List<T> All results. Single query. Query results can exhaust all memory. Fetching all data can be time-intensive.
Streamable<T> All results. Single query. Query results can exhaust all memory. Fetching all data can be time-intensive.
Stream<T> Chunked (one-by-one or in batches) depending on Stream consumption. Single query using typically cursors. Streams must be closed after usage to avoid resource leaks.
Flux<T> Chunked (one-by-one or in batches) depending on Flux consumption. Single query using typically cursors. Store module must provide reactive infrastructure.
Slice<T> Pageable.getPageSize() + 1 at Pageable.getOffset() One to many queries fetching data starting at Pageable.getOffset() applying limiting.
A Slice can only navigate to the next Slice.
  • Slice provides details whether there is more data to fetch.
  • Offset-based queries becomes inefficient when the offset is too large because the database still has to materialize the full result.
  • Window provides details whether there is more data to fetch.
  • Offset-based queries becomes inefficient when the offset is too large because the database still has to materialize the full result.
Page<T> Pageable.getPageSize() at Pageable.getOffset() One to many queries starting at Pageable.getOffset() applying limiting. Additionally, COUNT(…) query to determine the total number of elements can be required.
Often times, COUNT(…) queries are required that are costly.
  • Offset-based queries becomes inefficient when the offset is too large because the database still has to materialize the full result.

 

이 표는 다양한 상황에서 대용량 데이터를 어떻게 처리할지 결정하는 데 도움을 줍니다. 예를 들어, 대량의 데이터를 한 번에 가져오는 것이 부담이 되는 경우 Stream이나 Slice를 사용하는 것이 더 적합할 수 있습니다. 반면, 정확한 총 데이터 개수와 페이지 수를 알아야 하는 경우에는 Page를 사용하는 것이 필요합니다.

Paging and Sorting

이 내용은 Spring Data에서 페이징과 정렬을 다루는 방법에 대한 설명입니다. 정렬은 데이터를 특정 속성에 따라 정렬하는 기능으로, 다양한 방식으로 정의할 수 있으며, 코드 내에서 간단하게 표현하거나 더 타입-안전한 방식으로도 사용할 수 있습니다.

1. 단순한 정렬 표현 정의

가장 기본적인 방법은 Sort 클래스를 사용하여 속성 이름을 기반으로 정렬 기준을 정의하는 것입니다.

Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());
  • 이 예시에서, firstname 필드를 오름차순으로 정렬한 후, lastname 필드를 내림차순으로 정렬합니다.
  • Sort.by(…) 메서드를 사용해 각 속성을 지정하고, ascending() 또는 descending() 메서드로 정렬 순서를 설정할 수 있습니다.

2. 타입-안전한 정렬 표현 정의

TypedSort를 사용하면, 속성 이름을 문자열로 정의하지 않고 메서드 참조를 통해 더 타입-안전한 방식으로 정렬 표현을 만들 수 있습니다. 이는 컴파일 시점에 속성 이름의 오타 등을 방지해 줍니다.

TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());
  • TypedSort는 타입에 안전하게 접근할 수 있는 API입니다. 여기서 Person 클래스의 속성인 getFirstname()getLastname()에 메서드 참조를 사용합니다.
  • TypedSort를 사용하여 Person 클래스의 속성으로 정렬 기준을 정의하고, ascending() 또는 descending()으로 정렬 방향을 설정합니다.

주의 사항

  • TypedSort.by(…)는 런타임 프록시(주로 CGlib)를 사용합니다. 이는 GraalVM Native와 같은 네이티브 이미지 컴파일러를 사용할 때 충돌이 발생할 수 있으므로 주의해야 합니다.

3. Querydsl API를 사용한 정렬 표현 정의

만약 사용하는 데이터 저장소가 Querydsl을 지원하는 경우, 자동으로 생성된 메타모델 타입을 사용하여 더 정교한 정렬 표현을 정의할 수 있습니다.

QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));
  • Querydsl을 사용할 때, QPerson 같은 클래스는 메타모델 타입으로 자동 생성됩니다. 이 메타모델을 사용해 필드를 참조하고, asc()desc() 메서드로 정렬 방향을 정의할 수 있습니다.
  • QSort는 Querydsl에서 제공하는 정렬을 정의하는 클래스입니다.

정리

  • Sort는 속성 이름을 사용해 간단하게 정렬할 수 있는 도구입니다.
  • TypedSort는 메서드 참조를 사용하여 Type-Safed 방식으로 정렬을 정의합니다.
  • Querydsl은 메타모델 타입을 사용해 보다 복잡한 정렬 표현을 지원합니다.

이러한 방식으로 Spring Data는 다양한 정렬 옵션을 제공하며, 각각의 상황에 맞는 방법을 선택해 사용할 수 있습니다.

 

Limiting Query Results

이 내용은 Spring Data에서 쿼리 결과를 제한하는 방법을 설명하고 있습니다. 쿼리 결과를 페이징과 함께 처리하는 대신, 특정 크기로 제한하는 기능을 제공합니다. 이를 위해 Limit 파라미터나 FirstTop 키워드를 사용하여 쿼리 결과의 크기를 제한할 수 있습니다. 아래에 이 기능에 대해 자세히 설명하겠습니다.

1. Limit 파라미터를 사용한 결과 크기 제한

Limit 파라미터를 사용하여 쿼리의 결과 크기를 제한할 수 있습니다.

List<User> findByLastname(Limit limit);
  • 이 메서드는 Limit 파라미터를 받아, 해당 제한에 따라 User 엔티티 리스트를 반환합니다.
  • Limit는 쿼리의 결과 개수를 직접 지정할 때 사용됩니다.

2. First와 Top 키워드를 사용한 결과 크기 제한

FirstTop 키워드는 결과 크기를 제한하는 데 사용되며, 서로 교체하여 사용할 수 있습니다. 두 키워드는 동일하게 동작하며, 결과의 최대 크기를 지정할 수 있습니다. 숫자를 지정하지 않으면 기본값은 1입니다.

User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
  • findFirstByOrderByLastnameAsc()lastname 필드에 대해 오름차순 정렬 후 첫 번째 User를 반환합니다.
  • findTopByOrderByAgeDesc()age 필드에 대해 내림차순 정렬 후 첫 번째 User를 반환합니다.

숫자를 명시하여 상위 n개의 결과를 반환할 수도 있습니다.

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
  • queryFirst10ByLastname()lastname 필드로 검색하고 첫 10개의 User를 페이징 결과로 반환합니다.
  • findTop3ByLastname()lastname 필드로 검색하고 첫 3개의 User를 슬라이스 형태로 반환합니다.
  • FirstTop 키워드는 동적으로 페이징하거나 정렬할 때 함께 사용할 수 있으며, 해당 키워드를 통해 결과 제한을 할 수 있습니다.

3. Distinct와 Optional 키워드 지원

결과 제한을 적용할 때, 쿼리 결과에서 중복을 제거하는 Distinct 키워드를 사용할 수 있습니다. 또한, 쿼리 결과를 하나의 인스턴스로 제한할 때는 결과를 Optional로 감싸서 반환할 수 있습니다.

  • Distinct는 지원하는 데이터 저장소에서 중복을 제거한 쿼리를 실행할 수 있습니다.
  • 하나의 결과를 반환하는 경우, 그 결과를 Optional로 처리할 수 있습니다.

4. 페이징 및 슬라이싱과의 조합

LimitFirst, Top 키워드를 사용한 쿼리에서 Pageable 또는 Slice와 같은 페이징이 적용된 경우, 결과가 제한된 범위 내에서만 페이징이 적용됩니다.

  • 쿼리 결과를 제한한 상태에서 페이징을 적용하면, 전체 결과 중 제한된 범위 내에서 페이지 수를 계산합니다.

5. 정렬과 함께 제한 적용

정렬과 결합하여 제한된 쿼리 결과를 반환할 수 있으며, 이를 통해 'K개의 가장 작은 값' 또는 'K개의 가장 큰 값'을 표현하는 쿼리 메서드를 작성할 수 있습니다.

List<User> findFirst10ByLastname(String lastname, Sort sort);
  • 예를 들어, findFirst10ByLastname()lastname 필드를 기준으로 첫 10개의 결과를 지정된 Sort에 따라 반환할 수 있습니다.

정리

  • Limit 파라미터는 결과 크기를 제한하는 전용 파라미터입니다.
  • FirstTop 키워드는 숫자를 명시하지 않으면 기본적으로 첫 번째 결과만 반환하며, 숫자를 명시하면 그만큼의 결과를 반환합니다.
  • DistinctOptional을 함께 사용하여 중복을 제거하거나, 결과가 없을 때 안전하게 처리할 수 있습니다.
  • 정렬과 페이징 기능을 결합하여 더 정교한 쿼리 메서드를 정의할 수 있습니다.

이를 통해 Spring Data에서는 다양한 방식으로 쿼리 결과를 제한하고, 효율적으로 데이터를 처리할 수 있도록 지원하고 있습니다.