[스프링부트JPA_활용2] Section3 - API개발고급-지연로딩과 조회 성능 최적화

JiMin LEE·2022년 11월 3일
0

[Spring]JPA활용2

목록 보기
3/6

1️⃣ 간단한 주문 조회 V1 : 엔티티를 직접 노출

⭐ 요약
  • 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳에 @JsonIgnore 처리해야 한다.
  • 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다. 따라서 DTO로 변환해서 반환하는 것이 더좋은 방법이다.
  • 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다!
    즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.
  • 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용하자!
  • 핵심
  1. order 조회
  2. order에서 member와 연관이 걸리게 하는 것
  3. order에서 delivery와 연관이 걸리게 하는 것
  • XToOne
    • order - member ManyToOne
    • order - delivery OneToOne

api.OrderSimpleApiController

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    @GetMapping("/api/v1/simple-orders")
    public List<Order> orderv1(){
        List<Order> all = orderRepository.findAll(new OrderSearch());
        return all;
    }
}

첫번째 에러

  1. member의 order로 접근하면 order의 member로 다시 접근하는 식으로 무한 루프에 빠진다
  • 해결법

    • order와 관련 있는 객체 클래스로 가서 order 정의 위에 @JsonIgnore 를 붙여주어야 된다.
    • domain.member
    ...
    
    public class Member {
        ...
    
        @JsonIgnore
        @OneToMany(mappedBy = "member")
        private List<Order> orders = new ArrayList<>();
    }
    • domain.OrderItem

      public class OrderItem {
      
        ...
      
          @JsonIgnore
          @ManyToOne(fetch = FetchType.LAZY) // 주문 아이템 입장에서 여러 주문 아이템들은 한 주문에 담기게 된다.
          @JoinColumn(name = "order_id")
          private Order order;
      
          ...
      }
    • domain.Delivery

      public class Delivery {
      
          ...
      
          @JsonIgnore
          @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
          private Order order;
      
      		...
      }

두 번째 에러

  1. Order class에서 member는 지연로딩이기에 member 객체에 접근 전까지는 데이터를 가지고 오지 않는다.
  2. 이때 하이버네이트는 new proxymember()를 통해 프록시멤버 객체를 생성해서 넣어놓게 된다.
    → bytebuddy
  3. 여기서 json이 루프를 돌려 order를 쭉 뽑아보려고 하는데 member 안에는 진짜 member 객체가 아닌 프록시가 들어가 있어서 에러가 발생하게 된다.
  • 해결법
    - 하이버네이트 모듈을 설치해야 한다.
    - jpabook.jpashop.JpashopApplication

    ```java
    package jpabook.jpashop;
    
    import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    
    @SpringBootApplication
    public class JpashopApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(JpashopApplication.class, args);
    	}
    
    	@Bean
    	Hibernate5Module hibernate5Module() {
    		Hibernate5Module hibernate5Module = new Hibernate5Module();
    		return hibernate5Module;
    	}
    
    }
    ```

  • 결과

  • member = null 인 이유는 지연로딩이기 때문이다.

  • member 데이터와 delivery 데이터를 띄우고 싶다면?

  1. 방법 1

    1. jpabook.jpashop.JpaApplication

      package jpabook.jpashop;
      
      import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.context.annotation.Bean;
      
      @SpringBootApplication
      public class JpashopApplication {
      
      	public static void main(String[] args) {
      		SpringApplication.run(JpashopApplication.class, args);
      	}
      
      	@Bean
      	Hibernate5Module hibernate5Module() {
      		Hibernate5Module hibernate5Module = new Hibernate5Module();
      
      		//강제 지연 로딩 설정 
      		// entity 노출 하면 안 좋고 성능도 좋지 못 함
      		// hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
      		return hibernate5Module;
      	}
      
      }
    • entity를 직접 노출하는 것
    • 성능도 좋지 못하니 쓰지 말 것~.ᐟ.ᐟ

  2. 방법 2

    1. api.OrderSimpleApiController
    ...
    
    public class OrderSimpleApiController {
    
        private final OrderRepository orderRepository;
    
        @GetMapping("/api/v1/simple-orders")
        public List<Order> orderv1(){
            List<Order> all = orderRepository.findAll(new OrderSearch());
    
            for (Order order : all) {
                // order.getmember() 까지는 프록시 멤버(db에 쿼리가 날리진 않은 상태
                // order.getMember().getName() 까지 하면 LAZY 강제 초기화가 된다.
                order.getMember().getName(); // LAZY 강제 초기화
                order.getMember().getAddress(); //  LAZY 강제 초기화
            }
            return all;
        }
    }
    • 엔티티를 직접 노출하지 말 것~.ᐟ.ᐟ
  • 결과

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

  • 문제점

    • v1과 v2 모두 LAZY_LOADING으로 인한 데이터베이스 쿼리가 너무 많이 호출된다.
  • 엔티티를 DTO로 변환하는 일반적인 방법이다.

  • 쿼리가 총 1 + N번 실행된다. (v1과 쿼리 수 결과는 동일)

    • order → 조회 1번 → order 조회 결과 수 : N
    • order를 통해 member 조회 → 지연 조회 N번
    • order를 통해 delivery 조회 → 지연 조회 N번
  • LAZY 초기화

    • : 영속성 컨텍스트가 이 멤버 id를 가지고 영속성 컨텍스트를 찾아온다.
      만약 없다면 디비 쿼리를 날린다.

