본문 바로가기

CS 잡지식

DB 최적화 기법 - Projection 대상을 필요한 것만으로 한정(Feat.DTO)

 

@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDTO> orderV3() {

    //fetch join을 사용하여 Order 조회!
    List<Order> findOrders = orderRepository.findAllWithMemberDelivery();

    // 미리 정해진 API 결과 스펙을 반환하기 위하여, DTO에 정해진 스펙대로 필드값들을 넣어 준다.
    List<SimpleOrderDTO> resultList = findOrders.stream()
            .map(o -> new SimpleOrderDTO(o))
            .collect(Collectors.toList());

    return resultList;

}

위 API에 대한 쿼리문은 아래와 같다. 

@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDTO> orderV4() { //  JPA에서 DTO를 바로 조회하기
                                             //  즉, JPQL문을 실행하면, 바로 DTO 형태로 뱉게 만든다.

    return orderRepository.findOrderDTOs();

}

위 API에 대한 쿼리문은 아래와 같다.

 

2 쿼리문 모두 JOIN하는 것까지는 같다. 

그러나 차이는 select의 양이다. 

첫번째, 쿼리는 [API 결과 스펙(DTO)]에 어떤 값들이 있는지 전혀 고려하지 않고, JOIN된 모~든 필드값들을 조회하였다.

두번째 쿼리는 [API 결과 스펙(DTO)]을 보고, 거기에 필요한 값들만을 DB에서 조회하게 JQLP 안에 new DTO() 연산자를

이용하여 작성을 하였다.  

-> JPA를 사용하면서, 즉 JPQL을 작성을 할 때에는 [API 결과 스펙](DTO)에 무엇이 들어 있는지를 보고 그 필드값들을 

DB로 부터 조회 가능하게 [Projection]의 대상을 지정해주자.(new 연산자 사용해야 함)

-> DB로 부터 날아오는 데이터 양이 압도적으로 적어지므로, 네트워크 비용이 최적화가 된다. 

(요즘, 네트워크가 워낙 좋아서, 네트워크 측면에서는 차이가 거의 미미)

 


// orderReposiotry 중 일부

entityManager.createQuery(

        "select" +

                // new OrderSimpleQueryDTO(매개변수) 연산자를 통해 JPQL에서 바로 DTO로 변환해서 반환받을 수가 있다.
                // lazy가 걸려있는 member나 delivery도 즉시 조회가 된다(정확히는 member,delivery 객체가 조회되는 것이 아니라,
                // 그 안의 Projection 대상으로 지정된 필드들만 DB에서 가져온다)
                // -> JPA는 new 클래스(m.name, d.address)를 보고, Lazy가 걸려 있는 객체이더라도 [사용]되었다고 보고, 즉시 로딩을 한다.
         " new jpabook.jpashop.repository.OrderSimpleQueryDTO(o.id, m.name, o.orderDate, o.orderStatus, d.address)" +

         " from Order o" +

         " join o.member m" +

         " join o.delivery d", OrderSimpleQueryDTO.class)

         .getResultList();

그렇다면 항상 V4 API가 Best Practice인 걸까??

-> V3,V4 모두 Trade Off가 존재한다. 

확실히, V4는 필요한 것들만 조회해서 들고 오니깐 네트워크 비용이 줄어 드는 장점이 있다. 

그러나, 여러 API 호출에 대해서 [재사용]이 어렵다.

V3는 Projection의 대상이 객체의 모~~든 필드값이다. 

예를 들어, Member 객체 안에 10개의 필드값이 있다고 가정을 하자.

V3는 이 10개 필드값을 모두 가져와야 하기 때문에 네트워크 비용이 많이 든다.

하지만, 어떤 API에서는 Member :: name만 필요하고, 또 다른 어떤 API에서는 Member :: age가 필요하고

또 다른 어떤 API에서는 Member :: name, Member::age 모두가 필요할 수가 있다. 

근데, 만약 V4에서는 Member::name 필드만 조회하게 되면, 위 3개의 API중, 2개의 API에 대해서는 쿼리의 결과를

[재사용]이 불가능하므로, 다시 DB를 조회를 해야 한다. 

근데, V3에서는 Member의 모든 필드값을 조회해 놓았으므로, 캐싱을 통한 [재사용]으로 여러 API에게 결과값을 반환해 줄

수가 있다.

즉, 처음에는 비록 모든 필드값을 들고 와서, 네트워크 비용이 많이 들지만, 그 이후로는 캐싱을 통해 빠르게 여러 API를 처

리가 가능하게 된다. 

또한 V4는 [Repository]의 재사용성도 떨어지는 단점도 있다. 

클라이언트 코드에서는 repository(여기서는 orderRepository)을 사용하여서, [객체]를 조회하여서 자유로운 [객체 탐색]이

가능한 것이 좋다. 

V3 코드를 보면, orderRepository를 사용하여 findMember, 즉 Order객체(List형이지만 걍 무시)를 조회를 해 왔다.

이 List<Order> findOrders = [orderRepository].findAllWithMemberDelivery() 부분을 V3 메서드 안이 아니라, 

Controller의 필드에 List<Order> findOrder를 선언을 하고, 이 필드를 초기화하는 메서드를 딱 [1번만 호출]시키게 하면

여러 API가 각 Spec에 따라 필요한 Order의 연관 관계 [객체를 탐색]하여서 재사용이 가능하다. 

V4 같은 경우, 특정 API의 스펙에 맞춰서 [객체]가 아닌, 필드를 조회해 버렸으므로, 여러 API가 스펙에 필요한 [객체를 탐

색]하지 못하기 때문에, 다른 V4와 같은 방식대로라면, API 스펙마다 Repository에서 메서드를 따로 따로 다 만들어 주고,

서로 다른 API들이 호출될 때마다 Repository의 해당 메서드를 [매번] 호출해줘야 한다(이런 의미에서 repository의 재사용

성이 떨어진다.) 

결론 : Repository는 거의 절대적으로 [객체]를 조회하도록, JPQL을 작성을 해야 한다. 

(V4의 이러한 단점을 해결하는 방법이 있다. 아래 사이트 참조)

https://jbluke.tistory.com/421

 

특정 API 스펙에 맞는 JPQL문을 작성해야 할때!!

보통, JPQL은 Repository 계층에서 작성이 된다. 근데, Repository는 거의 절대적으로 [조회]를 할 때 [객체]를 조회하여서, 그와 연관된 객체 탐색이 가능하도록 하여야 한다. (그래야지 특정 API가 아닌,

jbluke.tistory.com

 

또 다른 차이는 아래와 같다. 

V3는 DB에서 [객체]를 조회한 것이기에, Context에 객체들이 캐쉬되어 있다.

고로, JPA의 Dirty Checking과 setter를 통해 객체의 [변경]이 가능하다.

그러나 V4는 객체를 DB로부터 넘겨 받은 것이 아니라, new 연산자를 통해서 DTO 객체를 넘겨 받은 것이기에

객체의 [변경]이 불가능 하다.

-> 개발 상황을 고려해서 V3,V4 전략 중 적절한 것을 개발자가 선택해서 개발해야 한다. 

쿼리 방식 선택 권장 순서

1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.

2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.

3. 그래도 안되면 DTO로 [직접] 조회하는 방법을 사용한다.

4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 [SQL을 직접 사용]한다