JPA 활용 - 지연 로딩과 조회 성능 최적화

2023. 7. 20. 20:19·Back-End/JPA

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의

 

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의

스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., 스프링 부트, 실무에서 잘 쓰고 싶다면? 복잡한 문제까지 해결하는 힘을 길러보세요

www.inflearn.com

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

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

문제점 - 무한루프 발생

위와 같이 엔티티를 직접 노출할 시, Order ↔ Member사이, Order ↔ Delivery 간의 엔티티 조회시 무한루프에 빠지면서 에러를 발생시킨다.

@JsonIgnore 사용

해당 무한루프 문제를 해결하기 위해서 @JsonIgnore를 양방향 연관관계중 하나에 추가해 주면 무한루프 문제를 해결할 수 있다. 하지만 다음과 같은 에러가 발생한다.

ByteBuddyInterceptor에서 Type defnition error가 발생했다. 왜 그럴까?

→ 해당 코드의 로딩 전략은 지연 로딩(@ManyToOne(fetch = FetchType.LAZY))전략을 사용하는데 이는 Member나 Delivery등의 엔티티를 조회하는 시점에 가짜 프록시 객체를 생성해서 넣어놓게 되는데, Jackson 라이브러리에서는 프록시 객체를 처리하지 못하면서 해당 에러가 발생하게 된다.

Hibernate5Module 사용

위의 오류는 Hibernate5Module 을 스프링 Bean으로 등록하여 해결한다.

// build.gradle
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

//jpashopApplication.java
@Bean
    Hibernate5Module hibernate5Module() {
        return new Hibernate5Module();
    }

엔티티를 API에 직접 노출하는 것은 좋지 않은 방법이므로, Hibernate5Module을 이용하는 방법은 알아만 두고 실무에서는DTO Response를 만들어서 사용하도록 하자!

간단한 주문 조회 V2 - 엔티티를 DTO로 변환

@GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        return orders.stream().map(SimpleOrderDto::new)
                     .collect(Collectors.toList());
    }

    @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(); //LAZY 초기화
        }
    }

Controller 추가 및 Dto 클래스를 추가하였다.

N + 1 의 문제

해당 쿼리에서는 쿼리가 총 1 + N + N 번 실행된다.

  • order 조회 1번
  • order → member 지연 로딩 조회 N 번
  • order → delivery 지연 로딩 조회 N 번

현재 order의 결과가 2개이므로 1 + 2 + 2 번 실행된다.(최악의 경우)

간단한 주문 조회 V3 - DTO 페치 조인 최적화

OrderRepository.java

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

OrderSimpleApiController.java

@GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        return orders.stream()
                .map(SimpleOrderDto::new)
                .collect(Collectors.toList());
    }
  • 엔티티를 페치 조인(fetch join)을 이용해서 쿼리 1번에 조회한다.
  • 페치 조인으로 order → member → delivery 는 이미 조회 되었으므로 지연로딩 X

다음과 같은 SQL 문으로 한번에 조회한다.

    select
        order0_.order_id as order_id1_6_0_,
        member1_.member_id as member_i1_4_1_,
        delivery2_.delivery_id as delivery1_2_2_,
        order0_.delivery_id as delivery4_6_0_,
        order0_.member_id as member_i5_6_0_,
        order0_.order_date as order_da2_6_0_,
        order0_.status as status3_6_0_,
        member1_.city as city2_4_1_,
        member1_.street as street3_4_1_,
        member1_.zipcode as zipcode4_4_1_,
        member1_.name as name5_4_1_,
        delivery2_.city as city2_2_2_,
        delivery2_.street as street3_2_2_,
        delivery2_.zipcode as zipcode4_2_2_,
        delivery2_.status as status5_2_2_ 
    from
        orders order0_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id 
    inner join
        delivery delivery2_ 
            on order0_.delivery_id=delivery2_.delivery_id

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

OrderSimpleApiController.java

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

따로 쿼리용 리포지토리를 만들면 구분이 쉽다.

OrderSimpleQueryRepository.java

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

    public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery(
                         "select new jpabook.jpashop.repository.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();
    }
}

OrderSimpleQueryDto.java

@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 초기화
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address; //LAZY 초기화
    }

  • 일반 적인 SQL 사용할 때 처럼 원하는 값을 선택해서 조회
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
  • SELECT 절에서 원하는 데이터를 직접 선택하므로 DB → 애플리케이션 네트웍 용량 최적화(생각보다 효과가 크지는 않다.)
  • 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

정리

엔티티를 DTO로 변환하고너, DTO로 조회하는 방법은 각각 장단점이 있다. 상황에 따라 더 좋은 방법을 선택할 것. 엔티티로 조회하면 피로치토리 재사용성도 좋고, 개발도 단순해진다.

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
  2. 필요하면 페치 조인으로 성능을 최적화 한다. → 대부분의 성능 이유가 해결됨
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용해 본다.
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
반응형
저작자표시 (새창열림)
'Back-End/JPA' 카테고리의 다른 글
  • JPA 활용 - 컬렉션 조회 최적화
  • JPA - 값 타입(1)
  • JPA - 즉시 로딩과 지연 로딩
  • JPA - 프록시란?
LightSource
LightSource
어제보단 발전한 오늘의 나를 위한 블로그
    반응형
  • LightSource
    LightSourceCoder
    LightSource
  • 전체
    오늘
    어제
    • 분류 전체보기 (152)
      • Git (4)
      • Language (6)
        • Java (6)
      • Back-End (63)
        • Spring Boot (4)
        • MyBatis (1)
        • Oracle (1)
        • PL SQL (3)
        • JPA (26)
        • Spring Data JPA (5)
        • Spring MVC (8)
        • Spring (12)
        • Spring Security (2)
        • Redis (1)
      • Front-End (38)
        • 아이오닉 (2)
        • JSP (7)
        • JavaScript (4)
        • React (16)
        • TypeScript (3)
        • Angular (6)
      • AWS (1)
      • CI & CD (1)
      • 개발지식 (13)
        • 네트워크 (9)
        • CS 지식 (4)
      • 데이터모델링 (2)
      • Tool (1)
      • 프로젝트 (5)
      • 독후감 (2)
      • 잡생각 (0)
      • 면접 준비 (1)
      • 알고리즘 (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    리액트
    배요소열추가
    배열요소삭제
    react
    배열요소수정
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
LightSource
JPA 활용 - 지연 로딩과 조회 성능 최적화
상단으로

티스토리툴바