컬렉션 조회 최적화
엔티티 직접 노출
@GetMapping("/api/v1/orders")
public List<order> orderV1(){
List<order> all = orderRepository.findAllByString(new OrderSearch());
reuturn all;
}
현재 하이버네이트 버전 5를 bean으로 등록 시켜준 상황이어서 영속성 컨텍스트에 전부 객체가 들어와 있는 상태다.
추가적으로 양방향 관계는 다 @JsonIgnore을 다 만들어주어야 한다.
이 방법은 역시 저번에도 설명했던 것 처럼 엔티티를 직접 노출하기에 단점이 굉장히 많아,
사용하면 안 된다.
엔티티를 Dto로 변환
@GetMapping("/api/v2/orders")
public List<OrderDto> orderV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
static class OrderDto{
private Long orderId;
private String name;
private LocalDateTime;
private OrderStatus orderStatus;
private Address address;
private List<OrderItem> orderItems;
public OrderDto(Order o){
// ~~~~~~~
this.orderItems = o.orderItems;
}
}
위와 같이 dto를 반환하는 api와 dto를 만들어준다.
이렇게 만들어서 api를 호출할 경우,
orderItems는 엔티티기 때문에 프록시 객체가 담겨있어서, null이 담기고 반환이 제대로 되지 않는다.
따라서 프로시를 초기화 시켜주고 그 다음 반환을 시켜야 한다.
static class OrderDto{
private Long orderId;
private String name;
private LocalDateTime;
private OrderStatus orderStatus;
private Address address;
private List<OrderItem> orderItems;
public OrderDto(Order o){
// ~~~~~~~
order.getOrderItems().stream().forEach(o -> o.getItem().getName());
this.orderItems = o.orderItems;
}
}
위와 같이 필요한 객체들의 프록시를 초기화 시켜주고 api 호출을 하면 무사히 결과가 반환이 된다.
하지만 이 방식에도 큰 문제가 있다 바로 dto로 감쌌다고 하지만 결국,
OrderItem은 엔티티기 때문에 엔티티가 그대로 노출된 것 과 다를게 없다는 것이다.
이 경우 OrderItem엔티티의 스펙을 바꾸면 api 스펙도 전부 바뀌는 문제점이 발생한다.
OrderItem 조차도 dto로 다 바꿔주어야 api 스펙 변경 문제와 엔티티 정보 외부 노출 문제를 막을 수 있다.
완전히 엔티티에 대한 의존을 끊어야 한다.
이렇게 dto로 반환을 받아 봤다.
이 결과 오더와 연관관계를 이루고 있는 모든 객체마다 select 쿼리가 한 개씩 나가기 때문에,
굉장히 많은 쿼리가 나가게 된다.
이것도 마찬가지로 페치 조인으로 최적화를 시켜줘야 한다.
페치 조인으로 최적화
우선 repository에 페치 조인을 활용한 쿼리를 하나 만들어 줘야 한다.
public List<Order> findAllWithItem(){
return em.createQuery("select 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();
}
다음과 같이 일대다 관계에서 join을 하면 데이터가 뻥튀기가 돼버린다.
이와 관련된 자세한 내용은 다음 포스트를 참고하길 바란다.
https://2junbeom.tistory.com/50?category=1236823
[JPA] 객체 지향 쿼리 언어 JPQL 5 (경로 표현식, fetch join)
경로 표현식.(점)을 찍어 객체 그래프를 탐색하는 것이다. select m.username -> 상태필드로 객체를 탐색한 경우from Member m join m.team t -> 단일 값 연관 필드 (여기서 team은 엔티티기 때문에 단일 값을
2junbeom.tistory.com
그래서 데이터 뻥튀기를 막기위해 다음과 같이 distinct를 추가해줘야 한다.
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();
}
SQL에서 distinct 는 열의 모든 값이 똑같아야 중복을 제거한다.
JPA에서는 SQL에서 distinct를 통해 걸려져 온 값을 한 번 더 필터링 하는데
JPA에서는 id 값을 기준으로 distinct를 한 번 더 실행시켜 중복을 완전히 제거 시켜준다.
위와 같이 쿼리를 페치 조인으로 수정해주면
order를 조회하기 위한 api에서 쿼리가 단 한 방 밖에 나가지 않게 된다.
페치 조인의 단점
페이징이 불가능하다.
- 컬렉션 페치 조인을 사용하면 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다.
- 컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면, 데이터가 뻥튀기 된 거에 또 뻥튀기가 돼버리니 감당할 수 없는 데이터가 만들어진다.
이러한 경우 페이징을 사용할 수 있는 방법은 다음 포스트에 정리하도록 하겠다.
참고: 김영한 실전! 스프링 부트와 JPA 활용 2
'spring > JPA' 카테고리의 다른 글
[JPA] OSIV와 성능 최적화 (Open Session in View란?) (0) | 2024.08.27 |
---|---|
[JPA] API 개발과 성능 최적화 3 (페이징과 한계 돌파) (0) | 2024.08.11 |
[JPA] API 개발과 성능 최적화 1 (지연 로딩과 조회 성능 최적화) (0) | 2024.08.08 |
[JPA] 객체 지향 쿼리 언어 JPQL 7 (엔티티 직접 사용, 네임드 쿼리, 벌크 연산) (0) | 2024.08.07 |
[JPA] 객체 지향 쿼리 언어 JPQL 6 (fetch join의 한계, 다형성 쿼리) (0) | 2024.08.06 |