[JPA 활용] 지연 로딩과 조회 성능 최적화
[JPA 활용] 지연 로딩과 조회 성능 최적화
JPA와 API
- 결론부터 말하면 API에서는 엔티티 그대로 반환하면 안 된다.
- 엔티티는 스펙이 변경될 가능성이 크기 때문이다.
- 또한 애플리케이션 내부 로직이 노출될 위험성도 존재하기 떄문이라는 이유도 있다.
v1 - 엔티티를 직접 노출
- 엔티티를 그대로 반환하기 때문에 API에서 사용하기에는 좋지 않은 방법
- 지연 로딩에 의해 프록시로 존재하는 부분을 강제로 초기화해줘야 한다.
//주문 단순 조회 v1
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAll(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화 (Member 엔티티)
order.getDelivery().getAddress(); //Lazy 강제 초기화 (Delivery 엔티티)
}
return all;
}public List<Order> findAll() {
return
em.createQuery("select o from Order o", Order.class)
.getResultList();
}Hibernate5Module
- JPA 사용 시 일반적으로 fetch 전략을 LAZY로 잡기 때문에 실제 엔티티 객체 대신에 프록시 객체를 갖고 있다.
- jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모르기 때문에 예외가 발생한다.
- 이를 해결하기 위해 Hibernate5Module 또는 Hibernate5JakartaModule를 스프링 빈으로 등록해준다.
스프링 부트 2.X를 사용 중인 경우
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}스프링 부트 3.X를 사용 중인 경우
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'@Bean
Hibernate5JakartaModule hibernate5Module() {
return new Hibernate5JakartaModule();
}@JsonIgnore
- 엔티티를 직접 반환할 때 해당 엔티티에 양방향 연관관계가 존재한다면 양측 엔티티가 서로 호출하면서 무한 루프가 발생한다.
@JsonIgnore를 추가해서 순환 참조를 막는다.@JsonIgnore는 주로 @ManyToOne 어노테이션이 있는 필드에 추가하면 된다.
v2 - 엔티티를 DTO로 변환
- 엔티티를 DTO로 변환 후 반환하는 방법
- 조회한 엔티티를 DTO의 생성자를 통해 DTO로 변환한다.
- 장점
- 해당 API를 위한 어느정도 고정된 스펙의 DTO를 반환하기 때문에 API의 스펙이 변경될 일이 적다.
- 단점
- 지연 로딩이기 때문에 N + 1의 문제가 존재한다.
- 발생하는 문제의 유형을 부르는 이름이 N + 1인거지 실제로는 더 많은 쿼리가 발생할 수도 있다.
- 만약에 조회한 엔티티 내부에 프록시로 존재하는 필드가 M개 있다면 실행되는 쿼리의 개수는 1 + N * M이 될 수도 있다.
- 팁
map(o -> new SimpleOrderDto(o))는 람다 레퍼런스 방식으로map(SimpleOrderDto::new)처럼 표현할 수 있다.
//주문 단순 조회 v2
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAll();
List<SimpleOrderDto> result =
orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(toList());
return result;
}public List<Order> findAll() {
return
em.createQuery("select o from Order o", Order.class)
.getResultList();
}@Data
public 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 강제 초기화 (Member 엔티티)
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); //Lazy 강제 초기화 (Delivery 엔티티)
}
}v3 - 페치 조인 최적화
- 페치 조인(fetch join)으로 데이터를 한꺼번에 가져온다.
- 엔티티로 조회했기 때문에 DB와의 작업이 수월하다. (ex : 변경 감지)
- 유연도가 높은 방식이다. (v4에 비해서 비교적 높은 편)
//주문 단순 조회 v3
@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;
}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();
}v4 - JPA에서 DTO로 바로 조회
- DB에서 조회한 데이터를 엔티티가 아닌 DTO로 바로 받는 방식
- DTO로 조회했기 때문에 DB와의 작업이 수월하지 못 하다. (ex : 변경 감지)
- 유연도가 낮은 방식이다. (v3에 비해서 비교적 낮은 편)
- 화면에 최적화되 있는 방식
- 대신에 특정 DTO에 의존도가 높다.
- JPQL 작성 방법이 좀 번거롭다. (패키지명 직접 명시)
//주문 단순 조회 v4
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}//패키지명을 직접 명시해줘야 한다.
//사용하는 IDE에서 관련 기능을 사용할 수 있다면 그나마 사용성이 증가한다.
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();
}출처
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.