본문 바로가기

Spring/SpringBoot

SpringBoot-JPA에서 DTO로 바로 조회하기

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    public List<Order> findAllWithMemberDelivery() {
        // join fetch: member, delivery가 Lazy로 fetchType이 설정되어 있는데,
        // join fetch 하면 프록시 무시하고 실제 엔티티를 다 넣어서 가져온다.
        // 즉, 지연로딩도 즉시로딩으로 가져온다.

        // 여기서 주의해야 할 점은 fetch join으로 가져오는 경우 Order와 연관된 Member, Delivery가 모두
        // join된 테이블을 가져오게 되는 것이다. 따라서 동일한 엔티티가 여러번 조회되는 중복 조회도 가능하다.
        List<Order> result = em.createQuery("select o from Order o " +
                "join fetch o.member m " +
                "join fetch o.delivery d", Order.class)
                .getResultList();

        return result;
    }

    public List<OrderSimpleQueryDto> findOrderDtos() {  //API 스펙에 맞춘 코드가 리포지토리에 들어간다는 단점 존재
        //jpa로 엔티티를 전달할 수는 없고, 식별자만 반환할 수 있기 때문에 Order 자체를 넘길 수는 없고 order의 필드값을 각각 따로 넘겨줘야한다.
        return em.createQuery("select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, m.name,o.orderDate,o.status,d.address) " +
                        "from Order o " +
                        "join o.member m " +
                        "join o.delivery d",
                        OrderSimpleQueryDto.class)
                .getResultList();
    }
}

 두 개의 메서드를 비교해보자.

 

먼저 findAllWithMemberDelivery( )는 Order 엔티티에 연관된 Member와 Dellivery 엔티티를 조인해서 즉시로딩(join fetch)로 한번에 싹 끌고 오는 메서드이다. 쿼리를 보면 대강 이러하다.

select
    order0_.order_id as order_id1_6_0_,
    member1_.member_id as member_i1_4_1_,
    delivery2_.delivery_id as delivery1_2_2_,
    order0_.delivery_id as delivery4_6_0_,
    order0_.member_id as member_i5_6_0_,
    order0_.order_date as order_da2_6_0_,
    order0_.status as status3_6_0_,
    member1_.city as city2_4_1_,
    member1_.street as street3_4_1_,
    member1_.zipcode as zipcode4_4_1_,
    member1_.name as name5_4_1_,
    delivery2_.city as city2_2_2_,
    delivery2_.street as street3_2_2_,
    delivery2_.zipcode as zipcode4_2_2_,
    delivery2_.status as status5_2_2_ 
from
    orders order0_ 
inner join
    member member1_ 
        on order0_.member_id=member1_.member_id 
inner join
    delivery delivery2_ 
        on order0_.delivery_id=delivery2_.delivery_id

 조인 후 order와 연관된 모든 member, delivery의 정보를 다 가져온다. 

 

조회 최적화 하기

 선택적으로 원하는 데이터만 조회하기

 하지만 선택적으로 원하는 Member와 Deliver의 데이터만 가져오고 싶을 수 도 있다. 예를 들어 Member에서는 name만 가져오고 싶고 Delivery에서는 Address만 가져오고 싶을 수 있을 것이다. 이럴 때는 별도의 DTO를 만들고 거기에 내가 가져오고자 하는 필드의 데이터만 담아서 가져오면 된다. 

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

    public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus status, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.status = status;
        this.address = address;
    }
}

findOrderDtos() 메서드가 바로 그것이다. 이 메서드는 OrderSimpleQueryDto라는 DTO에 id, Member.name, orderDate, status, Delivery.address 데이터를 담아서 반환한다. 결과적으로 모든 필드 값을 다 가져오는 것이 아닌 선택적으로 몇가지 데이터만 가져올 수 있다.

select
    order0_.order_id as col_0_0_,
    member1_.name as col_1_0_,
    order0_.order_date as col_2_0_,
    order0_.status as col_3_0_,
    delivery2_.city as col_4_0_,
    delivery2_.street as col_4_1_,
    delivery2_.zipcode as col_4_2_ 
from
    orders order0_ 
inner join
    member member1_ 
        on order0_.member_id=member1_.member_id 
inner join
    delivery delivery2_ 
        on order0_.delivery_id=delivery2_.delivery_id

 하지만 이렇게 하는 경우 쿼리도 짧아서 성능적으로 최적화가 되고 좋지만, 문제는 이를 재사용성을 포기해야 한다는 단점이 존재한다.

 

 먼저 처음 메서드 findAllWithMemberDeliver( )의 경우, 기본적으로 연관된 모든 엔티티의 정보를 다 가져오기 때문에 그 데이터들 속에서 필요시마다 원하는 데이터만 가져와 사용하는 것이 가능하다.

 

 하지만 두번째 메서드 findOrderDtos( )는 내가 가져오고자 하는 데이터가 정말 핏하게 정해져 있기 때문에 이렇게 가져온 데이터에서 추가적으로 어떠한 데이터를 가져온다던가 하는 것이 불가능하다. 그리고 사실 select문 조금 짧아졌다고 해서 성능이 드라마틱하게 좋아지진 않는다.

 그러므로 "내가 어떤 상황에서 사용할 것인지, 유연성을 포기하고 성능을 조금 더 챙길 것인지 등등을 잘 따지고 설계"를 해야한다.

 

정리

엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다. 둘중 상황에 따라 서 더 나은 방법을 선택하면 된다. 엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다. 따라 서 권장하는 방법은 다음과 같다.

 

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다. ( public List<SimpleOrderDto> ordersV2() )
  2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.  ( public List<SimpleOrderDto> ordersV3() )
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.( public List<SimpleOrderDto> ordersV4() )
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다

'Spring > SpringBoot' 카테고리의 다른 글

@SpringBoot- @NotNull VS @Column(nullable=false)  (0) 2024.03.13
SpringBoot- @DeleteMapping  (0) 2023.08.26
SpringBoot- 패치 조인 최적화  (0) 2023.07.06
SpringBoot- API  (0) 2023.07.04
SpringBoot- @Valid, @ResponseBody, @RequestBody  (0) 2023.06.28