0. 개요
- 이전 포스팅에서 DTO 변환을 이용한 컬렉션 조회 방법을 알아보았다.
- DTO 변환 방법을 사용하여 컬렉션 조회 시, 다수의 SQL 문이 발행된다는 문제점을 확인하였다.
- 이 문제를 해결하기 위해 Fetch Join을 이용한 컬렉션 조회 방법을 알아보자.
1. DTO 변환 + Fetch Join
a) Fetch Join 쿼리 작성
- 우선 Fetch Join을 이용하는 쿼리를 작성해보자.
- 쿼리는 다음과 같다.
public List<Order> findAllWithItem() {
return em.createQuery("select o from Order o " +
"join fetch o.member " +
"join fetch o.delivery " +
"join fetch o.orderItems oi " +
"join fetch oi.item i", Order.class).getResultList();
}
- 위의 쿼리에는 한 가지 문제점이 있다.
- 바로 Order 테이블과 OrderItems 테이블을 Join 하는 부분이다.
- 현재 DB에는 2개의 주문이 있고 주문당 2개의 물품이 할당되어 있다.
- 즉, Order 테이블에는 2개의 데이터가 존재하고 OrderItems 테이블에는 4개의 데이터가 존재한다.
- 4개의 데이터(= OrderItems)를 기준으로 2개의 데이터(= Order)를 Join 하면 결과적으로 4개의 데이터가 파생된다.
b) 문제 확인
- 위의 쿼리를 그대로 실행해보면 다음과 같은 결과를 API로부터 얻게 된다.
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o)).collect(Collectors.toList());
return result;
}
}
[
{
"orderId":4,
"name":"userA",
"orderDate":"2022-06-02T15:14:01.414705",
"orderStatus":"ORDER",
"address":{"city":"Seoul","street":"Gangnam","zipcode":"11111"},
"orderItems":[{"itemName":"JPA1 Book","orderPrice":10000,"count":1},
{"itemName":"JPA2 Book","orderPrice":20000,"count":2}]},
{
"orderId":4,
"name":"userA",
"orderDate":"2022-06-02T15:14:01.414705",
"orderStatus":"ORDER",
"address":{"city":"Seoul","street":"Gangnam","zipcode":"11111"},
"orderItems":[{"itemName":"JPA1 Book","orderPrice":10000,"count":1},
{"itemName":"JPA2 Book","orderPrice":20000,"count":2}]
},
{
"orderId":11,
"name":"userB",
"orderDate":"2022-06-02T15:14:01.491492",
"orderStatus":"ORDER",
"address":{"city":"Busan","street":"Haeundae","zipcode":"22222"},
"orderItems":[{"itemName":"Spring1 Book","orderPrice":20000,"count":3},
{"itemName":"Spring2 Book","orderPrice":40000,"count":4}]
},
{
"orderId":11,
"name":"userB",
"orderDate":"2022-06-02T15:14:01.491492",
"orderStatus":"ORDER",
"address":{"city":"Busan","street":"Haeundae","zipcode":"22222"},
"orderItems":[{"itemName":"Spring1 Book","orderPrice":20000,"count":3},
{"itemName":"Spring2 Book","orderPrice":40000,"count":4}]
}
]
- 이처럼 동일한 데이터가 중복되어 반환된다.
- 이 결과가 잘못된 것이 아니다. RDB의 특성상, 위의 결과처럼 4개의 결과가 반환되는 것이 올바른 것이다.
- 다만, 개발자는 중복 없이 Order 테이블의 2개 데이터와 매칭 하는 2개의 데이터만을 원하는 것이다.
c) 해결 방법 - distinct 키워드 사용
- distinct 키워드를 사용하면 다음과 같은 결과를 반환한다.
public List<Order> findAllWithItem() {
return em.createQuery("select distinct o from Order o " +
"join fetch o.member " +
"join fetch o.delivery " +
"join fetch o.orderItems oi " +
"join fetch oi.item i", Order.class).getResultList();
}
[
{
"orderId":4,
"name":"userA",
"orderDate":"2022-06-02T15:24:48.526842",
"orderStatus":"ORDER",
"address":{"city":"Seoul","street":"Gangnam","zipcode":"11111"},
"orderItems":[{"itemName":"JPA1 Book","orderPrice":10000,"count":1},
{"itemName":"JPA2 Book","orderPrice":20000,"count":2}]
},
{
"orderId":11,
"name":"userB",
"orderDate":"2022-06-02T15:24:48.604724",
"orderStatus":"ORDER",
"address":{"city":"Busan","street":"Haeundae","zipcode":"22222"},
"orderItems":[{"itemName":"Spring1 Book","orderPrice":20000,"count":3},
{"itemName":"Spring2 Book","orderPrice":40000,"count":4}]
}
]
- 이처럼 개발자가 원하던 대로 중복 데이터 없이 결과를 반환하는 것을 확인할 수 있다.
d) fetch join의 유일한 단점 - 페이징 사용 불가
- Fetch Join을 사용하면 페이징을 사용할 수 없다.
- Fetch Join은 Join 대상이 다수(= Many)이므로 결과 데이터의 수가 증가한다.
- 이로 인해 데이터의 순서가 틀어지게 되어 어떤 데이터가 첫 번째 데이터인지 그 순서를 알 수 없다.
- 즉, 데이터의 순서가 틀어지므로 페이징을 적용할 수 없다.
- Hibernate의 경우 fetch join을 사용하더라도 내부 메모리를 이용하여 페이징 작업을 수행한다.
- 다만, fetch join의 결과가 많다면 최악의 경우 OutOfMemory가 발생할 수 있다.
2. JPA의 Distinct 키워드
a) 원래는 중복 데이터가 아니다.
- 다음 사진을 살펴보자.
- 위의 사진에서 반환된 4개의 데이터는 DB의 distinct로 걸러질 수 없는 데이터다.
- RDB는 하나의 행을 하나의 데이터로 취급하기에, 행 자체의 값이 동일해야 중복으로 처리된다.
- 즉, 위의 사진처럼 distinct 키워드를 사용해도 RDB에서는 이를 중복으로 처리하지 않는다.
- 그렇다면 JPA에서는 이를 어떻게 중복 데이터로 처리하는 것일까?
b) JPA의 distinct는 중복 객체를 처리한다.
- JPA는 ORM 기술이다. 그러므로 객체 중점적으로 모든 것을 처리한다.
- 그러므로 DB에서 반환되는 데이터 또한 동일하게 객체 단위로 처리한다.
- 즉, 영속성 Context에서 중복 객체를 인식하는 것이다.
- 위의 사진은 distinct를 사용하지 않은 경우, JPA에서 반환하는 객체의 주소 값이다.
- JPA는 id(= primary key) 값이 동일한 경우, 동일한 객체로 판단한다.
- 즉, JPA의 distinct는 중복 객체를 필터링하는 역할을 수행하는 것이다.
c) 결론
- JPA의 Distinct 키워드는 단순 RDB의 distinct와 동일한 역할(= 행 간의 중복 데이터 필터링)뿐만 아니라,
중복 객체를 필터링하는 역할을 추가적으로 수행한다.
'Back-end > JPA 개념' 카테고리의 다른 글
API 조회 성능 최적화 - 컬렉션(DTO 직접 조회) (0) | 2022.06.04 |
---|---|
API 조회 성능 최적화 - 컬렉션(Fetch Join + 페이징 + Batch Size) (0) | 2022.06.03 |
API 조회 성능 최적화 - 컬렉션(DTO 변환) (0) | 2022.06.01 |
API 조회 성능 최적화 - DTO 직접 조회 (0) | 2022.05.30 |
API 조회 성능 최적화 - DTO 변환 방식(Fetch Join) (0) | 2022.05.30 |
댓글