단순 쿼리를 직접 작성할 필요 없이 대신 작성해주는 것은 JPA의 큰 장점이다.

 

하지만 역설적이게도 이러한 장점이 성능 문제를 초래할 수 있다.

테이블을 이것저것 마구잡이로 조회해서 불필요하게 많은 쿼리가 나가는 것이다.

 

이러한 문제를 방지하고 최적의 성능을 이끌어내려면 여러가지 사항들을 신경쓰며 개발해야 한다.

아래는 이에 관하여 알아두면 좋은 내용들을 정리한 것이다.

 

 

 

 

 

(하이버네이트 모듈)

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

@Bean
Hibernate5Module hibernate5Module() {
	return new Hibernate5Module();
}

모든 연관관계에 대하여 LAZY 전략을 사용하는 것이 좋다.

LAZY 로딩을 사용한 객체는 직접적으로 사용하기 전까지는 프록시 객체를 담고 있게 된다.

 

이렇게 프록시 객체를 담고 있는 엔티티를 JSON으로 반환할 경우 제대로 된 값이 들어있지 않아 문제가 발생한다.

 

위 코드처럼 하이버네이트 모듈을 빈으로 등록해  놓는다면 이런 프록시 객체 엔티티에 대해서 모두 null값을 전달하도록 하는 기능을 기본 기능으로서 사용할 수 있다. (LAZY로딩된 엔티티들에 대한 로딩을 강제하는 옵션도 있음)

 

 

(@JsonIgnore)

양방향 연관관계를 가진 엔티티들에 대해 조회가 이뤄질 경우 무한루프를 돌면서 계속 서로를 조회하는 문제가 발생할 수 있다.

이를 방지하기 위해 양방향 연관관계 중 한 곳의 관계를 @JsonIgnore로 끊어서 JSON 전달을 막을 수 있다.

 

 

 

사실 위의 두 방법 (하이버네이트 모듈, @JsonIgnore)는 실무에서는 별로 사용될 일이 없다. 어차피 실무에서는 엔티티를 직접 전달하지 않고 항상 DTO를 통해서만 API 통신이 이뤄지기 때문이다.

 

 

 

 

(DTO를 통한 전달)

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAllByCriteria(new OrderSearch());
        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
    
    @Data
    static class SimpleOrderDto {
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;  // 고객 주소 아니고 배송지 정보임

        public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();  // Member LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();  // Delivery LAZY 초기화
        }
    }

이렇게 inner class로 따로 만들어준 DTO 객체를 반환하면 내가 원하는 정보들만 담을 수 있으므로 위와 같은 상황을  고려하지 않아도 된다.

 

내가 필요한  엔티티에  Member, Delivery에 대해서는 getter를 사용할 때 쿼리가 날아가면서 프록시 객체가 아닌 진짜 객체가 들어간다.

 

하지만 이러한 방식은 order에 대한 쿼리 1번 + member, delivery에 대한 쿼리 각각 1번.  총 3번의 쿼리를 호출하게 되어 성능 저하를 초래할 수 있다.

 

 

 

 

(fetch join을 사용한 성능 최적화)

    // fetch 조인을 사용하여 발생하는 쿼리 수를 대폭 줄임
    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> orderV3() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
    
    .
    .
    .
    
    // OrderRepository 내부
    public List<Order> findAllWithMemberDelivery() {
        // order에 대한 한 번의 쿼리에서 member, delivery 를 한 번에 가져옴
        return em.createQuery(
                "select o from Order o" +
                        " join fetch o.member m" +
                        " join fetch o.delivery d", Order.class
        ).getResultList();
    }
    

레포지토리 내부에 페치 조인으로 order들을 조회하는 함수를 생성해 이를 사용한다.

 

fetch join을 사용해 JPQL을 작성하면 order에 대한 member, delivery를 별도의 쿼리로 조회하는 것이 아니라 order를 조회할 때 컬럼만 추가하여 함께 조회한다.

 

즉, order에 대한 쿼리 하나만 나가는 것이다.

 

 

 

 

+ Recent posts