API 개발 고급 - 지연 로딩과 조회 성능 최적화

유동우·2023년 8월 14일
0
post-thumbnail

수강하기 전

실무에서 JPA를 사용하려면 앞으로 나오는 내용을 100% 이해해야 한다.

예제

  • 주문 + 배송정보 + 회원을 조회하는 API
  • 지연 로딩으로 인한 성능 문제를 단계적으로 해결

간단한 주문 조회 V1 : 엔티티를 직접 노출

@GetMapping("/api/v1/simple-orders")
private List<Order> ordersV1() {
    List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
    
    return all;
}

음.. 그러니까
위 v1 버전을 사용했을 경우 문제점을 나열해보자

문제점:
Order 객체의 필드의 member와, Member 객체의 필드의 orders가 (다른것도 존재)
서로 무한루프를 돌게된다.

해결법:
이러한 문제를 해결하려면 양방향 중 한 쪽에다가 @JsonIgnore 을 설정해야 한다.
이뿐만 아니라, 지연로딩(FetchType.LAZY)로 설정해준 것들을 모두 EAGER로 바꿔야한다.

하지만 이때 다른 API 스펙과 호환성 및 성능 튜닝을 고려하여 EAGER로 바꾸면 안되고
Hibernate5Module 라이브러리를 추가하여 강제지연로딩 (Hibernate5Module.Feature.FROCE_LAZY,LOADING) 을 해주어야 한다.
(중요하지 않기 때문에 코드 생략)

Hibernate를 직접 가져다 쓰는것은 좋지않기 때문에 지연로딩된 것을 강제 초기화 할 수 있다

order.getMember().getName() //Lazy 강제 초기화
order.getDelivery().getAddress() //Lazy 강제 초기화

//getName(), getAddress() 시점에 초기화

아무튼 결론은 엔티티를 직접 노출하는 것은 좋지 않다

간단한 주문 조회 V2 : 엔티티를 DTO로 변환

@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
      List<Order> orders = orderRepository.findAllByCriteria(new OrderSearch());
      List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());

      return result;
}

orders가 2개가 조회되고, 그에 따라 서로 다른 members가 조회되어
총 쿼리는 1 + N + N 번 실행된다
1 => orders
2 => member
2 => delivery


만약 같은 멤버라면 (member_Id가 같다면)
(자주 있는 경우는 아님, 보통 최악의 경우를 가정하는게 좋다)

public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
}

첫 번째 멤버 조회
order.getMember().getName(); 을 처음 실행했을때는 영속성 컨텍스트에 없기때문에 member를 가져온다.
order.getDelivery().getAddress();을 처음 실행했을때는 영속성 컨텍스트에 없기때문에 delivery를 가져온다.

두 번째 멤버 조회
order.getMember().getName(); 이미 같은 user를 영속성 컨텍스트에 조회를 했기 때문에 다시 조회하지 않고 바로 delivery를 조회한다

1 + 1 + 2
1 => orders
1 => member
2 => delivery

간단한 주문 조회 V3 : 엔티티를 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(Collectors.toList());
                
     return result;
}

V2와 코드는 동일하지만, findAllWithMemberDelivey( ) 라는 메서드를 새로 생서한다.
이 메서드는 order_Id를 조회하는 동시에 member와 delivery를 fetch join 하여 모두 한 번에 가져오는 메서드이다.

//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();
}

패치 조인을 하게되면 프록시값이 아닌 본래의 값을 가져오고, 지연로딩의 영향을 받지 않게된다. (자세한건 ORM 기본편 참고)

그리고 항상 마지막엔 List로 감싸주어야 한다.

간단한 주문 조회 V4 : JPA에서 DTO로 바로 조회

결론 : v4가 v3보다 select 절이 조금 나가지만, 결과적으로 성능을 따져보면 별로 차이가 나지 않는다

Reference
김영한 님 - 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

profile
효율적이고 꾸준하게

0개의 댓글