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

김슬기·2022년 11월 4일
0
@Bean
Hibernate5Module hibernate5Module()    {
   return new Hibernate5Module();
}

고오오급..

샘플 데이터 입력

  • InitDb 만들기
@Component
@RequiredArgsConstructor
public class InitDb {

    private final InitService initService;

    @PostConstruct
    public void init() {
        initService.dbInit1();
        initService.dbInit2();
    }

    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {

        private final EntityManager em;

        public void dbInit1() {
            System.out.println("Init1" + this.getClass());
            Member member = createMember("userA", "서울", "1", "1111");
            em.persist(member);

            Book book1 = createBook("JPA1 BOOK", 10000, 100);
            em.persist(book1);

            Book book2 = createBook("JPA2 BOOK", 20000, 100);
            em.persist(book2);

            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);

            Delivery delivery = createDelivery(member);
            Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
            em.persist(order);
        }

        public void dbInit2() {
            Member member = createMember("userB", "진주", "2", "2222");
            em.persist(member);

            Book book1 = createBook("SPRING1 BOOK", 20000, 200);
            em.persist(book1);

            Book book2 = createBook("SPRING2 BOOK", 40000, 300);
            em.persist(book2);

            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4);

            Delivery delivery = createDelivery(member);
            Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
            em.persist(order);
        }

        private Member createMember(String name, String city, String street, String zipcode) {
            Member member = new Member();
            member.setName(name);
            member.setAddress(new Address(city, street, zipcode));
            return member;
        }

        private Book createBook(String name, int price, int stockQuantity) {
            Book book1 = new Book();
            book1.setName(name);
            book1.setPrice(price);
            book1.setStockQuantity(stockQuantity);
            return book1;
        }

        private Delivery createDelivery(Member member) {
            Delivery delivery = new Delivery();
            delivery.setAddress(member.getAddress());
            return delivery;
        }
    }

v1: 엔티티 직접노출 성능최적화

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
    private final OrderRepository orderRepository;

    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1(){
        List<Order> all =orderRepository.findAllByString(new OrderSearch());
        return all;
    }
}
  • 이것을 그대로 api호출 시 무한루프에 빠짐
  • why??
    • Member안에 order리스트가 있고
    • 또 order안에도 member이 있기때문에 계에에에에에속 돈다.~~
    • 양방향 연결관계이므로~
    • 그래서 둘중하나의 속성에 @JsonIgnore걸어주기
  • 그런데 이렇게 해도 실행 오류 뜬다
    • order 안에 member가 fetch = LAZY로 설정되어있기 때문이다.

    • 이건 지연로딩이라 멤버를 가져올때 프록시객체로 가져오는데 이것은 가짜 객체이고 그러므로 오류가 발생( ByteBuddyInterceptor이라는 클래스의 객체가 들어가있다.)

    • 이러고 나중에 order안의 member를 접근할때 그제서야 디비에서 가져옴~~(프록시 초기화)

    • 그래서 호출해서 띄워줄때 오류가 발생

    • 이 경우 하이버네이트한테 이 작동을 하지말라고 명령해야함
      - 그걸위해 하이버네이트5모듈등록~

      `implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'`
      
      - 이것을 build.gradle에 추가하고
      
      ```
      @Bean
      Hibernate5Module hibernate5Module()    {
         return new Hibernate5Module();
      }
      ```
      
      - 이것을 스프링 실행 메인함수에 집어넣어서 실행해준다
      - 그러면 오류는 안나지만 LAZY설정된 객체들은 이제 모두  null 로뜸
          - 이것은 이제 저 설정이 지연로딩되는 객체들을 일단  null로 뽑아내자는 설정이기떄문
          - 설정을 변경하면 바로 다 뜨게할수있다.
          - 하지만 그렇게되면 엔티티가 노출되어버림
              - 필요한것만 뽑아쓰도록하자 ~~~/
      @GetMapping("/api/v1/simple-orders")
      public List<Order> ordersV1(){
          List<Order> all =orderRepository.findAllByString(new OrderSearch());
          for (Order order : all){
              order.getMember().getName(); // LAZY 강제 초기화
          }
          return all;
      }
  • 이렇게api에 for문을 추가하여 모든 주문의 member의 멤버중 아무거나에 접근하면 LAZY를 강제로 초기화시킬수있음
    • order.getMember()여기까지는 프록시 객체
    • order.getMember().getName()여기서부터가 이제 진짜 객체로 반환됨
  • EAGER로 (즉시로딩으로 ) 설정하면 그 멤버가 필요하지 않는 경우에도 뽑아내서 성능최적하가 말도안되게 어려워진다. 그러므로 설정금지~

