JPA

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

송경훈 2023. 5. 29. 04:19
반응형
package jpabook.jpashop.api;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.*;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toList;

/**
 * xToOne(ManyToOne, OneToOne) 관계 최적화
 * Order
 * Order -> Member
 * Order -> Delivery
 */
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    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) { //이게 없다면 강제 지연 로딩 설정(JpashopApplication에 설정해둔 것)을 해야 하는데
            //이것은 전체 다 하는 거기 때문에그러면 원하지 않는 것들도 다 뿌려진다.
            //그래서 따로 for루프를 돌리면서 필요한 것들만 강제 지연 로딩을 해준다
            order.getMember().getName(); //order.getMember() 여기 까지는 프록시 객체 -> .getName() 이거까지 하면 Lazy 강제 초기화 -> 멤버에 쿼리를 날려서 JPA(hibernate)가 다 끌고옴
            order.getDelivery().getAddress(); //Lazy 강제 초기화
        }
        return all; //만약 위의 for문이 없이 이대로 반환한다면 Order -> Member -> Order -> Member ... 무한루프에 빠지게 됌(양방향 연관관계의 문제)
        //양방향 연관관계의 문제를 해결하기 위해서 둘 중 하나는 @JsonIgnore를 해줘야함. 그래서 Order와 Member중 member에 추가함.
        //그리고 OrderItem에도 Order로 가는게 있기 때문에 여기에도 @JsonIgnore 추가 / Delivery에도 추가
        //이렇게 양방향인 것 중 하나에 다 @JsonIgnore 추가
    }

    /**
     * V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X)
     * - 단점: 지연로딩으로 쿼리 N번 호출
     */
    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        //ORDER 2개
        //N + 1 -> 1 + 회원 N + 배송 N = 5
        List<Order> orders = orderRepository.findAllByString(new OrderSearch()); //이걸 이대로 반환하면 안되고 <SimpleOrderDto> 이 타입으로 바꿔야 함

        List<SimpleOrderDto> result = orders.stream() //그러기 위해서 for문도 가능한데 여기서는 스트림 사용
                .map(o -> new SimpleOrderDto(o))//바꿔치기
                .collect(Collectors.toList());

        return result;
    }
    @Data
    static class SimpleOrderDto { //API 스펙을 명확하게 규정
        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 초기화 - 영속성 콘텍스트가 memberId를 가지고 영속성 콘텍스트에 찾아봄. 없으면 DB 쿼리에 날림
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress(); //LAZY 초기화
        }
    }

}