[Spring Boot2][3] 2. API 개발 고급 - 지연 로딩과 조회 성능 최적화(1)

🏷 조회용 샘플 데이터 입력

API 개발 고급 설명을 위해 샘플 데이터를 입력하자😃

  • userA
    • JPA1 BOOK
    • JPA2 BOOK
  • userB
    • SPRING1 BOOK
    • SPRING2 BOOK

✔️ InitDB 클래스 생성

package jpabook.jpashop;

import jpabook.jpashop.domain.*;
import jpabook.jpashop.domain.item.Book;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;

/**
 * 총 주문 2개
 * * userA
 * 	 * JPA1 BOOK
 * 	 * JPA2 BOOK
 * * userB
 * 	 * SPRING1 BOOK
 * 	 * SPRING2 BOOK
 */
@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 : 엔티티를 직접 노출

주문 + 배송정보 + 회원을 조회하는 API를 만들어 보자☺️
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해 볼 것이다!


✔️ OrderSimpleApiController

@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) {
            order.getMember().getName(); // Lazy 강제 초기화
            order.getDelivery().getAddress(); // Lazy 강제 초기화
        }
        return all;
    }
}

🤚🏻 양방향 연관관계가 있을 땐, 한 쪽에 @JsonIgnore 추가
안 그러면 양쪽을 서로 호출하면서 무한 루프가 걸리게 된다! 조심!

  • 엔티티를 직접 노출하는 것은 좋지 않다! (이미 많이 설명함^_^)
  • order ➡️ memberorder ➡️ address 는 지연 로딩이다.
    따라서 실제 엔티티 대신에 프록시 존재
  • jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름 ➡️ Hibernate5Module 을 스프링 빈으로 등록하면 해결!!(스프링 부트 사용중)

📌 build.gradle에 다음 라이브러리 추가

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

📌 Hibernate5Module 등록
JpashopApplication 에 다음 코드를 추가!

    @Bean
    Hibernate5Module hibernate5Module() {
        return new Hibernate5Module();
    }

➡️ 이렇게 설정하면, 기본적으로 초기화 된 프록시 객체만 노출하고 초기화 되지 않은 프록시 객체는 노출 안함!


🤚🏻 주의할 점 정리

1️⃣ 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리 해야 한다.

2️⃣ 앞에서 계속 강조했듯이 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다🙁
➡️ 따라서 Hibernate5Module 를 사용하기 보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법이다!

3️⃣ 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다!
➡️ 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용하자!



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

✔️ OrderSimpleApiController - 추가

    /**
     * V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X) * - 단점: 지연로딩으로 쿼리 N번 호출
     */
    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<SimpleOrderDto> result = orders.stream()
                // order를 SimpleOrderDto로 바꿔치기
                .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번 실행된다!(v1과 쿼리수 결과는 같음)
    • order 조회 1번(order 조회 결과 수가 N이 됨)
    • order ➡️ member 지연 로딩 조회 N번
    • order ➡️ delivery 지연 로딩 조회 N번
    • 예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우)
      • 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.


초쿰,,, 많이,,,, 어렵네요,,,,,

좋은 웹페이지 즐겨찾기