본문 바로가기

Spring

[Spring JPA] JPA 에서 fetch join(페치 조인)이란

SQL 문에서 outer join , inner join 등 join 문에 대해서는 익히 알고 있다.

그런데 fetch join(페치 조인)은 어떠한 것일까

 


 

 

페치 조인이란?

 

먼저, fetch join 은 SQL에서 사용하는 join문이 아니라, JPQL에서 사용하는 성능 최적화를 위해 제공하는 조인의 한 종류이다.

연관된 엔티티나 컬렉션을 한 번에 조회할 수 있는 기능으로, 기본적인 기능은 join과 유사하기 때문에 쉽게 이해할 수 있을 것이다.  jpql에서  join fetch 명령어를 이용해 사용할 수 있다.

 

 

JPA에서는 데이터를 조회할 때 지연로딩, 즉시로딩 두가지 기법이 있는데, 지연로딩을 기본적으로 사용하고 간다. 

주의: 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다! 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매~우 어려워 진다.

 

지연로딩을 기본으로 깔아두고, 성능 최적화가 필요할 때에는 페치 조인을 사용하자.

 

그렇다면, 예제 코드를 보며 페치 조인에 대해 더 알아보자.

 

 

 

예제 코드

 

 

@GetMapping("/api/v2/simple-orders")
 public List<SimpleOrderDto> ordersV2() {
     List<Order> orders = orderRepository.findAllByString(new OrderSearch());
     List<SimpleOrderDto> result = orders.stream()
             .map(o -> new SimpleOrderDto(o))
             .collect(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();
} }

 

 

 

order 는 주문을 나타내는 객체이고, 한명의 member는 여러개의 Order객체와 연관관계를 가질 수 있다.

 

 

위는 엔티티를 Dto로 변환해서 사용하는 기본적인 방법이다. 

Dto 생성자에서 name = order.getMember().getName()
address = order.getDelivery().getAddress()를 이용하여, 지연 로딩에서 생길 수 있는 영속성 문제를 해결하였다.

이렇게 되면 쿼리문이 총 1 + N + N 번이 호출되게 된다.

 

  • order 조회 1번(order 조회 결과 수가 N이 된다.)
  • order -> member 지연 로딩 조회 N 번
  • order -> delivery` 지연 로딩 조회 N
  • 예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우) 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

이렇게 연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다. 이러한 문제를 N+1 문제라고 부른다.

 


 

이러한 문제를 해결하기 위해 우리는 fetch join(페치 조인)을 사용할 수 있다.

바뀐 코드들을 확인해보자.

 

 

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

 

 

 

findAllWithMemberDelivery는 직접 페치 조인을 적용한 쿼리문을 추가한 코드이다.

이렇게 되면 orders를 조회할 때, 쿼리문은 한번만 나가게 된다.

 

페치조인으로 order -> member, order -> delivery는 이미 조회된 상태이므로, 지연로딩은 되지 않을 것이다.

이처럼 페치조인을 이용한 쿼리문을 사용하면 효과적으로 사용되는 쿼리문의 개수를 줄일 수 있다.

 

 

일반 조인과의 차이점

그런데, 이렇게 되면 일반 조인을 사용하면 되는 것이 아닌가 하는 의문이 생길 수도 있다. 일반 조인과 차이점은 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다. 조인은 하지만 데이터가 조회되지 않는다는 의미입니다. 일반 조인을 사용하면 위와 마찬가지로 N+1 문제가 발생하게 된다.

 

만일 페치조인을 이용해서도 성능 최적화가 부족한 경우에는 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용하면 된다.

 

 

 

 

 

페치조인에는 크게 두가지 종류가 있다. 위에서 보여준 것은 엔티티 페치 조인이고, 컬렉션을 페치 조인하는 것을 다음 포스팅에 설명하겠다.