https://sundaland.tistory.com/411
[ ▶ Paging, Iterating Large Results, Sorting & Limiting ]
이 내용은 Spring Data JPA에서 쿼리 메서드에 Paging, Sorting, Limiting 등의 기능을 적용하는 방법을 설명하고 있다. 구체적으로 Pageable, Sort, Limit와 같은 특정 타입의 파라미터를 사용하여 쿼리 결과를 동적으로 처리하는 방법을 다룬다.
[ ▷ 페이징, 정렬, 제한을 적용한 쿼리 메서드 사용 예시 ]
▼ 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);
[ ▷ 특정 파라미터 설명 ]
- Pageable: org.springframework.data.domain.Pageable 인스턴스를 사용해 쿼리에 페이징을 동적으로 추가할 수 있다. Page 객체는 전체 엘리먼트 수와 페이지 수에 대한 정보를 포함하고 있어, 결과에 대한 총 개수를 알려준다. 하지만, 총 개수를 계산하기 위해서는 별도의 count 쿼리가 실행되며, 데이터 저장소에 따라 이 작업이 비싼 연산이 될 수 있다.
- Slice: Slice는 결과 세트에서 다음 슬라이스(Slice)가 있는지 여부만 알 수 있다. 대규모 데이터 셋을 다룰 때는 Page보다 성능 면에서 유리할 수 있다. [ https://blank001.tistory.com/142 ]
- Sort: 쿼리 메서드에서 정렬 옵션을 적용하고자 할 때 사용한다. 정렬이 필요할 때 Sort 파라미터를 메서드에 추가하여 동적으로 처리할 수 있다.
- Limit: 쿼리에서 조회할 결과의 수를 제한하고자 할 때 사용된다. Limit은 List 결과를 반환할 때 쿼리가 조회하는 엔티티 수를 제한한다.
[ ▷ 중요 사항 ]
- Null 값 허용되지 않음: Sort, Pageable, Limit을 파라미터로 사용한 쿼리 메서드에게 아규먼트로 null 값을 전달해서는 안 된다. 만약 페이징, 정렬 또는 제한을 적용하고 싶지 않다면 Sort.unsorted(), Pageable.unpaged(), Limit.unlimited()를 사용해야 한다.
- 카운트 쿼리: 전체 쿼리의 페이지 수를 알아내기 위해서는 추가적인 count 쿼리를 실행해야 한다. 이 쿼리는 실제 트리거된 쿼리에서 자동으로 유도되지만, 성능에 영향을 미칠 수 있다.
[ ▷ 파라미터 사용 규칙 ]
- 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. |
[ ▷ Top 키워드와 Pageable의 조합 ]
Top 키워드는 쿼리 결과의 최대 개수를 제한하는데 사용되며, 이를 Pageable과 함께 사용할 수 있다. 이때 Top은 결과의 최대 수를 정의하고, Pageable 파라미터는 이 수를 더 줄일 수 있다.
이렇게 페이징, 정렬, 제한 기능을 사용하면 대규모 데이터 처리에서 쿼리 성능을 최적화할 수 있으며, 비즈니스 로직에 필요한 형태로 결과를 동적으로 구성할 수 있다.
[ ▷ Which Method is Appropriate? ]
이 내용은 Spring Data JPA에서 쿼리 메서드의 결과를 처리하는 다양한 방법을 설명하며, 각각의 메서드가 어떤 상황에 적합한지에 대한 정보를 제공한다. 각 메서드의 특성, 데이터 처리 방식, 제약 사항을 기반으로 대용량 쿼리 결과를 처리하는 적절한 방법을 선택할 수 있다.
△ List
- 데이터 처리 방식: 모든 결과를 한 번에 가져온다.
- 쿼리 구조: 단일 쿼리로 데이터를 조회한다.
- 메모리가 충분하지 않을 경우 쿼리 결과가 메모리를 모두 소진할 수 있다.
- 대량의 데이터를 한 번에 처리할 경우 시간이 많이 소요될 수 있다.
△ Streamable
- 데이터 처리 방식: 모든 결과를 한 번에 가져온다.
- 쿼리 구조: 단일 쿼리로 데이터를 조회한다.
- List<T>와 동일하게 메모리 소모가 크다.
- 대량의 데이터를 처리하는 데 시간이 많이 소요될 수 있다.
- Streamable은 스트림처럼 데이터를 편리하게 처리할 수 있지만 메모리 상에서 모든 결과를 보유하므로 큰 데이터 셋에서는 주의가 필요하다.
△ Stream
- 데이터 처리 방식: 데이터를 하나씩 또는 배치로 처리하며, Stream을 소비하는 방식에 따라 달라진다.
- 쿼리 구조: 주로 커서를 사용하여 단일 쿼리로 데이터를 가져온다.
- 스트림을 사용한 후에는 반드시 close()를 호출하여 자원 누수를 방지해야 한다. 그렇지 않으면 메모리 누수 등의 문제가 발생할 수 있다.
- try-with-resources 블록을 사용해 스트림을 자동으로 닫는 것이 일반적이다.
△ Flux
- 데이터 처리 방식: 데이터를 하나씩 또는 배치로 처리하며, Flux를 소비하는 방식에 따라 달라진다.
- 쿼리 구조: 주로 커서를 사용하여 단일 쿼리로 데이터를 가져온다.
- Reactive Infrastructure를 제공하는 스토어 모듈이 필요하다. 즉, 데이터베이스나 시스템이 리액티브 프로그래밍을 지원해야 한다.
- 리액티브 프로그래밍을 통해 효율적인 비동기 처리가 가능하지만, 모든 시스템에서 지원되는 것은 아니므로 환경에 맞게 사용해야 한다.
△ Slice
- 데이터 처리 방식: Pageable.getOffset()에서 시작해 Pageable.getPageSize()보다 하나 더 많은 데이터를 가져온다.
- 쿼리 구조: 데이터를 나누어 페이징하며, 여러 번의 쿼리를 통해 데이터를 조회할 수 있다.
- 한 번에 다음 슬라이스(Slice)로만 이동할 수 있다. 즉, 뒤로 돌아가거나 이전 페이지로 이동하는 기능이 없다.
- 결과에 더 가져올 데이터가 있는지 여부를 알려준다.
- Offset 기반 쿼리는 오프셋 값이 커질수록 비효율적일 수 있다. 데이터베이스가 전체 결과를 물리적으로 처리해야 하기 때문이다.
△ 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.
|
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.
|
이 표는 다양한 상황에서 대용량 데이터를 어떻게 처리할지 결정하는 데 도움을 준다. 예를 들어, 대량의 데이터를 한 번에 가져오는 것이 부담이 되는 경우 Stream이나 Slice를 사용하는 것이 더 적합할 수 있다. 반면, 정확한 총 데이터 개수와 페이지 수를 알아야 하는 경우에는 Page를 사용하는 것이 필요하다.
[ ▷ Paging and Sorting ]
이 내용은 Spring Data에서 페이징과 정렬을 다루는 방법에 대한 설명이다. 정렬은 데이터를 특정 속성에 따라 정렬하는 기능으로, 다양한 방식으로 정의할 수 있으며, 코드 내에서 간단하게 표현하거나 더 타입-안전한 방식으로도 사용할 수 있다.
[ ▷ 단순한 정렬 표현 정의 ]
가장 기본적인 방법은 Sort 클래스를 사용하여 속성 이름을 기반으로 정렬 기준을 정의하는 것이다.
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
- 이 예시에서, firstname 필드를 오름차순으로 정렬한 후, lastname 필드를 내림차순으로 정렬한다.
- Sort.by(…) 메서드를 사용해 각 속성을 지정하고, ascending() 또는 descending() 메서드로 정렬 순서를 설정할 수 있다.
[ ▷ 타입-안전한 정렬 표현 정의 ]
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와 같은 네이티브 이미지 컴파일러를 사용할 때 충돌이 발생할 수 있으므로 주의해야 한다.
[ ▷ 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 파라미터나 First와 Top 키워드를 사용하여 쿼리 결과의 크기를 제한할 수 있다.
[ ▷ Limit 파라미터를 사용한 결과 크기 제한 ]
Limit 파라미터를 사용하여 쿼리의 결과 크기를 제한할 수 있다.
List<User> findByLastname(Limit limit);
- 이 메서드는 Limit 파라미터를 받아, 해당 제한에 따라 User 엔티티 리스트를 반환한다.
- Limit는 쿼리의 결과 개수를 직접 지정할 때 사용된다.
[ ▷ First와 Top 키워드를 사용한 결과 크기 제한 ]
First와 Top 키워드는 결과 크기를 제한하는 데 사용되며, 서로 교체하여 사용할 수 있다. 두 키워드는 동일하게 동작하며, 결과의 최대 크기를 지정할 수 있다. 숫자를 지정하지 않으면 기본값은 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를 슬라이스 형태로 반환한다.
- First와 Top 키워드는 동적으로 페이징하거나 정렬할 때 함께 사용할 수 있으며, 해당 키워드를 통해 결과 제한을 할 수 있다.
[ ▷ Distinct와 Optional 키워드 지원 ]
결과 제한을 적용할 때, 쿼리 결과에서 중복을 제거하는 Distinct 키워드를 사용할 수 있다. 또한, 쿼리 결과를 하나의 인스턴스로 제한할 때는 결과를 Optional로 감싸서 반환할 수 있다.
- Distinct는 지원하는 데이터 저장소에서 중복을 제거한 쿼리를 실행할 수 있다.
- 하나의 결과를 반환하는 경우, 그 결과를 Optional로 처리할 수 있다.
[ ▷ 페이징 및 슬라이싱과의 조합 ]
Limit와 First, Top 키워드를 사용한 쿼리에서 Pageable 또는 Slice와 같은 페이징이 적용된 경우, 결과가 제한된 범위 내에서만 페이징이 적용된다.
- 쿼리 결과를 제한한 상태에서 페이징을 적용하면, 전체 결과 중 제한된 범위 내에서 페이지 수를 계산한다.
[ ▷ 정렬과 함께 제한 적용 ]
정렬과 결합하여 제한된 쿼리 결과를 반환할 수 있으며, 이를 통해 'K개의 가장 작은 값' 또는 'K개의 가장 큰 값'을 표현하는 쿼리 메서드를 작성할 수 있다.
List<User> findFirst10ByLastname(String lastname, Sort sort);
- 예를 들어, findFirst10ByLastname()는 lastname 필드를 기준으로 첫 10개의 결과를 지정된 Sort에 따라 반환할 수 있다.
[ ▷ 정리 ]
- Limit 파라미터는 결과 크기를 제한하는 전용 파라미터다.
- First와 Top 키워드는 숫자를 명시하지 않으면 기본적으로 첫 번째 결과만 반환하며, 숫자를 명시하면 그만큼의 결과를 반환한다.
- Distinct와 Optional을 함께 사용하여 중복을 제거하거나, 결과가 없을 때 안전하게 처리할 수 있다.
- 정렬과 페이징 기능을 결합하여 더 정교한 쿼리 메서드를 정의할 수 있다.
이를 통해 Spring Data에서는 다양한 방식으로 쿼리 결과를 제한하고, 효율적으로 데이터를 처리할 수 있도록 지원하고 있다.
'Spring Boot > Spring Data JPA' 카테고리의 다른 글
Using JPA Named Queries (0) | 2024.10.24 |
---|---|
Query Lookup Strategies (0) | 2024.10.24 |
Slice (0) | 2024.10.24 |
Repository Methods Returning Collections or Iterables (0) | 2024.10.24 |
Property Expressions (0) | 2024.10.24 |