Spring/API

API 개발 - 컬렉션 조회 (페치 조인 최적화)

JWonK 2022. 7. 1. 19:22
728x90
반응형
  • 주문 내역에서 추가로 주문한 상품 정보를 조회한다. 
  • 하나의 주문에 상품은 여러 개가 있을 수 있으므로 xToMany형태의 컬렉션 조회가 될 것이다.
  • 지금까지 게시글로 원하는 API 반환을 위해서는 API 스펙에 맞춰 DTO 클래스를 설계하여 반환해야한다는 것을 알고 있다는 가정 하, DTO로 변환하여 반환하는 코드로 작성
  • 조건 : API 반환으로 원하는 조건은 { 주문 번호, 사용자 이름, 주문 날짜, 주문 상태, 배송지 정보, 주문한 상품 정보} 이다. 이 스펙에 맞춰 DTO를 개발해야한다.

 

위 조건에 맞춰 전 게시글은 DTO로 컬렉션을 조회하는 API 개발이었다. DTO 개발을 통해 API가 요청하는 스펙에 따라 DTO로 변환하여 개발할 수 있었고, 엔티티에 의존하지 않는 API를 개발할 수 있었다. 하지만 이에 대한 단점으로 매우 비효율적인 면이 존재했었다.

 

오늘은 이러한 비효율적인 코드를 페치 조인을 통해 최적화하는 방법이다.


DTO 개발을 하게 되면 원하는 정보들을 위해 DB에서 원하는 정보를 담고 있는 모든 테이블에 쿼리문을 날렸어야했다. 이는 매우 비효율적인 면을 갖고 있다.

 

JPA에서의 페치 조인을 통해 우리가 원하는 정보를 모두 한 테이블에 조인시켜 단 한 번의 쿼리문을 통해 조회하면 효율적인 코드를 작성 할 수 있다.

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderRepository orderRepository;
    
    @GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV3(){
        List<Order> orders = orderRepository.findAllWithItem();
        return orders.stream()
                .map(o -> new OrderDto(o))
                .collect(Collectors.toList());
    }

    @Data
    static class OrderDto{

        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
        private List<OrderItemDto> orderItems;
        
        public OrderDto(Order order){
            orderId = order.getId();
            name = order.getMember().getUsername();
            orderDate = order.getOrderDate();
            orderStatus = order.getOrderStatus();
            address = order.getDelivery().getAddress();
            
            orderItems = order.getOrderItems().stream()
                    .map(orderItem -> new OrderItemDto(orderItem))
                    .collect(Collectors.toList());
        }
    }

    @Data
    static class OrderItemDto{
        /**
         * 개발 요구사항이 상품명, 주문가격, 주문수량만 요구했다는 가정.
         */
        private String itemName;
        private int orderPrice;
        private int count;
        
        public OrderItemDto(OrderItem orderItem){
            itemName = orderItem.getItem().getName();
            orderPrice = orderItem.getOrderPrice();
            count = orderItem.getCount();
        }
    }
}

 

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();
}

단순 조회할 때와 같이 쿼리문을 통해 우리가 원하는 정보를 모두 페치 조인해주면 한 테이블의 모든 정보가 담겨온다.

즉, 한 번의 쿼리문으로 정보들을 모두 조회하는 것이다.

 

하지만 이렇게만 하게 되는 경우, 일대다 조인이 있으므로 데이터베이스 row가 증가한다.

그 결과 같은 order 엔티티의 조회 수도 증가하게 된다. 같은 정보를 모두 가져 올 필요가 없으므로 이 부분은 다 중복제거를 해주어야한다.

 

public List<Order> findAllWithItem(){
        return em.createQuery(
                        /**
                         * DB에서의 distinct은 모든 값들이 중복되어야 중복 제거가 되지만
                         * JPA에서 distinct은 루트 엔티티(아래에서는 Order) id값이 중복되면 중복 제거가 이루어진다.
                         */
                "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();
    }

distinct를 사용하여 중복을 제거해준다.

여기서 알아야 할 점이 데이터 베이스에서의 distinct는 행에 담긴 값들이 같아서 중복이라고 판단되어야 중복 제거가 되지만 JPA에서의 distinct는 루트 엔티티(찾고자 하는 정보)의 id값이 중복이면 다른 정보들과 관계 없이 중복 제거가 이루어진다.

 

이렇게 하면 페치 조인을 통해 최적화를 진행한 후, 추가로 중복제거까지 하여 중복 조회까지 할 수 있다.

하지만 컬렉션 경우에서의 페치 조인의 심각한 단점이 존재한다. 바로 페이징이 불가능하다.

 

페이징이란, 정보의 양을 원하는 만큼만 가져오는 것을 말하는데 컬렉션 페치 조인을 사용하면 이 기능이 불가능하다.

하이버네이는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다. 이는 매우 위험하기 때문에 절대 페이징 처리를 하면 안된다.

 

다음 게시글이 컬렉션 조회 페이징 처리 부분이다.

 

※ 참고 : 컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다. 데이터가 부정확하게 조회될 수 있다.

※ 반환할 때는 좀 더 유연한 API 개발을 위해 Result 클래스로 반환해주어야한다.

https://wonsjung.tistory.com/421

 

회원 조회 API 개발 / Result 클래스로 유연한 JSON 반환

회원 조회는 값을 가져와 화면에 보여주기만 하면 된다. 즉, 생성 / 수정 없이 조회만 하면 된다 -> REST API : GET Method 사용 회원 조회 V1 : 응답 값으로 엔티티를 직접 외부에 노출 @RestController @Require

wonsjung.tistory.com

 

728x90
반응형