api.OrderSimpleApiController

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
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;

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    ...

    **@GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2(){
        List<Order> orders = orderRepository.findAll(new OrderSearch()); // orders를 가지고 와서
        List<SimpleOrderDto> result = orders.stream() // stream으로 하나하나 For문 거쳐가며
                .map(o -> new SimpleOrderDto(o)) // Order 엔티티를 DTO로 바꿔준 다음
                .collect(Collectors.toList()); // collect를 이용해 list로 다시 변환해 준다.

        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(); // LAZY 초기화 
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
        }
    }**
}

3️⃣ 간단한 주문 조회 V3 : fetch join 최적화

  • v2에서 쿼리를 하나하나 날렸다면 V3에서는 fetch join을 이용해 order를 불러오려 할 때 관련된 member나 delivery도 같이 한꺼번에 조회를 해 버린다.
  • 재사용이 가능하다
  • 엔티티로 조회한 것이다.

api.OrderSimpleApiController

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
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;

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    ...

    @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(); // LAZY 초기화 : 영속성 컨텍스트가 이 멤버 id를 가지고 영속성 컨텍스트를 찾아온다. 만약 없다면 디비 쿼리를 날린다.
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
        }

    }

    @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;
    }

}

repository.OrderRepository

package jpabook.jpashop.repository;

...

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    ...

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

4️⃣ 간단한 주문 조회 V4 : JPA에서 DTO로 바로 조회

  • 재사용이 불가능하다.
  • DTO로 조회한 것이다. → 변경 불가능
  • 일반적인 SQL사용 할 때 처럼 원하는 값을 선택해서 조회한다.
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환한다.
  • 애플리케이션 네트워크 용량이 최적화되나 생각보다 미비하다.
  • Repository 재사용성이 떨어진다. API 스펙에 맞춘 코드가 Repository에 들어가는 단점 존재
  • repository는 순수하게 엔티티를 조회하는 용도로 사용할 것~.ᐟ.ᐟ

api.OrderSimpleApicontroller

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto;
import jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryRepository;
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;

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;
    private final OrderSimpleQueryRepository orderSimpleQueryRepository;

    ...

    @GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4(){
        return orderSimpleQueryRepository.findOrderDtos();
    }

}

repository.OrderRepository

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

   ...

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

}

repository.order.simplequery.OrderSimpleQueryRepository

package jpabook.jpashop.repository.order.simplequery;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
    private final EntityManager em;

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

repository.order.simplequery.OrderSimpleDto

package jpabook.jpashop.repository.order.simplequery;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;

import java.time.LocalDateTime;

@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) {
        this.orderId = orderId;
        this.name = name; // LAZY 초기화 : 영속성 컨텍스트가 이 멤버 id를 가지고 영속성 컨텍스트를 찾아온다. 만약 없다면 디비 쿼리를 날린다.
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

정리

  • 엔티티를 조회하여 DTO로 변환하기 → V3
    • 리포지토리 재사용성 좋고 개발도 단순해진다.
  • DTO로 바로 조회하기 → V4

권장하는 방법

  1. 엔티티를 DTO로 변환하는 방법을 선택한다. → V2
  2. 필요하면 fetch join으로 성능을 최적화한다. → V3
  3. 그래도 안 되면 DTO로 직접 조회하는 방법을 사용한다. → V4
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서
    SQL을 직접 사용한다.

0개의 댓글