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

9. 바로 Dto로 받기

sdafdq 2023. 11. 14. 08:44
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4(){
    return orderRepository.findOrderDtos();
}

바로 리포지토리에서 Dto로 가져오는 걸 정의하는 거다.

 

public List<OrderSimpleQueryDto> findOrderDtos() {
    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();
}

이렇게. 그런데 이렇게 하려면 위 처럼 엔티티가 아니면 못 읽으니 

패키지까지 다써서, 생성자도 따로 저렇게 만들어 주면 된다.

 

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

 

쿼리는

    select
        o1_0.order_id,
        m1_0.name,
        o1_0.order_date,
        o1_0.status,
        d1_0.city,
        d1_0.street,
        d1_0.zipcode 
    from
        orders o1_0 
    join
        member m1_0 
            on m1_0.member_id=o1_0.member_id 
    join
        delivery d1_0 
            on d1_0.delivery_id=o1_0.delivery_id
2023-11-14T08:17:53.981+09:00  INFO 22024 --- [nio-8080-exec-2] p6spy                                    : #1699917473981 | took 0ms | statement | connection 7| url jdbc:h2:tcp://localhost/./jpashop
select o1_0.order_id,m1_0.name,o1_0.order_date,o1_0.status,d1_0.city,d1_0.street,d1_0.zipcode from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id
select o1_0.order_id,m1_0.name,o1_0.order_date,o1_0.status,d1_0.city,d1_0.street,d1_0.zipcode from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id;

이렇게 딱 필요한 것만 나간다.

 

JPA는 기본적으로 엔티티나 밸류오브젝트(임베디드타입)만 반환할 수 있다.

 

JPQL에서 저렇게 o 하면 기본적으로 넣는건 식별자를 넣는다고 한다.(왜 where할때도 엔티티로 비교해서 나가는 쿼리는 id니까) 그래서 이렇게 따로 값을 자세히 지정해 주는 것이다.

 

 

 

그러면,

@GetMapping("/api/v3/simple-orders")
public List<OrderSimpleQueryDto> orderV3(){
    List<Order> orders = orderRepository.findAllWithMemberDelivery();

    List<OrderSimpleQueryDto> result = orders.stream()
            .map(o-> new OrderSimpleQueryDto(o))
            .collect(Collectors.toList());

    return result;
}
public List<Order> findAllWithMemberDelivery(){
    return em.createQuery(
            "select o from Order o" +
                    " join fetch o.member m" +
                    " join fetch o.delivery d", Order.class
    ).getResultList();
}
    select
        o1_0.order_id,
        d1_0.delivery_id,
        d1_0.city,
        d1_0.street,
        d1_0.zipcode,
        d1_0.status,
        m1_0.member_id,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.name,
        o1_0.order_date,
        o1_0.status 
    from
        orders o1_0 
    join
        member m1_0 
            on m1_0.member_id=o1_0.member_id 
    join
        delivery d1_0 
            on d1_0.delivery_id=o1_0.delivery_id

v4와 v3을 비교해서 뭐가 더 좋을까?

둘 다 트레이드 오프가 있다. 상황에 따라 쓰면 된다.

 

 

v4는 정확히 필요한 열만 가져온다. 성능 상의 약간의 이점이 있다.

하지만 리포지토리가 api에게 의존하게 된다는 점, 저렇게 코드가 약간 더러워 진다는 점, 그리고 v3처럼 joint fetch로 여러가지 값을 가져오는 게 아니라서 딱 저거에만 맞춰진 거니 재사용성에서는 조금 밀리긴 한다.

 

그리고 보통 저렇게 열을 몇개 더 가져온 다고, 성능 상 크게 차이가 나지는 않는다. join할때나, 페이징 등에서 크게 차이가 나지..

 

v3은 저렇게 쓸데없는 열을 몇개 더 가져와서 성능상 약간 불리한 점이 있을 수 있다.

근데 정말 가지고 오는 열의 차이가 많다면 v4도 고려 해야 한다.

 

v3은 엔티티 객체 그래프를 끌고 오는 거니, 정말 join해서 가져오는 데 그 그것들의 하나의 값 들만 사용하는데 막 join 7개 8개 하면.. 

이럴 경우 고려할만 할 것 같다.

 

 

 

리포지토리는 기본적으로 순수한 엔티티를 조회하는데 사용하여야 한다.

 

 

근데 v4의 경우..

이럴 경우 해결책 하나는, 그냥 아예 저런 쿼리용 클래스를 하나 따로 만든다.

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
    private final EntityManager em;
    public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery("select " +
                        "new jpabook.jpashop.repository.order.simplequery.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();
    }
}

이렇게 아예 따로 만들고, 다른 여러 쿼리들도 여기다 정의하면 좋을 듯 하다. 많이 쓸 방법 일 듯.

 

 

쿼리 방식 선택 순서

우선 엔티티를 DTO로 변환하는 방법을 선택

필요하면 join fetch로 성능 최적화 (대부분의 성능 이슈 해결)

그래도 안되면 DTO로 직접 조회하는 방법 사용.

최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBCTemplate 등 SQL을 직접 사용.

 

그러니까 더 짧게 말하면, 일단 대부분 그냥 join fetch를 쓴다.

그런데 그래도 성능 상 조금 하자가 있는, 그러니까 막

테이블 7-8개 join 해 오는 데 그 테이블 중 값 하나씩만 쓰고 그런 거 

그런 거는 그냥 이렇게 따로 만듦.

그래도 안되겠는 거는 아예 sql 자체를 직접 씀.

 

보통 한 95%는 그냥 join fetch 쓰면 해결 됨.

'스프링데이터 + JPA > API 개발' 카테고리의 다른 글

11. 컬렉션 엔티티를 Dto로  (0) 2023.11.14
10. 컬렉션 조회 최적화  (0) 2023.11.14
8. fetch join 최적화  (0) 2023.11.13
7. 엔티티를 Dto로  (0) 2023.11.13
6. 지연로딩과 조회성능 최적화  (0) 2023.11.13