[JPA2] 간단한 주문조회/ API개발 고급-지연로딩과 조회 성능 최적화

레몬커드요거트·2022년 11월 2일
0

조회용 샘플 데이터 입력

jpabook > jpashop > service > InitDb.java

@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() {
            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.createOrder(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.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 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;
        }
    }
}

지연 로딩과 조회 성능 최적화

V1. 엔티티 직접노출

문제1. order에 걸리는 양방향 관계 설정문제
-> @JsonIgnore를 통해 끊어야함
문제2. [Type Definition Error] fetch가 Lazy로 설정 되어 있음
->

@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;
}
//build.gradle
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

//JpashopApplication
@Bean
Hibernate5Module hibernate5Module() {
	return new Hibernate5Module();
}
/*****안좋은 코드, Lazy *****/
  @Bean
  Hibernate5Module hibernate5Module() {
      Hibernate5Module hibernate5Module = new Hibernate5Module();
      //강제 지연 로딩 설정
      hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING,true);
      return hibernate5Module;
  }

[
    {
        "id": 4,
        "member": {
            "id": 1,
            "name": "userA",
            "address": {
                "city": "서울",
                "street": "1",
                "zipcode": "1111"
            }
        },
        "orderItems": [
            {
                "id": 6,
                "item": {
                    "id": 2,
                    "name": "JPA1 BOOK",
                    "price": 10000,
                    "stockQuantity": 99,
                    "categories": [],
                    "author": null,
                    "isbn": null
                },
                "orderPrice": 10000,
                "count": 1,
                "totalPrice": 10000
            },
            {
                "id": 7,
                "item": {
                    "id": 3,
                    "name": "JPA2 BOOK",
                    "price": 20000,
                    "stockQuantity": 98,
                    "categories": [],
                    "author": null,
                    "isbn": null
                },
                "orderPrice": 20000,
                "count": 2,
                "totalPrice": 40000
            }
        ],
        "delivery": {
            "id": 5,
            "address": {
                "city": "서울",
                "street": "1",
                "zipcode": "1111"
            },
            "status": null
        },
        "orderDate": "2022-11-02T18:05:35.042539",
        "status": "ORDER",
        "totalPrice": 50000
    },
    {
        "id": 11,
        "member": {
            "id": 8,
            "name": "userB",
            "address": {
                "city": "진주",
                "street": "2",
                "zipcode": "2222"
            }
        },
        "orderItems": [
            {
                "id": 13,
                "item": {
                    "id": 9,
                    "name": "SPRING1 BOOK",
                    "price": 20000,
                    "stockQuantity": 197,
                    "categories": [],
                    "author": null,
                    "isbn": null
                },
                "orderPrice": 20000,
                "count": 3,
                "totalPrice": 60000
            },
            {
                "id": 14,
                "item": {
                    "id": 10,
                    "name": "SPRING2 BOOK",
                    "price": 40000,
                    "stockQuantity": 296,
                    "categories": [],
                    "author": null,
                    "isbn": null
                },
                "orderPrice": 40000,
                "count": 4,
                "totalPrice": 160000
            }
        ],
        "delivery": {
            "id": 12,
            "address": {
                "city": "진주",
                "street": "2",
                "zipcode": "2222"
            },
            "status": null
        },
        "orderDate": "2022-11-02T18:05:35.114489",
        "status": "ORDER",
        "totalPrice": 220000
    }
]

정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않음. 따라서 Hibernate5Module 를 사용하기 보다는 DTO로 변환 해서 반환하는 것이 더 좋은 방법

V2: 엔티티를 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;

        public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate=order.getOrderDate();
            orderStatus = order.getStatus();
            address=order.getDelivery().getAddress();

        }
    }
[
    {
        "orderId": 4,
        "name": "userA",
        "orderDate": "2022-11-02T19:34:07.864681",
        "orderStatus": "ORDER",
        "address": {
            "city": "서울",
            "street": "1",
            "zipcode": "1111"
        }
    },
    {
        "orderId": 11,
        "name": "userB",
        "orderDate": "2022-11-02T19:34:08.045904",
        "orderStatus": "ORDER",
        "address": {
            "city": "진주",
            "street": "2",
            "zipcode": "2222"
        }
    }
]

📍단점
테이블 세개 JOIN 해야하므로, Lazy 때문에 쿼리가 너무 많이 나온다.


ORDER -> SQL 1개 -> 결과 주문서 2개

쿼리갯수: ORDER 2개 N+1 -> 1+ 회원 N + 배송 N
쿼리가 총 1 + N + N번 실행된다. (v1과 쿼리수 결과는 같다.)

order 조회 1번(order 조회 결과 수가 N이 된다.)
order -> member 지연 로딩 조회 N 번
order -> delivery 지연 로딩 조회 N 번

지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다

V3: 엔티티를 DTO로 변환 - 페치(fetch)조인 최적화

OrderSimpleApiController

@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(toList());
  return result;
}

OrderRepository

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

엔티티를 Fetch 조인을 이용하여 쿼리 1번에 조회됨.

(Fetch 조인으로 order -> member , order -> delivery 는 이미 조회 된 상태 이므로 지연로딩X)

V4: JPA에서 DTO로 바로 조회

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

쿼리 한번으로 select 절에서 원하는 데이터만 선택해서 조회 가능

엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있음. 둘중 상황에 따라서 더 나은 방법을 선택하면 됨. 엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해짐.

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

0개의 댓글