<aside> ❗

조회용 샘플 데이터 입력

@Component
@RequiredArgsConstructor
public class InitDB {
    private final InitService initService;
    @PostConstruct
    public void init() {
        initService.dbInit1();
        initService.dbInit2();
    }

    // init 메서드에 코드를 추가하지 않고 서로 다른 빈으로 등록해서 호출하는 이유
    // @Transactional으로 인해 정상적으로 동작 안 할 수 있음
    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {
        private final EntityManager em;
        public void dbInit1() {
            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);
            Order order = Order.creatOrder(member, createDelivery(member),
                    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);
            Delivery delivery = createDelivery(member);
            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4);
            Order order = Order.creatOrder(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 book = new Book();
            book.setName(name);
            book.setPrice(price);
            book.setStockQuantity(stockQuantity);
            return book;
        }
        private Delivery createDelivery(Member member) {
            Delivery delivery = new Delivery();
            delivery.setAddress(member.getAddress());
            return delivery;
        }
    }
}

<aside> ❗

지연 로딩과 조회 성능 최적화 - 웬만하면 사용하지 말 것

V1 - 엔티티를 그대로 반환할 때

[해결책]

@Bean
Hibernate5JakartaModule hibernate5Module() {
	return new Hibernate5JakartaModule();
}
@Bean
	Hibernate5JakartaModule hibernate5Module() {
		Hibernate5JakartaModule hibernate5JakartaModule = new Hibernate5JakartaModule();
		hibernate5JakartaModule.configure(Hibernate5JakartaModule.Feature.FORCE_LAZY_LOADING, true);
		return hibernate5JakartaModule;
	}
@GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            // 프록시 객체를 초기화해서 실제 엔티티를 가져오도록 함
            order.getMember().getName();
            order.getDelivery().getAddress();

        }
        return all;
    }

엔티티를 API 응답으로 외부에 노출하는 것은 좋지 않다.

그러므로 DTO로 변환해서 사용하자

지연 로딩을 피하기 위해 즉시 로딩을 사용해서는 안 된다.

연관관계가 필요 없는 경우에도 항상 조회해서 성능 문제가 발생할 수 있다. → 성능 최적화 여지가 확 줄어든다.

항상 지연 로딩을 기본으로 하고

성능 최적화가 필요한 경우 패치 조인을 사용해라

</aside>

<aside> ❗

V2 - 엔티티를 DTO로 변환

@GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());

        return orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
    }

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

        public SimpleOrderDto(Order order) {
            // 필요한 API 스펙만 만들어서 반환하면 된다. 
            this.orderId = order.getId();
            this.name = order.getMember().getName();
            this.orderDate = order.getOrderDate();
            this.orderStatus = order.getOrderStatus();
            this.address = order.getDelivery().getAddress();
        }
    }

[문제점]

<aside> ❗

하이버네이트5 모듈이 없으면 초기화 되었든 되지 않았든

프록시 객체는 JSON으로 변환될 수 없음

</aside>

<aside> ❗

V3 - 페치 조인 최적화

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

[문제점]

<aside> ❗

V4 - JPA에서 DTO로 바로 조회

@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> orderV4() {
	return orderRepository.findOrderDtos();
}
// API 스펙에 맞춘 코드가 리포지토리에 들어가게 됨 (논리적으로 계층이 깨져 있다.)
// API 스펙이 바뀌면 해당 코드를 고쳐야 하는 문제 발생 
// OrderRepository에 넣는게 아니라 리포지토리 패키지 내에 하위 패키지를 만든다. 
// repository.order.simplequery
public List<OrderSimpleQueryDto> findOrderDtos() {
	return em.createQuery(
		"select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, o.member.name, o.orderDate, o.orderStatus, d.address) from Order o " +
                                " join o.member m " +
                                " join o.delivery d ", OrderSimpleQueryDto.class)
                .getResultList();
    }
@Data
public class OrderSimpleQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        // 필요한 API 스펙만 만들어서 반환하면 된다.
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

페치조인 VS DTO 바로 조회

<aside> ❗

정리

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
  2. 필요하면 페치 조인으로 성능을 최적화한다. → 대부분의 성능 이슈 해결
  3. 그래도 안 되면 DTO로 직접 조회하는 방법을 사용한다.
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다. </aside>