-
간단한 주문조회 API - 지연 로딩과 조회 성능 최적화JPA/JPA 활용 2023. 7. 11. 18:15
지연 로딩 때문에 발생하는 성능 문제를 단게적으로 해결하는 것이다.
버전을 1부터 4까지 늘리면서 잘 실수하는 부분을 조금씩 개선해나가는 것이다.
실험용 초기 데이터
@Component @Transactional @RequiredArgsConstructor static class InitService { private final EntityManager em; public void dbInit1() { Member member = new Member(); member.setName("userA"); member.setAddress(new Address("서울", "1", "1111")); em.persist(member); // 영속상태로 만듦 Book book1 = new Book(); book1.setName("JPA1 BOOK"); book1.setPrice(10000); book1.setStockQuantity(100); em.persist(book1); Book book2 = new Book(); book2.setName("JPA2 BOOK"); book2.setPrice(20000); book2.setStockQuantity(100); em.persist(book2); OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1); OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2); Delivery delivery = new Delivery(); delivery.setAddress(member.getAddress()); Order order = Order.createOrder(member, delivery, orderItem1, orderItem2); em.persist(order); } public void dbInit2() { Member member = new Member(); member.setName("userB"); member.setAddress(new Address("인천", "2", "2222")); em.persist(member); // 영속상태로 만듦 Book book1 = new Book(); book1.setName("SPRING1 BOOK"); book1.setPrice(10000); book1.setStockQuantity(100); em.persist(book1); Book book2 = new Book(); book2.setName("SPRING2 BOOK"); book2.setPrice(20000); book2.setStockQuantity(100); em.persist(book2); OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1); OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2); Delivery delivery = new Delivery(); delivery.setAddress(member.getAddress()); Order order = Order.createOrder(member, delivery, orderItem1, orderItem2); em.persist(order); }
Version 1
@RestController @RequiredArgsConstructor public class OrderSimpleApiController { private final OrderRepository orderRepository; @GetMapping("api/v1/simple-orders") public List<Order> ordersV1() { // 엔티티를 외부로 노출하면 안됨 List<Order> all = orderRepository.findAllByString(new OrderSearch()); return all; } }
이전 포스팅에서도 계속해서 API를 설계할 때에는 엔티티를 그대로 노출시키면 안된다는 얘기를 했었다. 버전 1은 엔티티를 직접 노출시키는 과정인데, 이러한 설계 방법은 효율적인 설계 방법이 아니기 때문에 무조건 별도의 Dto를 만들어서 설계해야한다는 점을 명심해야한다.
Version 2
@GetMapping("api/v2/simple-orders") // 지연로딩으로 인한 쿼리가 너무 많이 호출됨 public List<SimpleOrderDto> ordersV2() { List<Order> orders = orderRepository.findAllByString(new OrderSearch()); // 이걸 그대로 반환하지말고 Dto로 바꿔서 반환해야함 -> 아래 과정에서 Dto로 바꿈 List<SimpleOrderDto> result = orders.stream() .map(o -> new SimpleOrderDto(o)) // 이거는 map(a -> b) ---- a를 b로 바꾸는거 .collect(Collectors.toList()); // 여기서 루프돌 때 각각 주문마다 member, delivery 쿼리를 내보냄 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) { // Dto의 파라미터에서 엔티티를 받아오는건 괜찮다. orderId = order.getId(); name = order.getMember().getName(); orderDate = order.getOrderDate(); orderStatus = order.getStatus(); address = order.getDelivery().getAddress(); } }
SimpleOrderDto라는 별도의 Dto를 만들어서 주문을 조회하는 2번째 버전이다. 하지만 이러한 설계 방법에도 문제점이 있는데, 그 문제점은 지연로딩으로 인해서 쿼리가 너무 많이 호출된다는 것이다. N+1이라는 공식에 따라 order에서 member를 호출할 때에 지연 로딩이 N번, order에서 delivery를 호출할 때에 지연로딩이 N번 호출된다.
N이 2라고 했을 때 1 + member의 갯수(2), delivery의 갯수(2)로 인한 5개의 쿼리가 호출된다.
만약에 한 회원이 주문 2개를 하면 1 + 1 + 2로 4개의 쿼리가 호출된다. member는 영속성 컨텍스트로 되어있기 때문에 쿼리를 날려서 select를 해오는게 아니기 때문이다.
쿼리가 너무 많이 나가게 된다면 조회를 할 때 시간이 너무 오래걸려서 성능이 제대로 안나온다는 단점이 있다.
'JPA > JPA 활용' 카테고리의 다른 글
컬렉션 조회 최적화 1 - 페치 조인을 안 했을 시 (0) 2023.08.10 간단한 주문조회 API - 지연 로딩과 조회 성능 최적화(2) (0) 2023.07.11 회원 조회 API (0) 2023.07.11 회원 수정 API (0) 2023.07.11 회원 등록 API (0) 2023.07.11