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

지니🧸·2023년 2월 24일
0

Spring Boot & JPA

목록 보기
20/35

본 문서는 인프런의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 (김영한) 강의를 공부하며 작성한 개인 노트입니다.

간단한 주문 조회

지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자

🍼 V1) 엔티티 직접 노출

XToOne의 최적화 (ex) ManyToOne, OneToOne

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

문제 1: 무한 루프

  • order 호출 > member 호출 > member에서 order 호출 > order에서 멤버 호출
  • 해결 방법: 양방향 연관관계가 있으면 한 쪽은 @JsonIgnore 에노테이션 추가

문제 2:

  • 지연 로딩은 디비에서 정보를 가져오지 않고 proxy library로 proxy 객체를 상속받아 넣어둠 > json 에러
    • (예) order를 호출하면 member를 디비에서 가져오지 않음
    • ByteBuddy

문제 3:

  • 지연
    JpaShopApplication
@Bean
Hibernate5Module hibernate5Module() {
	Hibernate5Module hibernate5Module = new Hibernate5Module();
	//강제 지연 로딩 설정
	hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
	return hibernate5Module;
}
  • 이것보다는 DTO로 변환해서 반환하는 것이 더 좋음

OR

@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
	List<Order> all = orderRepository.findAllByString(new OrderSearch());
	for (Order order : all) {
		order.getMember().getName();
		order.getDelivery().getAddress();
	}
	return all;
}
  • order.getMember().getName()order.getDelivery().getAddress()을 통해 지연로딩임에도 디비에서 데이터를 가져온다
  • 단점: 회원명과 주소는 필요 없는 경우에도 api가 다 노출됨

결론:

  • 엔티티를 직접 노출하는 것은 안 좋음
  • 불필요한 쿼리가 많이 나감
  • 지연로딩을 즉시로딩으로 바꾸면 안됨 > 성능최적화가 거의 불가능

🧋 V2) 엔티티를 DTO로 변환

DTO 클래스

@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();
    }
}
  • 생성자에서 order.getMember().getName(); 등을 통해 지연로딩 초기화

DTO를 사용한 컨트롤러

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

	return result; 
}

문제점

  • 메소드에서 테이블 세개 사용
    • V1도 있는 문제
  • N+1 문제: 첫번째 쿼리의 결과로 N번만큼 쿼리가 추가 실행된다
    • (예) 예시에서는 회원(N)+ 배송(N)으로 인해 문제
      • order 조회 1번 > order -> member 지연로딩 조회 N번 > order -> delivery 지연로딩 조회 N번
      • 총 2N + 1

🫗 V3) 엔티티를 DTO로 변환 > 페치 조인 최적화

public List<Order> findAllWithMembersDelivery() {
	return em.createQuery(
                "select o from Order o" +
                " join fetch o.member m" +
                " join fetch o.delivery d", Order.class
    ).getResultList();
}
  • fetch join 사용
    • 지연 로딩 설정을 무시하고 proxy가 아닌 객체를 직접 가져옴
  • 위 두가지보다 성능이 확연히 빠름
  • 단점: select문에 여러 테이블을 건듬

🍵 V4) JPA에서 DTO로 바로 조회

컨트롤러

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

DTO 클래스 생성

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

OrderRepository.class에 만듬

public List<OrderSimpleQueryDto> findOrderDto() {
        return em.createQuery("selectnew jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address) " +
                " join o.member m" +
                " join o.delivery d", OrderSimpleQueryDto.class)
                .getResultList();
    }

V4

  • 일반적인 SQL 사용처럼 값을 선택해서 조회
    • SELECT 절에서 원하는 데이터만 조회 > 애플리케이션 네트워크 용량 최적화 (미비)
  • new 명령어로 JPQL 결과를 즉시 DTO 변환
  • 리포지토리 재사용성 떨어짐
    • API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

V3 vs. V4

  • V4의 select문은 필요한 테이블만 건듬
  • V4가 무조건 V3보다 좋지는 않음
    • V3는 외부를 건들지 않고 내부 내용만으로 튜닝 > 재사용 가능
      • 원하는 DTO에 따라 사용 가능
    • V4는 화면상에서는 최적화지만 재사용성 없음
      • 성능상에서는 조금 더 나을 수 있음
      • 코드가 조금 더 지저분함
  • 상황에 따라 선호하는 방식 선택

V4 보완 방법

  • Repository에 하위 폴더 만듬 > query용 repository 만듬
    • (예) OrderQueryRepository

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법 선택
  2. 필요하면 페치 조인으로 성능 최적화 > 대부분 성능 이슈 해결
  3. 그래도 안되면 DTO로 직접 조회
  4. 최후: JPA가 제공하는 네이티브 SQL/스프링 JDBC Template 사용
profile
우당탕탕

0개의 댓글