제육's 휘발성 코딩
article thumbnail
반응형

1. 조회용 샘플 데이터 추가

image

  • 다음과과 같이 샘플 데이터를 만들어두자.
    • userA 는 JPA1 BOOK1, JPA2 BOOK2 주문
    • userB 는 SPRING1 BOOK, SPRING2 BOOK 주문
<code />
package jpabook.jpashop; import jpabook.jpashop.domain.*; import jpabook.jpashop.domain.item.Book; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; @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; } } }
  • 조회용 샘플 데이터 소스코드는 다음과 같다.

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

  • 주문 + 배송정보 + 회원을 조회하는 API를 만들어보며, 지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자.

2.1. 주문 조회 V1

<code />
import jpabook.jpashop.domain.Order; import jpabook.jpashop.domain.OrderSearch; import jpabook.jpashop.repository.OrderRepository; 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> ordersV1() { List<Order> all = orderRepository.findAllByString(new OrderSearch()); for (Order order : all) { order.getMember().getOrders(); // Lazy 강제 초기화 order.getDelivery().getAddress(); // Lazy 강제 초기화 } return all; } }
  • V1 상황 : DTO변환을 사용하지 않은 경우
    • Order 는 양방향연관관계로 지연 로딩 설정이 되어 있다. 따라서 실제 엔티티가 아닌 프록시로 존재한다. 현재 상황에서 조회를 하게되면, 무한루프 (서로의 컬렉션이 계속 조회)가 발생한다.
    • 양방향 연관관계에서 한 쪽에 @JsonIgnore 설정을 해줘야한다.
    • jackson 라이브러리는 프록시 객체를 json으로 생성하는 방법을 모르기 때문에 Hibernate5Module 을 스프링 빈으로 등록하여 초기화 된 프록시 객체만 노출 시켜야한다.
<code />
@JsonIgnore @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) private List<OrderItem> orderItems = new ArrayList<>();
  • 다음과 같이 양방향 연관관계에서 한 쪽을 @JsonIgnore 설정을 해줘야 한다.

2.1.1. Hibernate5Module (참고)

Hibernate5Module의 방법은 DTO가 아닌 실제 엔티티를 반환하기 위해 사용하는 라이브러리이다. 실무에선 DTO를 사용하여 반환하기 때문에 해당 내용은 참고만 하자.

<code />
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
  • build.gradle에 라이브러리 추가
<code />
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(); // hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true); return hibernate5Module; } }
  • Hibernate5Module를 빈으로 등록한다. 주석 처리를 한 부분에서 강제로 지연 로딩 설정을 할 수 있다.

양방향 연관관계인 경우 무한 루프 방지를 위해 한 곳을 @JsonIgnore 처리 해야 한다.

지연 로딩을 피하기 위해 즉시 로딩으로 설정하면 안된다. (오히려 성능 상의 문제 발생 및 성능 튜닝이 매우 어려워진다.)

지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우엔 페치 조인을 사용하자.

2.2. 주문 조회 V2

<code />
@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(); // LAZY 초기화 - 영속성컨텍스트에 없으므로 Member를 조회 후 저장 orderDate = order.getOrderDate(); orderStatus = order.getStatus(); address = order.getDelivery().getAddress(); // LAZY 초기화 } }
  • 조회 결과를 위한 DTO를 생성했다. 하지만 로그에 찍힌 쿼리 내용을 보면 이상한 점이 있다.
  • 로그 내용 : order 조회 - member 조회 - delivery 조회 - member 조회 - delivery 조회
    • LAZY 초기화 되는 부분에서 member 와 delivery 조회는 당연하다. 하지만 존재하는 데이터 갯수만큼 반복적으로 LAZY 초기화하는 것을 볼 수 있다. (N+1 문제 발생

2.3. 주문 조회 V3

<code />
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(); }
  • 페치 조인을 적용하기 위해 OrderRepository 에 해당 메서드를 추가하자.
<code />
@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; }
  • 페치 조인을 적용해서 로그를 확인해보면 하나의 쿼리문으로 성능이 최적화 된 것을 확인 할 수 있다.

2.4.  

2.5. 주문 조회 V4

<code />
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; this.orderDate = orderDate; this.orderStatus = orderStatus; this.address = address; } }
  • 필요한 데이터를 사용하기 위해 별도의 DTO를 만든다.
<code />
@GetMapping("/api/v4/simple-orders") public List<OrderSimpleQueryDto> ordersV4() { return orderSimpleQueryRepository.findOrderDtos(); }
  • DTO 형식의 리스트를 반환하도록 만든다.
<code />
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(); } }
  • 엔티티가 아닌 DTO를 반환하는 레포지토리는 별도로 생성하자.
    • 레포지토리는 엔티티를 조회하는 용도로 사용되며, 성능 최적화를 위해 DTO를 사용한다면 별도의 레포지토리를 생성하는 것이 올바르다.
    • DTO를 select 하기 위해 new 연산자를 사용해서 불러온다.

2.5.1.  

2.5.2. V3 vs V4

image

성능 상으로는 V4가 좋지만, 리포지토리의 재사용성이 떨어지고 API 스펙에 맞춘 코드가 리포지토리에 들어가게 된다.

<쿼리 방식 선택 권장 순서>

DTO 변환 - V2

필요하면 페치 조인으로 성능 최적화 (대부분의 이슈 해결) - V3

해결이 안된 경우 DTO로 직접 조회하는 방법 사용 - V4

최후의 방법으로 네이티브 SQL 이나 JDBC Template을 사용해서 직접 SQL 사용


본 포스팅은 인프런 김영한님 강의(실전! 스프링 부트와 JPA 활용2)를 토대로 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^