(2024모각소)지연로딩, 조회 성능 최적화

LEEHYUNJE·2024년 8월 14일
0

아주대학교_모각소!

목록 보기
19/23

오늘 모각소 시간에 들은 대 김영한님의 강의 중 정말 중요하다는 내용을 한번 정리해보려고한다.

엄청난 경고이니 뼈와 살로 만들어보자.

코드의 발전 단계는 점점 심화되어가니 처음 코드들은 목적이 무엇인지 파악하는 용도로만 보면 좋을 것 같다.
또한 이미 개발중인 코드에 대해 리뷰이므로 디테일한 부분을 알고 싶다면 김영한님의 강의를 듣는 것을 추천한다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94/dashboard

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

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
 private final OrderRepository orderRepository;
 /**
 * V1. 엔티티 직접 노출
 * - Hibernate5Module 모듈 등록, LAZY=null 처리
 * - 양방향 관계 문제 발생 -> @JsonIgnore
 */
 @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;
 }
}
  • 엔티티를 반환타입으로 두어 직접 노출하게 되었다.
  • order -> member, order -> delivery는 지연로딩(연관관계) 따라서 프록시가 존재한다.
  • jackson 라이브러리는 기본적으로 이프록시 객체를 json으로 어떻게 생성해야 하는지 모른다
    -> 오류 발생
  • 이에 Hibernate5Module을 Bean으로 등록하여 초기화 된 프록시 객체만 노출, 초기화 되지 않은 프록시 객체는 노출하지 않게 만든다.

양방향 연관관계 무한루프

@Bean
Hibernate5Module hibernate5Module() {
 Hibernate5Module hibernate5Module = new Hibernate5Module();
 //강제 지연 로딩 설정
 hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING,
true);
 return hibernate5Module;
}

다음과 같이 설정하면 강제로 지연로딩이 가능하다.
하지만, 무한루프를 도는 문제가 발생한다.

  • order -> member , member -> orders 서로가 서로를 참조하는 관계이므로 계속해서 서로에 대한 데이터를 가져오면서 루프에 빠지게 된다.
  • @JsonIgnore : 지연로딩을 토앻서 필요없는 데이터들에 대해서는 위 무한루프를 방지하기 위해서 엔티티의 필드에 @JsonIgnore를 사용해야한다.

질문: 그럼 그냥 즉시로딩(EARGER)로 설정할까요?

절대안된다!
즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워진다.

그러므로 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join) 을 사용해라.

간단한 주문조회 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(toList());
 return result;
}

Controller의 모습.

@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();
 }
}
  • 엔티티를 DTO로 변환하는 일반적인 방법이다.
  • 쿼리가 총 1 + N + N번 실행된다.
    • order 조회 1번(order 조회 결과 수가 N이된다.) 이때 결과는 튜플이라고 이해하면 편하다.
    • order -> member 지연 로딩 조회 N번
    • order -> delivery 지연 로딩 조회 N번
    • order의 결과(튜플)가 4개면 최악의 경우 1 + 4 + 4번 실행된다 -> 각 order와 연관되어있는 member와 delivery X 4
      • 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

간단한 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화

/**
 * V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
 * - fetch join으로 쿼리 1번 호출
 * 참고: fetch join에 대한 자세한 내용은 JPA 기본편 참고(정말 중요함)
 */
@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;
}

Controller의 모습

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의 모습

  • 엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회
  • 페치 조인으로 order -> member, order -> delivery 는 이미 조회된 상태이므로 지연로딩 되지 않는다.

  • 쿼리 한번에 조회된 모습.

간단한?주문 조회 V4: JPA에서 DTO로 바로 조회

private final OrderSimpleQueryRepository orderSimpleQueryRepository; //의존관계 주입
/**
 * V4. JPA에서 DTO로 바로 조회
 * - 쿼리 1번 호출
 * - select 절에서 원하는 데이터만 선택해서 조회
 */
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
 return orderSimpleQueryRepository.findOrderDtos();
}

OrderSimpleApiController - 추가

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

OrderSimpleQueryRepository 조회 전용 리포지토리

@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;
 }
}
  • 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
  • SELECT 절에서 원하는 데이터를 직접 선택하므로 DB -> 애플리케이션 네트웍 용량 최적화
  • 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점 존재

정리

엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다. 둘중 상황에 따라서 더나은 방법을 선택하면 된다.

쿼리 방식 선택 권장 순서
1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다. -> v2
2. 필요하면 페치 조인으로 성능을 최적화 한다. -> 대부분의 성능 이슈가 해결 -> v3
3. 그래도 안되면 DTO 로 직접 조회하는 방법을 사용한다.
4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.


모각소 후기

오늘 또 팀원들과 모여 모여서 각자 소프트웨어했다. 벌써 8월 13일..... 이제 3학년 2학기를 진행하는 나는 이번 겨울방학에 무조건 링크인턴에 성공해야만 한다. 그러기 위해서는 뭘 준비해야하는지 미친듯이 한번 알아보고 그것을 파볼 예정이다. 꼭 인턴을 합격하겠다는 각오를 가지고 열심히 코딩해보도록 하자.

profile
현재진행중

0개의 댓글