JPA를 사용해 지연 로딩(Lazy Loading)으로 인해 발생하는 성능 문제를 해결하는 다양한 방법을 설명합니다. 주문 + 배송 정보 + 회원 정보를 조회하는 API를 예시로, 성능 최적화를 단계별로 어떻게 접근할 수 있는지 살펴보겠습니다.
가장 먼저, 엔티티를 직접 외부로 노출하는 방식입니다.
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); // 지연 로딩 강제 초기화
order.getDelivery().getAddress(); // 지연 로딩 강제 초기화
}
return all;
}
문제점:
@JsonIgnore를 적용해야 합니다.V2에서는 엔티티 대신 DTO를 사용하여 응답을 반환합니다.
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
return orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
}
@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();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
장점:
단점:
V3에서는 페치 조인을 사용해 지연 로딩을 없애고, 한 번의 쿼리로 필요한 데이터를 모두 가져옵니다.
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
return orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
}
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();
}
장점:
단점:
V4에서는 JPA에서 DTO로 바로 조회하는 방법을 사용해, 성능 최적화를 극대화합니다.
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
@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();
}
}
장점:
단점:
조회 성능을 최적화할 때는 상황에 맞는 방법을 선택하는 것이 중요합니다:
1. 우선 엔티티를 DTO로 변환하는 방법을 사용하세요.
2. 필요에 따라 페치 조인으로 성능을 최적화합니다. 대부분의 성능 문제가 해결됩니다.
3. 그래도 성능 이슈가 해결되지 않으면, DTO로 바로 조회하는 방법을 고려하세요.
4. 최후의 방법으로 네이티브 SQL이나 스프링 JDBC Template을 사용할 수 있습니다.