스프링데이터 + JPA/API 개발

16. Dto로 조회. 플랫 데이터 최적화 쿼리 하나

sdafdq 2023. 11. 16. 12:25

컬렉션인 OrderItem까지도 join해서 쿼리 하나로 가져오는 거다.

 

sql로 정말 flat하게 조회 해와야 한다

 

@Data
public class OrderFlatDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    private String itemName;
    private int orderPrice;
    private int count;
}

저렇게 OrderItem의 내용들까지고 펼쳐놔서 flat하게 만들 것이다.

 

일단 그러면 가져오려면

public List<OrderFlatDto> findAllByDto_flat() {
    return em.createQuery(
            "select new" +
                    " jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
                    " from Order o" +
                    " join o.member m" +
                    " join o.delivery d" +
                    " join o.orderItems oi" +
                    " join oi.item i", OrderFlatDto.class)
            .getResultList();
}

이렇게 가져오면 된다.

근데 이러면?

당연히 orderItems에 의해 데이터가 뻥튀기 된다.

 

그래서 쿼리는 한번이지만 페이징은 의도되로 안된다. (이번 같은 경우 orderItem 개수만큼 가져온다.)

 

그리고 애플리케이션에서 한번 가공을 해 줘야 한다.

저 flat하게 얻어온 데이터들을..

 

@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6(){
    List<OrderFlatDto> orderFlats = orderQueryRepository.findAllByDto_flat();

    Map<Long, List<OrderItemQueryDto>> orderItemMap = new HashMap<>();
    Map<Long, OrderQueryDto> orderMap = new HashMap<>();

    orderFlats.forEach(orderFlat -> {
        Long orderId = orderFlat.getOrderId();
        if(orderMap.get(orderId) == null){
            orderMap.put(orderId,new OrderQueryDto(orderId, orderFlat.getName(), orderFlat.getOrderDate(), orderFlat.getOrderStatus(), orderFlat.getAddress()));
        }

        if(orderItemMap.get(orderId) == null){
            orderItemMap.put(orderId,new ArrayList<OrderItemQueryDto>());
        }
        orderItemMap.get(orderId).add(new OrderItemQueryDto(orderId, orderFlat.getItemName(), orderFlat.getOrderPrice(), orderFlat.getCount()));
    });

    orderItemMap.forEach((orderId, orderItem)->{
        orderMap.get(orderId).setOrderItems(orderItem);
    });

    return new ArrayList<OrderQueryDto>(orderMap.values());
}

이런 식으로 가공을 해 줘야 한다.

flat한 것을 좀더 객체 패러다임 형식으로 바꿔줬다.

 

 

성능 최적화 고려 순서

1. 엔티티 조회 방식으로 우선 접근 (그 후 애플리케이션에서 Dto로 변환)

2. join fetch 으로 쿼리 수 최적화

3. 컬렉션 최적화 (페이징 필요 시 batch로 해결, 필요 없으면 그냥 join fetch)

4. Dto 사용 고려

5. 다 안되면 그냥 NativeSQL이나 JdbcTemplate

 

엔티티를 우선적으로 고려하는 이유는

엔티티 조회 방식은 join fetch나 batch등 코드를 거의 수정하지 않고 옵션만 약간 줘서 다양한 성능 최적화 시도할 수 있는 반면,

Dto 직접 조회는 성능 최적화 하려고 하면 코드를 많이 변경해야 함. (Dto 직접 조회는 거의 sql을 쓰는 거와 비슷하니까.)

 

 

사실 batch + join fetch로 성능 최적화가 해결이 안될 정도라면, 이미 클라이언트가 많은 서비스이다.

그 때는 사실 캐시를 쓰는 방향으로 문제를 해결해야 한다. Dto를 쓴다고 해결될 정도는 아마 아닐거다.

 

참고로, 엔티티는 직접 캐싱 하면 안된다. 영속성 컨텍스트에서 레퍼런스 자체를 관리하기 때문에.

 

무조건 Dto로 변환하여 Dto를 캐시하여야 한다.

 

Redis 나 메모리 캐시 등

 

보통 개발자는 성능 최적화와 코드 복잡도 사이에서 줄타기를 한다.

JPA는 이미 엔티티 조회방식에서 많은 부분을 최적화 해 주기 때문에, 코드를 간단하게 유지하면서 성능을 많이 최적화 할 수 있다. 

반면 Dto 방식은 거의 sql을 직접 다루는 것과 유사하기 때문에, 줄 사이의 줄타기를 해야 한다.

 

 

Dto 조회 방식의 선택지

v4~v6까지 해봤는데,

v4

https://qwefdg3.tistory.com/860

 

v5

https://qwefdg3.tistory.com/861

 

v6

https://qwefdg3.tistory.com/862

 

나는 v5가 좋아보이긴 한다.

뭐 사실 그냥 단건 조회는 v4처럼 성능보단 가시성에 우선을 둘 텐데,

v5는 코드가 좀 생기긴 하지만, 그냥 id를 뽑아서 그걸 in에다 넣어서 가져오는 내 입장에서는 사실 꽤 명확해 보인다.

 

v6은, 쿼리 한번이지만

그냥 v5처럼 쿼리 두번이더라도 저렇게 하는 게 좋아보인다.

오히려 애플리케이션에 부담이 갈 것 같다. (뭐 이정도는 아마 별거 아니라고 하겠지만.)

 

솔직히 v6은 중복된 데이터도 끌고오고 좀 비효율적으로 보였음.

쿼리 하나 줄이겠다고 이렇게까지 하는 건 애플리케이션에 안좋아 보임. 코드 가시성도 그렇고.

페이징도 불가능하고.(사실 대부분 단건 조회 아닌 것들은 아마 페이징 할 것이다. 애플리케이션이 감당할 수 있을지 못 할지 모르는 데 모든 데이터를 조회해오진 않을 거다. 그럴경우 이거는 중복데이터가 굉장히 많아지므로 좋아보이진 않는다. 그래서 상황에 따라 v5보다 성능이 안나올 수 있다고 한다.)

 

선생님도 Dto 조회방식은 v5를 많이 선택한다고 함. 사실 batch랑 똑같은 기능이긴 한데.. 이런 점을 참고 해서 다른 곳에서  채택한 여러가지 것을 최적화 하는 방식을 생각해 보는 것도 좋을 듯.