컬렉션 조회 최적화
주문내역에서 추가로 주문한 상품 정보를 추가로 조회하자.
Order 기준으로 컬렉션인 OrderItem 와 Item 이 필요하다.
OneToOne이나 ManyToOne은 페치 조인을 통해 한 번에 조회해도 크게 성능이 떨어지지 않는데
OneToMany의 경우, join하면 데이터가 증가하는 현상 -> 최적화 필요
주문 조회 V1: 엔티티 직접 노출
orderItem , item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성한다.
*Hibernate5Module: 지연 로딩에 의해 프록시가 초기화된 데이터만 api를 통해 반환
양방향 연관관계: 무한 루프에 걸리지 않게 한곳에 @JsonIgnore 를 추가
주문 조회 V2: 엔티티를 DTO로 변환
DTO 변환: 필드에 엔티티를 포함(의존)하지 않는다.
엔티티 컬렉션도 DTO 컬렉션으로 변환. DTO는 엔티티 단위로 생성
*임베디드(value) 타입은 반환해도 OK
지연 로딩으로 너무 많은 SQL 실행
주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
리포지토리에 페치 조인을 통한 조회 메서드 추가 (컨트롤러의 코드는 V2와 V3이 같음)
일대다 연관관계 join에서 중복 데이터 증가 (in DB, 다(N) 기준으로 row 생성)
*fetch join: select 절에 열 데이터 추가하여 한 쿼리에 여러 개의 join 수행
JPA의 distinct 키워드 사용하여 중복 제거
DB의 distinct 키워드는 row가 완전히 똑같아야 제거해주는데, JPA의 distinct는 sql에 distinct 추가 + pk 값이 같은 엔티티 제거
"select distinct o from Order o"
+ " join fetch o.member m"
+ " join fetch o.delivery d"
+ " join fetch o.orderItems oi"
+ " join fetch oi.item i"
페이징 불가능
일대다 연관관계에서 페치 조인을 하는 순간, DB상에서 데이터가 증가하면서 페이징의 의미가 사라진다.
FirstResult(offset), MaxResults(limit)가 collection fetch (join)와 함께 정의되면 하이버네이트는 경고 로그를 남기며 메모리 상에서 페이징 처리를 한다. -> out of memory의 위험
참고: 컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다. 데이터가 부정합하게 조회될 수 있다. 자세한 내용은 자바 ORM 표준 JPA 프로그래밍을 참고하자.
페이징과 한계 돌파
주문 조회 V3.1: 엔티티를 DTO로 변환
일다대에서 일(1)을 기준으로 페이징을 하는 것이 목적이다. 그런데 데이터는 다(N)를 기준으로 row 가 생성된다.
페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까?
1. ToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 한다.
ToOne 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.
2. 컬렉션(OneToMany)은 지연 로딩으로 조회한다.
*1(Order)+N(OrderItem)+M(Item)개의 쿼리
3. 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size , @BatchSize 를 적용한다.
*1(Order)+1(OrderItem)+1(Item)개의 쿼리
- hibernate.default_batch_fetch_size: 글로벌 세팅
- @BatchSize: 특정 엔티티 개별 세팅
- ex) data size: 1000, batch size: 100이라면 1000/100=10개의 IN 쿼리
이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.
즉, 1번의 과정이 없어도 IN 쿼리로 해당 데이터를 각각 모두 조회하지만, 1번의 쿼리가 발생하는 페치조인과 다르게 추가 쿼리 발생
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
jpa-properties-hibernate에서 적용
장점
- 쿼리 호출 수가 1 + N -> 1 + 1 로 최적화 된다.
- 데이터 양이 많다면 조인보다 DB 데이터 전송량이 최적화 된다. (Order와 OrderItem을 조인하면 Order가 OrderItem 만큼 중복해서 조회된다. 이 방법은 각각 조회하므로 전송해야 할 중복 데이터가 없다.)
- 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
- 컬렉션 페치 조인은 페이징이 불가능 하지만 이 방법은 페이징이 가능하다.
결론
ToOne 관계는 페치 조인해도 페이징에 영향을 주지 않는다.
따라서 ToOne 관계는 페치조인으로 쿼리 수를 줄이고 해결하고, 나머지는 hibernate.default_batch_fetch_size 로 최적화 하자.
참고: default_batch_fetch_size 의 크기는 적당한 사이즈를 골라야 하는데, 100~1000 사이를 선택하는 것을 권장한다.
이 전략을 SQL IN 절을 사용하는데, 데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기도 한다. 1000으로 잡으면 한번에 1000개를 DB에서 애플리케이션에 불러오므로 DB에 순간 부하가 증가할 수 있다. 하지만 애플리케이션은 100이든 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다. 1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정하면 된다.
SQL IN 연산자
SELECT * FROM 테이블명
WHERE 컬럼명 IN (값1, 값2, ...);
- WHERE 절 내에서 특정값 여러 개를 선택하는 SQL 연산자
- 괄호 내의 값 중 일치하는 것이 있으면 TRUE
컬렉션은 stream().map 을 통해서 루프를 돌면서 Lazy Loading을 하는데, 이때는 쿼리를 날리지 않고 batch size만큼 루프를 돌고 난 후(batch size만큼 엔티티 개수가 채워지면) Hibernate가 IN절에 조회한 엔티티의 id 값을 채우고 한 번에 쿼리
'Advance I > Springboot & JPA' 카테고리의 다른 글
| 22.06.26 (0) | 2022.06.26 |
|---|---|
| 22.06.24 (0) | 2022.06.24 |
| 22.06.21 (0) | 2022.06.21 |
| 22.06.20 :: 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 (0) | 2022.06.20 |
| 22.06.13 (0) | 2022.06.13 |