이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


API를 만들 때 엔티티 자체를 리턴하는 방식을 절대로 사용하면 안된다.

기본적으로 엔티티를 DTO로 바꾸어서 리턴해야 한다.

 

또한 트래픽이 낮다면 굳이 성능 최적화가 필요 없겠지만, 사용자가 늘어난다면 성능 최적화를 고려해야한다.

 

성능 최적화 순서

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

 

엔티티를 DTO로 변환

@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 초기화
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress(); //Lazy 초기화
    }
}

/**
 * V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X) * - 단점: 지연로딩으로 쿼리 N번 호출
 */
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    List<SimpleOrderDto> result = orders.stream()
            .map(o -> new SimpleOrderDto(o))
            .collect(toList());
    return result;
}

이 방식의 가장 큰 문제점은 N+1 문제가 발생한다.

즉, DB에 쿼리가 예상했던 것 보다 많이 날라가는 문제가 발생한다.

 

FETCH JOIN으로 성능 최적화

/**
 * V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
 * - fetch join으로 쿼리 1번 호출
 * 참고: fetch join에 대한 자세한 내용은 JPA 기본편 참고(정말 중요함)
 */
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
    List<Order> orders = orderRepository.findAllWithMemberDelivery();
    List<SimpleOrderDto> result = orders.stream()
            .map(o -> new SimpleOrderDto(o))
            .collect(toList());
    return result;
}
//orderRepository
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();
}

이 방식은 N+1 문제가 해결되지만 필요 없을 수도 있는 컬럼마저 가져온다는 단점이 있다.

 

 

DTO로 직접 조회하는 방법

이 방법은 JPQL을 통해서 원하는 컬럼 정보만 가져오는 쿼리를 작성하고, 작성한 쿼리에 맞는 DTO 객체를 반환하는 방법이다.

성능이 확실히 올라가긴 하지만, 특수한 경우가 아니라면 굳이 이렇게까지 안해도 된다.

/**
 * V4. JPA에서 DTO로 바로 조회
 * - 쿼리 1번 호출
 * - select 절에서 원하는 데이터만 선택해서 조회
 */
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
    return orderSimpleQueryRepository.findOrderDtos();
}
@Data
public class OrderSimpleQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    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;
    }
}
@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();
    } 
}
  • 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
  • SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트워크 용량 최적화(생각보다 미비)
  • 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

'Java Category > JPA' 카테고리의 다른 글

[Spring Data JPA] Spring Data JPA  (0) 2024.08.12
[JPA] 컬렉션 조회 최적화(OneToMany)  (0) 2024.08.11
[JPA] 병합(Merge)과 변경 감지(Dirty Checking)  (0) 2024.08.05
[JPA] JPQL 고급  (0) 2024.08.03
[JPA] JPQL 기본 문법  (0) 2024.07.29