1. 개요

이 예제에서는 Spring Data JPA 로 검색된 대규모 데이터 세트를 반복하는 다양한 방법을 탐색 할 것 입니다.

먼저 페이지가 매겨진 쿼리를 사용하고 SlicePage 의 차이점을 살펴보겠습니다 . 그런 다음 데이터베이스에서 데이터를 수집하지 않고 스트리밍하고 처리하는 방법을 배웁니다.

2. 페이지를 매긴 쿼리

이 상황에 대한 일반적인 접근 방식은 페이지가 매겨진 쿼리 를 사용하는 것 입니다. 이렇게 하려면 배치 크기를 정의하고 여러 쿼리를 실행해야 합니다 . 결과적으로 모든 엔터티를 더 작은 배치로 처리하고 메모리에 많은 양의 데이터를 로드하는 것을 방지할 수 있습니다.

2.1 슬라이스를 사용한 페이지 매김

이 문서의 코드 예제에서는 Student 엔터티를 데이터 모델로 사용합니다.

@Entity
public class Student {

    @Id
    @GeneratedValue
    private Long id;

    private String firstName;
    private String lastName;

    // consturctor, getters and setters

}

모든 학생을 firstName 으로 쿼리하는 메서드를 추가해 보겠습니다 . Spring Data JPA를 사용하면 Pageable 을 매개변수로 수신하고 Slice 를 반환 하는 메소드를 JpaRepository 에 추가하기만 하면 됩니다 .

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
    Slice<Student> findAllByFirstName(String firstName, Pageable page);
}

반환 유형이 Slice<Student> 임을 알 수 있습니다 . Slice 개체를 사용하면 Student 엔터티 의 첫 번째 배치를 처리할 수 있습니다. 슬라이스 객체는 처리 중인 배치가 결과 세트의 마지막 배치인지 확인할 수 있는 hasNext ( ) 메서드를 노출합니다.

또한 nextPageable() 메서드를 사용하여 한 슬라이스에서 다음 슬라이스로 이동할 수 있습니다 . 이 메서드는 다음 슬라이스를 요청하는 데 필요한 Pageable 개체를 반환합니다. 따라서 while 루프 내에서 두 가지 방법을 조합하여 슬라이스별로 모든 데이터를 검색할 수 있습니다 .

void processStudentsByFirstName(String firstName) {
    Slice<Student> slice = repository.findAllByFirstName(firstName, PageRequest.of(0, BATCH_SIZE));
    List<Student> studentsInBatch = slice.getContent();
    studentsInBatch.forEach(emailService::sendEmailToStudent);

    while(slice.hasNext()) {
        slice = repository.findAllByFirstName(firstName, slice.nextPageable());
        slice.get().forEach(emailService::sendEmailToStudent);
    }
}

작은 배치 크기를 사용하여 간단한 테스트를 실행하고 SQL 문을 따라가 보겠습니다. 여러 쿼리가 실행될 것으로 예상합니다.

[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.first_name=? limit ?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.first_name=? limit ? offset ?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.first_name=? limit ? offset ?

2.2. 페이지를 사용한 페이지 매김

Slice <> 의 대안으로 Page<> 를 쿼리의 반환 유형으로 사용할 수도 있습니다.

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
    Slice<Student> findAllByFirstName(String firstName, Pageable page);
    Page<Student> findAllByLastName(String lastName, Pageable page);
}

Page 인터페이스 Slice를 확장하여 getTotalPages()getTotalElements() 의 두 가지 다른 메서드를 추가합니다 .

Page s는 페이지가 매겨진 데이터가 네트워크를 통해 요청될 때 반환 유형으로 자주 사용됩니다. 이렇게 하면 호출자는 남은 행 수와 필요한 추가 요청 수를 정확히 알 수 있습니다.

반면에 Page s를 사용하면 기준을 충족하는 행을 계산하는 추가 쿼리가 발생합니다.

[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.last_name=? limit ?
[main] DEBUG org.hibernate.SQL - select count(student0_.id) as col_0_0_ from student student0_ where student0_.last_name=?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.last_name=? limit ? offset ?
[main] DEBUG org.hibernate.SQL - select count(student0_.id) as col_0_0_ from student student0_ where student0_.last_name=?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.last_name=? limit ? offset ?

따라서 총 엔터티 수를 알아야 하는 경우에만 Page<>를 반환 유형으로 사용해야 합니다.

3. 데이터베이스에서 스트리밍

Spring Data JPA를 사용하면 결과 세트에서 데이터를 스트리밍할 수도 있습니다.

Stream<Student> findAllByFirstName(String firstName);

결과적으로 우리는 동시에 메모리에 로드하지 않고 엔터티를 하나씩 처리합니다. 그러나 try-with-resource 블록 을 사용하여 Spring Data JPA에서 생성한 스트림을 수동으로 닫아야 합니다 . 또한 쿼리를 읽기 전용 트랜잭션으로 래핑해야 합니다.

마지막으로 행을 하나씩 처리하더라도 지속성 컨텍스트가 모든 엔터티에 대한 참조를 유지하지 않도록 해야 합니다. 스트림을 사용하기 전에 엔터티를 수동으로 분리하여 이를 달성할 수 있습니다.

private final EntityManager entityManager;

@Transactional(readOnly = true)
public void processStudentsByFirstNameUsingStreams(String firstName) {
    try (Stream<Student> students = repository.findAllByFirstName(firstName)) {
        students.peek(entityManager::detach)
            .forEach(emailService::sendEmailToStudent);
    }
}

4. 결론

이 기사에서는 대규모 데이터 세트를 처리하는 다양한 방법을 살펴보았습니다. 처음에는 페이지가 매겨진 여러 쿼리를 통해 이를 달성했습니다. 호출자가 요소의 총 수를 알아야 하는 경우 Page<> 를 반환 유형으로 사용하고 그렇지 않은 경우 Slice<> 를 사용해야 한다는 것을 배웠습니다 . 그런 다음 데이터베이스에서 행을 스트리밍하고 개별적으로 처리하는 방법을 배웠습니다.

항상 그렇듯이 코드 샘플은 GitHub 에서 찾을 수 있습니다 .

Persistence footer banner