엔티티를 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;
}
@Data
static class SimpleOrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate; //주문시간 private OrderStatus orderStatus;
    private Address address;
    private OrderStatus orderStatus;
    public SimpleOrderDto(Order order) {
        orderId = order.getId();
        name = order.getMember().getName();
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress();
    } }
  • 직접 엔티티를 노출하지 않는다는 장점은 있다
  • 하지만, 단점도 존재
  • LAZY객체의 쿼리 실행 횟수가 많아지게 된다.
    • order 조회 1번
    • order→ member조회 횟수 N 회 (리스트안의 내용물갯수 )( LAZY객체)
    • order→ delivery 조회 횟수 N 회 (리스트안의 내용물갯수 )( LAZY객체)
    • 총 2N +1회로 실행횟수가 많아짐
    • 즉, 실행시간이 길어져버린다.
    • 단, 총 실행시간안에 이미 한번 조회했던 지연로딩 객체의 경우 다시 조회하지않음 N-1회가 된다.

페치조인 최적화

  • 위의 경우 조회시간이 너무 길어진다는 단점이 생겨버림~~
  • 그걸 보안하기위해 멤버리포지토리에 이러한 메서드를 추가하고
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();
}
  • 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(Collectors.toList());
    return result;
}
  • join fetch이녀석을 사용하여 member, delivery를 쿼리 한번에 조회해버린다.
  • 위에 설명했던대로 이미 조회된 상태라면 지연로딩 x이므로, 실행시간 단축~~
    • 여기서는 2N+1 이었던 조회횠수가 1번으로 확줄었다!
  • join fetch는 무조건 더 공부해보기!!!!
  • 여기까지 해도 대부분의 성능문제는 해결!

JPA에서 DTO바로 조회

@Data
public class SimpleOrderQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate; //주문시간 private OrderStatus orderStatus;
    private Address address;
    private OrderStatus orderStatus;
    public SimpleOrderDto(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;
    }
}
  • repository에 SimpleOrderQueryDto 만들어주기
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();
}
  • select new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address) 이렇게 생성자를이용해서 DTO를 생성해준다
  • 생성자의 매개변수를 쿼리에서 가져온다. from, join이용
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
    return orderRepository.findOrderDtos();
}
  • 이 V4의 장점은 V3와 나머진 같지만 select절에서 가져오는 정보가 현저히 차이가있다.
    • V4로하면 원하는데이터만 가져옴
    • V3는 멤버, 이런 객체들 전부 가져오긴함~
    • 그렇지만 두개의 우열을 가리긴 어렵다.
    • v4는 재사용성이 없다.
      • SQL문으로 그냥 원하는 데이터만 가져온거기때문에 다른데이터를 가져오려면 저기 있는 쿼리문장을 바꿔야하므로findOrderDtos()자체를 바꿔야함
      • 또 리포지토리 재사용성이 떨어짐 >>> 새로 만들어야하니깐~
        • 또 api스펙에 맞춘 코드가 리포지로 들어가는것도 단점
      • 하지만 V3 는 애초에 객체를 가져오되 노출을 시키지않는것이므로 호출 후 다른 속성들에 접근가능~
      • 로직재활용의 문제~
      • 난 V3가 맘에든다
  • 쿼리 방식 선택 권장 순서
    1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
    2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
    3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
    4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
profile
낭만그리고김슬기

0개의 댓글