일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 코드트리
- JPA
- 알고리즘
- 백준
- SWEA
- JUnit
- 코딩테스트실력진단
- Union Find
- database
- 코테
- 그리디
- 다익스트라
- 코딩테스트
- 완탐
- SSAFY
- Java
- 완전탐색
- 트러블슈팅
- BFS
- 자바
- 기본유형
- 유니온파인드
- Spring
- 다시보기
- 싸피
- DFS
- 부분수열의합2
- DP
- 알고리즘기본개념
- 그래프
- Today
- Total
HwangHub
[JPA] (컬렉션) JOIN FETCH 본문
장점
만약 어떤 엔티티의 dto를 조회한다고 했을 때,
해당 엔티티의 필드 중 어떤 엔티티가 컬렉션으로 연관관계가 묶여 있을 경우
이 조회 기능에 fetch join을 사용하지 않고 dto로 변환하고자 한다면, 이 과정에서 LAZY인 연관관계를 조회하기 위해 무수히 많은 쿼리문이 발생할 것이다. (1+N 이슈)
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAll();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
public List<Order> findAll() {
return em.createQuery("select o from Order o", Order.class)
.getResultList();
}
이는 분명 성능상 크리티컬한 문제를 야기한다. 따라서 N+1 이슈를 방지하기 위해(==하나의 엔티티를 조회하기 위해 실직적으로 여러 쿼리가 나가는 것을 방지하고자) JPA를 사용할 때에는 JOIN FETCH를 보통 사용한다.
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
public List<Order> findAllWithItem() {
return em.createQuery(
"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", Order.class)
.getResultList();
}
단, 이 때에도 OneToOne 또는 ManyToOne인 경우에는 JOIN FETCH만 적용해도 큰 문제가 발생하지 않지만, 만약 OneToMany인 필드를 가진 엔티티를 조회해야 하는 경우에는 select distinct까지 해줘야 중복되지 않은 결과를 얻을 수 있다. (JPA가 JOIN FETCH만으로 콜렉션 엔티티 필드를 조회하여 불러오게 되면, 데이터는 조회한 이후 컬렉션을 기준으로 row를 구성하여 반환하기 때문에 컬렉션의 수 만큼 주 엔티티의 데이터 또한 뻥튀기되어 반환되게 된다(RDB JOIN 쿼리 상의 특징). 이를 어플리케이션 레벨에서 필터링하기 위해 distinct라는 JPQL을 사용하면, 이를 SQL의 distinct로 적용시켜서 불필요한 중복을 제거하여 엔티티 정보를 조회할 수 있게 해 준다.)
이렇게 JOIN FETCH를 하게 되면, 각 연관관계 필드에 LAZY로 되어 있어도, 내부적으로 inner join SQL을 적용하여 한 번의 쿼리로 모든 정보를 불러올 수 있게 된다.
단점
JOIN FETCH를 이용하면 SQL 쿼리를 1번만 보내어 조회를 할 수 있기 때문에 성능상으로 봤을 때, JOIN FETCH를 사용하지 않을 때와 비교하여 많은 성능 개선을 이뤄낼 수 있다.
하지만, 일대일 또는 다대일 JOIN FETCH와 다르게, 일대다 JOIN FETCH의 경우에는 페이징이 불가능하다는 점을 꼭 유념해야 한다. 즉, 일대다 JOIN FETCH를 수행하고 나서 .setFirstResult(1).setMaxResults(100).getResultList();와 같이 JPA 로직을 구성하게 되면, 쿼리 레벨에서 페이징을 수행하지 않고, 스프링 어플리케이션은 우선 요청된 엔티티 데이터를 메모리에 모두 로드시킨 뒤에, 메모리에서 페이징을 수행하려고 시도한다. 이는 메모리가 아웃될 수 있는 매우 위험한 매커니즘이므로, 일대다 관계의 컬렉션 필드를 가진 엔티티는 JOIN FETCH 방식으로 조회해서는 안된다.
참고: 페이징을 위한 메서드인 setFirstResult(숫자) 의 parameter인 숫자는 0부터 시작.
참고로, 컬렉션 JOIN FETCH는 1개의 컬렉션 필드에 대해서만 적용이 가능하다. 만약 둘 이상의 컬렉션 엔티티 필드를 가진 엔티티를 JOIN FETCH할 경우에는 데이터가 부정확하게 조회될 수 있음을 유의하자.(어차피 근데 컬렉션 JOIN FETCH는 사용하지 않아야 한다.) -> 이유: 일대다 JOIN FETCH만 해도 데이터가 뻥튀기되는 것이 심한데, JOIN FETCH를 여러 일대다 필드를 가진 엔티티에 적용하게 되면 일대다*일대다 조회가 되어버리는 것과 같아서 엄청난 뻥튀기 현상이 발생한다. 이 경우에, JPA는 이렇게 조회한 데이터를 잘못 맞출 수 있게 된다. (정합성 이슈 존재)
결론
OneToOne, ManyToOne 필드를 가진 엔티티 조회 : JOIN FETCH (O ; 사용을 적극 권장한다.)
OneToMany 필드를 가진 엔티티 조회 : JOIN FETCH (X ; 페이징을 메모리에서 수행하기 때문에, 사용하면 안된다.)
'무엇을 합니다 > 기술이해' 카테고리의 다른 글
[JPA] OSIV (0) | 2023.06.28 |
---|---|
[JPA] 연관관계 컬렉션 필드 조회 최적화 : 엔티티/DTO 조회 방식 (0) | 2023.06.28 |
[Java] 문자열 비교 ==, equals() (0) | 2023.06.28 |
[Spring] 패키지 구성 : repository와 query 분리 (0) | 2023.06.28 |
[JPA] 컬렉션 LAZY Loading시 페이징 기법 (0) | 2023.06.27 |