@JsonIgnore 을 추가하더라도 지연로딩으로 인한 proxy(bytebuddy) 객체를 jackson 라이브러리가 읽을 수 없는 문제 발생 Type definition error
Hibernate5Module 을 Spring Bean 으로 등록하면 해결 가능
단, 지연 로딩 객체는 null 출력, 강제 지연 로딩도 가능하지만 성능 악화 발생
// OrderSimpleApiController.java@GetMapping("/api/simple-orders")publicList<Order>ordersV1() {List<Order> all =orderRepository.findAllByString(newOrderSearch());for (Order order : all) {order.getMember().getName(); // Lazy 강제 초기화order.getDelivery().getAddress(); // Lazy 강제 초기화 }return all;}
엔티티를 DTO로 변환
엔티티를 직접 노출하지 않고, 엔티티를 DTO로 변환
1 + N 문제 발생 (엔티티 직접 노출과 동일)
첫 번째 쿼리의 결과 N번 만큼 쿼리가 추가로 실행되는 문제
ex) Order 조회 시 Member - N번, Delivery - N 번, 총 1 + N + N 개의 쿼리 발생
// OrderRepository.javapublicList<Order>findAllWithMemberDelivery() {returnem.createQuery("select o from Order o"+" join fetch o.member m"+" join fetch o.delivery d",Order.class).getResultList();}
DTO로 바로 조회
조회된 엔티티를 DTO로 변환하는 과정 필요 없이, 바로 DTO 조회해서 성능 최적화하기
원하는 필드만 선택(SELECT)해서 조회
DB <-> 네트워크 용량 최적화 (생각보다 미비한 차이)
new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
API 스펙에 맞추다보니 변경이 어려우므로, 다른 API에서 Repository 재사용이 어려움
사용할 경우 순수한 엔티티를 조회하는 레파지토리와 화면 종속적인 레파지토리를 분리하는 것을 추천
// OrderRepository.javapublicList<OrderSimpleQueryDto>findOrderDtos() {returnem.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();}
컬렉션 조회 최적화
toOne(OneToOne, ManyToOne)관계에 이어서 컬렉션인 일대다 관계(OneToMany)를 최적화해보자.
toOne 관계와 동일한 부분들이 포함되어 있다.
.
엔티티를 직접 노출
엔티티가 변하면 API 스펙이 변함
트랜잭션 안에서 지연 로딩(LAZY) 강제 초기화 필요
양방향 연관관계 문제 (@JsonIgnore 설정으로 해결 가능하지만 지연로딩 객체를 읽을 수 없는 문제 발생할 수 있음)
// OrderApiController.java@GetMapping("/api/orders")publicList<Order>ordersV1() {List<Order> all =orderRepository.findAllByString(newOrderSearch());for (Order order : all) {order.getMember().getName(); // Lazy 강제 초기화order.getDelivery().getAddress(); // Lazy 강제 초기환List<OrderItem> orderItems =order.getOrderItems();orderItems.stream().forEach(o ->o.getItem().getName()); // Lazy 강제 초기화 }return all;}
엔티티를 DTO로 변환
트랜잭션 안에서 지연 로딩 필요 (지연 로딩으로 너무 많은 SQL 실행)
지연 로딩은 영속성 컨텍스트에 있는 엔티티 사용을 시도하고 없으면 SQL을 실행
ex) Order 조회 시 Member - N번, Address - N 번, OrderItem - N 번, item M번
publicList<OrderQueryDto>findOrderQueryDtos() {// 루트 조회 : ToOne 관계를 한 번에 조회 (1 번의 쿼리 N 개의 Row)List<OrderQueryDto> result =findOrders();// 컬렉션 조회 : 컬렉션은 루프를 돌면서 별도로 조회 (N 번의 쿼리)result.forEach(o -> {List<OrderItemQueryDto> orderItems =findOrderItems(o.getOrderId());o.setOrderItems(orderItems); });return result;}privateList<OrderQueryDto>findOrders() {returnem.createQuery("select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)"+" from Order o"+" join o.member m"+" join o.delivery d",OrderQueryDto.class).getResultList();}privateList<OrderItemQueryDto>findOrderItems(Long orderId) {returnem.createQuery("select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)"+" from OrderItem oi"+" join oi.item i"+" where oi.order.id = : orderId",OrderItemQueryDto.class).setParameter("orderId", orderId).getResultList();}
컬렉션 조회 최적화
IN 절을 활용해서 메모리에 미리 조회 후 최적화
루트 1 번, 컬렉션 1 번 조회
Map을 사용하여 매칭 성능 개선 - O(1)
ToOne 관계를 먼저 조회한 후, 얻은 식별자 Id로 ToMany 관계를 한꺼번에 조회
위 방법과 비교하면
발생하는 N + 1 문제를 1 + 1 로 해결
코드가 복잡해지긴 하지만, 다수의 데이터를 한 번에 조회 할 경우 환경에 따라 100배 이상 성능 최적화 가능
보통 많이 사용하는 방법
publicList<OrderQueryDto>findAllByDto_optimization() {// 루트 조회 : ToOne 관계를 한 번에 조회 (1 번의 쿼리)List<OrderQueryDto> result =findOrders();// 컬렉션 조회 : IN 절을 활용하여 한 번에 조회 (1번의 쿼리)Map<Long,List<OrderItemQueryDto>> orderItemMap =findOrderItemMap(toOrderIds(result));// 루프를 돌면서 컬렉션 세팅result.forEach(o ->o.setOrderItems(orderItemMap.get(o.getOrderId())));return result;}privateList<Long>toOrderIds(List<OrderQueryDto> result) {returnresult.stream().map(o ->o.getOrderId()).collect(Collectors.toList());}privateMap<Long,List<OrderItemQueryDto>>findOrderItemMap(List<Long> orderIds) {List<OrderItemQueryDto> orderItems =em.createQuery("select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)"+" from OrderItem oi"+" join oi.item i"+" where oi.order.id in :orderIds",OrderItemQueryDto.class).setParameter("orderIds", orderIds).getResultList();returnorderItems.stream().collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));}
플랫 데이터 최적화
JOIN 결과를 그대로 조회한 후, 애플리케이션에서 원하는 스팩으로 직접 변환
쿼리를 한 번 실행하는 장점이 있지만,
조인으로 생기는 중복 데이터가 DB에서 애플리케이션으로 전달되어 상황에 따라 위 방법보다 느릴 수 있음