JPA 활용편 2 - API 개발 고급(지연 로딩과 조회 성능 최적화)

Stella·2022년 6월 13일
0

Java

목록 보기
15/18

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

간단 주문 조회 V1: Entity 노출

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

문제 1: 무한루프

  • 서로 양방향 연관관계가 있는 곳에서 무한 루프에 빠짐

해결

  • 양방향이 연관관계 둘 중 한 곳에는 @JsonIgnore 해줘야함

문제 2: 프록시 객체 인식 문제

  • fetch타입이 lazy로 되어 있음. 지연로딩은 db에서 안 가지고 옴 -> Hiberante에서 프록시 라이브러리를 상속받아서 프록시 객체를 넣어둠. order에서 member와 address는 프록시 객체. 우선 프록시 객체를 넣어놓고 member객체의 값을 꺼내거나 손대면 그때 db에 sql를 날려서 member 객체의 값을 넣어줌(프록시 초기화)
  • 순순한 member객체가 아니여서 프록시 객체 인식 문제 발생

해결

  • Hibernate5Module 설치해서 해결 가능
  • data를 다 외부에 노출하면 운영이 어려움. field 중 하나를 외부에서 쓰고있으면 수정하기 어려움. 꼭 필요한 data만 api 스펙에 노출하는 게 좋은 방법!

=> 실무에서 사용하지 않는 방법. entity 외부에 노출하면 안됨! -> DTO로 반환

간단 주문 조회 V2: 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(Collectors.toList());
        return result;

문제: N+1 문제

  • v1과 v2의 공통 문제: lazy 로딩으로 인한 db query가 너무 많이 호출
  • order 하나 당 member와 delivery를 조회하는 query 각각 발생 => N+1 문제(첫번째 쿼리의 결과로 N번만큼 query가 추가실행)

해결

  • fetch join 사용

DTO

 @Data
    static class SimpleOrderDto{
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        public Address address;

        public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName(); //LAZY 초기화. Member id를 가지고 영속성 컨텍스트 찾아옴. DB QUERY 날려서 DATA 가지고옴
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();//LAZY 초기화.
        }
    }
  • Constructor에서 name과 address를 조회할 때 query 날림
  • query가 총 1+N+N번 실행(v1과 query 수가 같음)
    • order 조회 1번
    • oder -> member 지연로딩 조회 N번
    • order -> Delivery 지연로딩 조회 N번
    • 최악의 경우 모든 결과에 N번 실행
      • 지연로딩은 영속성 컨텍스트를 조회하므로, 이미 조회된 경우는 query 생략

간단 주문 조회 V3: DTO + fetch Join

api

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

Fetch Join

  • Lazy 무시하고 프록시 객체 아닌 진짜 객체에 값을 채워서 가지고 옴
  • query 한번으로 조회
  • fetch join으로 order->member, order->delivery 이미 조회된 상태이므로 지연로딩 일어나지 않음

문제: 모든 column 값 반환

  • v1과 v2의 공통 문제: lazy 로딩으로 인한 db query가 너무 많이 호출
  • order 하나 당 member와 delivery를 조회하는 query 각각 발생 => N+1 문제(첫번째 쿼리의 결과로 N번만큼 query가 추가실행)

해결

  • JPQL로 직접 설정한 DTO 반환

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

api

 @GetMapping("api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4(){
        return orderRepository.findOrderDtos();

    }

OrderRepository

 public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.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();
    }
  • new package name.dto(조회하고 싶은 field)를 통해서 JPQL 결과를 DTO로 즉시 변환

OrderSimpleQueryDto

@Data
public class OrderSimpleQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    public 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;

    }
}
  • SQL 처럼 원하는 값 선택 조회
  • v3과 똑같이 query 한번만 나가지만, v3와 다르게 선택된 column만 조회
  • JPQL에 DTO를 직접 설정해서 선택된 field만 조회
  • fetch join은 네트워크를 더 많이 사용 -> 모든 column 조회
  • 하지만 v3와 v4는 우열을 가리기 어려움

V3 vs V4

V3V4
fetch join은 네트워크를 더 많이 사용->모든 column 조회성능 최적화에서는 나음(네트워크 용량 최적화(미비))-> 선택된 column만 조회
많은 곳에서 활용 가능화면에는 최적화 되어있지만 재사용성이 어려움, 이 dto를 사용할 때만 사용가능
API스펙에 맞춘 코드가 Repository에 들어감 -> queryRepository를 따로 만들어서 사용
  • resporitory는 가급적이면 순수한 entity를 조회하는데 사용

query 방식 선택

  • Entity를 DTO로 변환하거나, DTO로 바로 조회하는 방법 둘다 장단점이 있기 때문에,상황에 따라서 더 나은 방법 선택

쿼리 방식 선택 권장 순서
1. 우선 Entity를 DTO로 변환하는 방법을 선택
2. 필요하면 페치 조인으로 성능을 최적화(대부분의 성능 이슈 해결 가능)
3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용
4. 최위의 방법으로 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용

profile
Hello!

0개의 댓글