I 개발 고급 - 지연 로딩과 조회 성능 최적화
📌 간단한 주문 조회 V1: 엔티티를 직접 노출
📍 OrderSimpleApiController
/**
* xToOne(ManyToOne, OneToOne) 관계 최적화
* Order
* Order -> Member
* Order -> Delivery
*/
@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
=>address
는 지연 로딩임 (따라서 실제 엔티티 대신에 프록시 존재)- jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름 => 예외 발생
Hibernate5Module
을 스프링 빈으로 등록하면 해결
📍 Hibernate5Module 등록
JpashopApplication 에 다음 코드 추가
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
기본적으로 초기화 된 프록시 객체만 노출, 초기화 되지 않은 프록시 객체는 노출 안함
📍 build.gradle
에 라이브러리 추가
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
⚠ 주의
- 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한 곳을
@JsonIgnore
처리해야 함 - 정말 간단한 애플리케이션이 아니면, 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않음!!
=>Hibernate5Module
을 사용하기보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법!! - 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안됨!!
=> 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있음
=> 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워짐 - 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용하자!!
📌 간단한 주문 조회 V2: 엔티티를 DTO로 변환
📍 OrderSimpleApiController
/**
* V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용 X)
* - 단점: 지연로딩으로 쿼리 N번 호출
*/
@GetMapping("/api/v2/simple-orders")
public List <SimpleOrderDto> ordersV2() {
//ORDER 2개
//N + 1 -> 1 + 회원 N + 배송 N
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();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
- 엔티티를 DTO로 변환하는 일반적인 방법임
- 쿼리가 총 1 + N + N번 실행됨 (V1과 쿼리수 결과는 같음)
order
조회 1번(order 조회 결과 수가 N이 됨)order -> member
지연 로딩 조회 N번order -> delivery
지연 로딩 조회 N번
ex) order의 결과가 4개 => 최악의 경우, 1 + 4 + 4번 실행됨
지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략함
📌 간단한 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
📍 OrderSimpleApiController
/**
* V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용 O)
* - fetch join으로 쿼리 1번 호출
*/
@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;
}
📍 OrderRepository
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();
}
- 엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회
페치 조인으로order -> member
,order -> delivery
는 이미 조회된 상태이므로 지연 로딩 X
📌 간단한 주문 조회 V4: JPA에서 DTO로 바로 조회
📍 OrderSimpleApiController
private final OrderSimpleQueryRepository orderSimpleQueryRepository; //의존관계 주입
/**
* V4. JPA에서 DTO로 바로 조회
* - 쿼리 1번 호출
* - select 절에서 원하는 데이터만 선택해서 조회
*/
@GetMapping("/api/v4/simple-orders")
public List <OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
📍 OrderSimpleQueryRepository 조회 전용 리포지토리
@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();
}
}
📍 OrderSimpleQueryDto 리포지토리에서 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;
}
}
- 일반적인 SQL을 사용할 때처럼 원하는 값을 선택해서 조회
new
명령어를 사용해 JPQL의 결과를 DTO로 즉시 변환- SELECT 절에서 원하는 데이터를 직접 선택하므로 DB => 애플리케이션 네트웍 용량 최적화(생각보다는 미비)
- 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점
📝 정리
- 엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두 가지 방법은 각각 장단점이 있음
- 둘 중 상황에 따라서 더 나은 방법을 선택하면 됨
(엔티티로 조회하면 리포지토리 재사용성이 좋고, 개발도 단순해짐)
쿼리 방식 선택 권장 순서
1. 우선 엔티티를 DTO로 변환하는 방법을 선택함
2. 필요하면 페치 조인으로 성능을 최적화함 (=> 대부분의 성능 이슈가 해결됨!!)
3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용함
4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용함
'실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 듣고 학습한 내용을 정리하였습니다.
Author And Source
이 문제에 관하여(I 개발 고급 - 지연 로딩과 조회 성능 최적화), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sxbxn/API-개발-고급-지연-로딩과-조회-성능-최적화저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)