본문 바로가기
개발일지/JPA

JPA 사용 시 쿼리 방식 선택 권장 순서(컬렉션 조회 안할 때)

2022. 8. 3.

JPA 사용 시 컬렉션 조회 안할 때 쿼리방식 선택 권장 순서

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

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

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

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

(4번 까지 갈일은 거의 없다.)

 

2번 까지 사용하는 경우와 3번 까지 사용하는 경우를 비교하면

2번 까지 사용하는 경우는 다음처럼 엔티티를 fetch join해서 엔티티 전체 컬럼 값을 가져오는 것이다.

// v3 방법
public List<Order> findAllWithMemberDelivery() {
    /*
    프록시도 아니고 진짜 객체값를 채워서 가져온다.
    fetch 라는 명령어는 sql은 없고 jpa에만 있는 문법이다.
    실무에서 jpa를 쓰려면 100% 이해해야한다.
     */
    return em.createQuery(
            "select o from Order o" +
                    " join fetch o.member m"+
                    " join fetch o.delivery d", Order.class
    ).getResultList();
}

이렇게 조회 하면 로그는 다음과 같다.

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_.memder_id as memder_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_.memder_id=member1_.member_id 
    inner join
        delivery delivery2_ 
            on order0_.delivery_id=delivery2_.delivery_id

 

3번 까지 사용하는 경우는 다음 처럼 원하는 컬럼들만 조회한다.

// DTO를 직접 조회하는 v4 방법
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();
}

로그는 다음과 같이 원하는 컬럼들만 찍힌다.

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_.memder_id=member1_.member_id 
    inner join
        delivery delivery2_ 
            on order0_.delivery_id=delivery2_.delivery_id

 

 

참고로 둘다 다음 SimpleOrderDto를 사용한다.

그러므로 반환해주는 값은 똑같다.

@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(); // LAZY 초기화 - 영속성 컨텍스트가 멤버ID를 가지고 영속성 컨텍스트에서 찾아본다. 근데 없으면 DB에 쿼리를 날린다.
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress(); // LAZY 초기화 -> 마찬가지
    }
}

 

 

v4 방법은 내가 원하는 것만 select 할 수 있다. v3와 log 비교해보면 알 수 있다.
그런데 무조건 v4 가 좋다고 볼수 없다 trade off 관계이다.
즉 v4는 원하는 값만 가져오기 때문에 재사용성이 떨어진다. 하지만 v3는 테이블 차체 정보를 다 가져와서 재사용성이 높다.
즉, v4는 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점이 있다.
하지만 성능 최적화에서는 v4가 v3보다는 조금 더 좋다.
(성능이 그렇게 차이 나지는 않는다. 데이터 사이즈가 클 때는 차이가 좀 있을 수 있다. 또는 굉장히 요청이 많으면 v4를 선택하는 게 좋을 수도 있다.)

이런 트레이드 오프 관계를 잘 이용하면 되는데 v4는 QueryDSL로 해결할 수 있을 것이다.