지연 로딩과 조회 성능 최적화
: @ToOne 관계에서의 성능 최적화
간단한 주문조회 API
Version #1 - 엔티티를 직접 조회
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all;
}
Java
복사
•
Data는 조회되나, 무한루프 발생으로 application 터지는 문제 발생
◦
발생 이유
▪
양방향 연관관계로 인한 문제 → 양방향 무한참조가 일어남
◦
해결 방법
▪
양방향 참조 → @JsonIgnore 를 통해 단방향 참조로 변경하면 해결 가능
•
500 Internal Error 발생
◦
발생 이유
public class Order {
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
...
}
Java
복사
▪
지연 로딩 → Order를 조회했을 때,Member의 Proxy 객체를 생성해서 가지고 있음(ByteBuddyInterceptor)
// 실제 Order객체에서 가지고 있는 Member 객체
// ByteBuddyInterceptor 객체
private Member member = new ByteBuddyInterceptor();
Java
복사
▪
Member 객체 사용시 Proxy 객체를 초기화하여 사용
▪
Jackson Library는 ByteBuddyInterceptor 객체를 다룰 수 없기 때문에 Error 발생!
◦
해결 방법
▪
HibernateModule을 이용 (Hibernate5)
•
HibernateModule이 Lazy Loading인 경우 무시하도록
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
Java
복사
•
강제 Lazy Loading도 가능
@Bean
Hibernate5Module hibernate5Module() {
Hibernate5Module hibernate5Module = new Hibernate5Module();
hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true); // 이 옵션은 사용하면 안된다!!!!!!!!!!!
return hibernate5Module;
}
Java
복사
성능상 문제가 생김, Entity 노출의 문제도 존재
간단한 application을 만드는 것이 아니면 entity를 api 응답으로 외부로 노출하는 것은 좋지 않다.
→ DTO로 변환해서 반환하는 것이 더 좋은 방법!
Version #2 - DTO로 변환해서 반환
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
// order 2개 조회
// N+1 -> 1+N 문제 발생
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
// Lazy Loading -> order 별로 constructor 생성
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();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();;
}
}
Java
복사
•
API 스펙과 동일한 dto를 생성하여 결과 값 반환
•
LAZY Loading으로 인한 N+1 문제 발생!!!!!!!!!!!
◦
Order → SQL 1번 → 결과 주문 수 (Order 수) 2개
◦
N+1 → 1 + 회원 N + 배송 N
▪
order 조회 1번(order 조회 결과 수가 N이 된다.)
▪
order -> member 지연 로딩 조회 N 번
▪
order -> delivery 지연 로딩 조회 N 번
▪
예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우)
◦
지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.
Version #3 - 성능 최적화 (Fetch Join)
// fetch join
public List<Order> findAllwWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).getResultList();
}
Java
복사
•
한방 쿼리로 해결!!
→ order, member, delivery를 한 번에 가져온다!
•
but Entity를 통째로 가져오기때문에 query가 복잡해지는 결과..
Version #4 - Repository에서 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; //LAZY 초기화
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address; //LAZY 초기화
}
}
Java
복사
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();
}
Java
복사
•
SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트웍 용량 최적화(생각보다
미비)
•
리포지토리 재사용성 떨어짐
•
API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점
◦
Repository는 Entity의 객체 그래프를 조회하는 역할
◦
Entity에 대한 순수성이 유지가 되지 않는 ?!
◦
Repository가 화면에 의존하고 있음!
▪
API스펙이 바뀌면 Repository를 뜯어야...
정리
•
엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다. 둘중 상황에
따라서 더 나은 방법을 선택하면 된다.
•
엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다.
쿼리 방식 선택 권장 순서
1.
우선 엔티티를 DTO로 변환하는 방법을 선택한다.
2.
필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
3.
그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
4.
최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접
사용한다.