spring/JPA

[JPA] API 개발과 성능 최적화 2 (컬렉션 조회 시 성능 최적화)

대기업 가고 싶은 공돌이 2024. 8. 10. 03:41

컬렉션 조회 최적화

엔티티 직접 노출

@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에서 쿼리가 단 한 방 밖에 나가지 않게 된다.

 

페치 조인의 단점

페이징이 불가능하다.

 

  1. 컬렉션 페치 조인을 사용하면 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다. 
  2. 컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면, 데이터가 뻥튀기 된 거에 또 뻥튀기가 돼버리니 감당할 수 없는 데이터가 만들어진다.

이러한 경우 페이징을 사용할 수 있는 방법은 다음 포스트에 정리하도록 하겠다.

 

참고: 김영한 실전! 스프링 부트와 JPA 활용 2