-
JPA(2) - 지연 로딩과 조회 성능 최적화카테고리 없음 2024. 1. 22. 11:31
1. 엔티티 직접 노출
private final OrderRepository orderRepository; /** * V1. 엔티티 직접 노출 * - Hibernate5Module 모듈 등록, LAZY=null 처리 * - 양방향 관계 문제 발생 -> @JsonIgnore */ @GetMapping("/api/v1/simple-orders") public List<Order> ordersV1() { List<Order> all = orderRepository.findAllByString(new OrderSearch()); for (Order order : all) { order.getMember().getName(); //Lazy 강제 초기화 order.getDelivery().getAddress(); //Lazy 강제 초기환 } return all; }
- 일단 엔티티 직접 노출부터 문제
- 가져온 것을 바로 리턴하면 무한 루프 문제 발생합니다.
해결법1: @JsonIgnore
Order와 관련된 모든 Lazy로딩 (프록시 객체)에 @JsonIgnore를 사용해야함
양방향은 한쪽에 JsonIgnore를 붙여준다.
//Order @JsonIgnore @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) private List<OrderItem> orderItems = new ArrayList<>(); @JsonIgnore @OneToOne(fetch = LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "delivery_id") private Delivery delivery; //Member @JsonIgnore @OneToMany(mappedBy = "member") private List<Order> orders = new ArrayList<>(); //orderItem @JsonIgnore @ManyToOne(fetch = LAZY) @JoinColumn(name = "order_id") private Order order; //..
해결법2: Hibernate5JakartaModule 등록 -> 지연로딩일 경우 response를 제외시켜준다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5- jakarta'
- 초기화된 프록시 객체만 노출 초기화 안된건 노출 안함
- Json으로 만들 때 강제 지연로딩 설정 : 다 불러와서 초기화하라고 설정하는 것
Hibernate5Module hibernate5Module = new Hibernate5Module(); //강제 지연 로딩 설정 hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true); return hibernate5Module;
해결법 3: 리턴전에 Lazy강제 초기화 -> N+1문제 발생 (애플리케이션 레벨에서 지연로딩 풀어주기)
- 물론 이 옵션을 켜면 order -> member, member -> orders 양방향 연관관계를 계속 로딩해야함 따라서 @JsonIgnore를 한 곳에 주어야함 (엔티티 직접 노출 + 양방향 관계에선 반드시 한쪽에 @JsonIgnore 걸어야함)
- em.find는 성능과 별개 (어차피 식별자로 불러오는거라) 즉시로딩은 jpql에서 성능문제 발생 (연관관계만큼 select)
- 항상 지연로딩 -> 성능 최적화 필요시 페치 조인을 사용
* 3가지 해결법 모두 구린 방법! 결국 엔티티를 그냥 리턴하기위한 궁리일 뿐이다.
2. 엔티티 DTO로 변환
@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; } @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(); } }
- 엔티티를 DTO로 바꾸는 일반적인 방법임
- 하지만 지연로딩때문에 1 + N + N 실행
-> order 1번조회 (member 지연로딩 2개, 주소 지연로딩 2개)
-> 두 지연로딩을 풀기위해 추가로 select 4번 나감
3. DTO 변환 + 페치조인 최적화
@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; } //repository 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(); }
- 패치조인용으로 따로 메서드를 생성했다.
- 패치 조인을 통해 쿼리가 1번만 나가서 엔티티를 채웠다! (이미 조회 상태이므로 지연로딩 x)
4. 한발 더 나아가서 DTO로 직접 조회하기
@GetMapping("/api/v4/simple-orders") public List<OrderSimpleQueryDto> ordersV4() { return orderSimpleQueryRepository.findOrderDtos(); } 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(); }
- value object나 Entity 제외하고, jqpl의 리턴값을 만드려면 new 연산자를 사용해줘야함 (queryDSL에서 이를 해결한다함)
- DTO를 바로 만들어서 리턴하면, 애플리케이션 레벨에서 변환 로직이 빠지고, 조회결과 데이터 수가 줄어서 성능 최적화를 노려볼 수 있음
- 하지만 리포지토리 재사용성이 떨어지고, API 스펙에 맞춘 코드가 리포지토리에 들어감 (상충관계있음)
* 권장 순서
엔티티 -> DTO는 무조건 지켜야한다.
엔티티에 지연로딩이 걸려있어 성능 최적화 필요시 페치조인을 사용한다.
더 나아가 성능을 최적화하고 싶을 때만 DTO직접조회 방식을 사용해본다.
최후의 방법은 네이티브 SQL 혹은 JDBC Template를 사용해서 직접 SQL을 사용한다.