Spring Data JPA Stream으로 대용량 데이터 조회하기 (100만 건 이상)
Java 8 에서부터 제공되는 Stream을 Srping Data 1.8
부터 지원하게 되면서, JPA를 통해 100만 건 이상의 대용량 데이터 효율적으로 조회할 수 있게 되었다.
JPA가 Stream을 지원하기 이전에는 대용량 데이터 조회를 위해 아래와 같은 방법을 사용하였다.
이 포스팅은 하단 참고문헌에 링크해둔 글을 이해하고 학습하기 위해 의역하며 샘플코드를 작성하였다.
정확한 정보 슥듭을 확인하고 싶으신 분들은 아래 링크를 꼭 확인하도록 권한다.
High-performance data fetching using Spring Data JPA Stream
기존에 대용량 데이터 조회를 위한 방법들
1. 한번에 조회하여 메모리에 올려두기
이 방법은 작은 규모의 데이터베이스의 경우 간단하게 적용 가능한 방법이다. 조회한 데이터를 메모리에 롤리고 간단한 캐싱 처리(memoize)나 lazy load 등을 적용하면 된다. 그러나 100 만 건 이상의 데이터를 조회하는 경우 메모리 이슈가 발생하게 된다. 그럴 경우 어플리케이션 서버와 분리된 캐싱 서버(Redis, Hazelcast)를 활용하면 되는데, 단순히 일회성 조회하여 간단(?)처리를 원하는 경우 캐싱 서버를 활용하는 것은 적합하지 않다.
2. 페이징 처리
첫번째 방법의 경우 메모리 이슈가 발생할 수 있기 때문에 페이징 처리를 통해 적당한 수의 데이터를 쪼개서 페이징 처리 하는 방법이 있다. Spring Data가 지원하고 있기 때문에 JpaRepository
상속을 통해 간단히 적용할 수 있다. 하지만 페이징 처리 시 count 조회 쿼리가 매번 일어나기 때문에 성능 이슈가 발생할 수 있다. 이를 해결하기 위해 return type을 Slice
로 변경하면 되는데, 문제는 이게 끝이 아니다. 시간이 흐르면서 더욱 많은 데이터가 쌓이는데, 페이징 처리는 대용량 데이터베이스에서 느린 처리 속도를 보이기 때문에 이 방법도 좋은 선택은 아니다.
3. Apache Spark
대용량 데이터를 다루기 위해 Apach Spark는 적절한 방법이다. Spark는 탄력적이고 고가용성이 특징이다. 한번 설정하고 나면 시스템 내에서 대용량 처리를 수월하게 할 수 있다. 하지만 Spark를 설정하고 동작하는 방법에 대해 공부하기 위한 러닝커브가 필요하므로 정말 필요한 대규모의 시스템이 아니라면 섣불리 적용하기 어려울 수 있다.
대용량 데이터 조회, 그런데 이제 Spring Data JPA Stream를 곁들인
Spark나 다른 대용량 처리 도구를 적용하지 않는다면, 마지막 옵션은 Spring Data JPA Stream이다. 적용 방법은 Spring 공식문서를 참고하였다.
Stream 사용 시 주의할 점은 아래와 같다.
- Spring Data 1.8 버전 이상
- 리턴타입은 Stream,
@Query
혹은@QueryHints
어노테이션 사용 @Transactional(readOnly = true)
- 사용이 끝난 Entity는 detach하여 메모리에서 제거
Stream을 이용한 코드는 아래와 같다.
Entity
@Getter
@NoArgsConstructor
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Builder
public Product(String name) {
this.name = name;
}
}
Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("select p from Product p")
Stream<Product> streamAll();
}
Service
@RequiredArgsConstructor
@Service
public class ProductService {
@PersistenceContext
private EntityManager entityManager;
private final ProductRepository repository;
@Transactional(readOnly = true)
public void orderAll() {
Stream<Product> products = repository.streamAll();
products.forEach( product -> {
// 제품에 대한 생산 요청 API를 호출한다.
// ...
// 메모리에 올라간 Entity를 GC이 클리어할 수 있도록 풀어둠
entityManager.detach(product); //JPA 2.0, to detach a single entity from persistence context
});
}
}
만약 사용이 끝난 entity를 detach 하지 않는다면, 메모리 힙 사이즈에 따라 out of memory exception이 발생한다. 각 entity를 처리한 후에는 detach를 하여 가비지콜렉터(Garbage Collecotr)가 메모리를 정리할 수 있게 해주자.
github 예제 코드 보기 : here
참고자료
'데이터 관리 > Spring data(JPA)' 카테고리의 다른 글
JPA 자식 엔티티가 변경되지 않을 때 (feat. cascade) (0) | 2021.02.05 |
---|---|
JPA @ElementCollection (0) | 2021.01.29 |
[JPA Error] IllegalStateException: Multiple representations of the same entity (0) | 2021.01.20 |
Hibernate에서 제공되지 않는 DB 종속 함수 사용(mysql group_concat) (0) | 2020.11.19 |
ORM 뉴비가 바라본 JPA 학습 방법 (0) | 2020.09.13 |
댓글
이 글 공유하기
다른 글
-
JPA 자식 엔티티가 변경되지 않을 때 (feat. cascade)
JPA 자식 엔티티가 변경되지 않을 때 (feat. cascade)
2021.02.05 -
JPA @ElementCollection
JPA @ElementCollection
2021.01.29 -
[JPA Error] IllegalStateException: Multiple representations of the same entity
[JPA Error] IllegalStateException: Multiple representations of the same entity
2021.01.20 -
Hibernate에서 제공되지 않는 DB 종속 함수 사용(mysql group_concat)
Hibernate에서 제공되지 않는 DB 종속 함수 사용(mysql group_concat)
2020.11.19