JPA fetch join

유기훈·2025년 3월 9일

✅ JPA Fetch Join

Fetch Join(패치 조인)은 JPA에서 N+1 문제를 해결하고 연관된 엔티티를 효율적으로 가져오기 위해 사용하는 방법이다.
기본적으로 JPA는 지연 로딩(LAZY Loading)이 기본이지만, Fetch Join을 사용하면 한 번의 SQL 쿼리로 연관된 엔티티를 함께 조회할 수 있다.

✅ 1. Fetch Join 기본 개념

⚡ 기본 JOIN vs Fetch Join 차이점

📌 기본 JOIN (연관된 엔티티는 지연 로딩됨)

SELECT u FROM User u JOIN u.orders o WHERE o.status = 'DELIVERED'
•	User와 Order를 JOIN했지만, User 엔티티만 조회됨.
•	Order 엔티티는 지연 로딩(LAZY) 전략을 따른다.
•	따라서 u.orders를 조회할 때 추가 쿼리(N+1 문제)가 발생할 수 있음.

📌 Fetch Join 사용 (즉시 로딩, 연관된 엔티티 함께 조회)

SELECT u FROM User u JOIN FETCH u.orders WHERE o.status = 'DELIVERED'
•	JOIN FETCH를 사용하면 User와 함께 Order 엔티티까지 한 번의 SQL 쿼리로 가져옴
•	즉시 로딩(EAGER)처럼 동작하지만, 필요한 경우에만 적용 가능
•	N+1 문제 해결 가능!

✅ 2. Fetch Join 예제 코드

⚡ 엔티티 예제

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    private String status;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
}
•	User와 Order는 1:N 관계
•	User는 여러 개의 Order를 가질 수 있음.
•	기본적으로 Order는 지연 로딩(FetchType.LAZY) 설정됨.

⚡ Fetch Join을 사용한 조회

String jpql = "SELECT u FROM User u JOIN FETCH u.orders WHERE u.name = :name";
List<User> users = em.createQuery(jpql, User.class)
                     .setParameter("name", "Alice")
                     .getResultList();

✅ 한 번의 SQL 쿼리로 User와 Order를 함께 가져옴!

📌 실행되는 SQL (예제)

SELECT u.id, u.name, o.id, o.status 
FROM user u 
JOIN orders o ON u.id = o.user_id
WHERE u.name = 'Alice'
•	JOIN FETCH를 사용했기 때문에, 쿼리 한 번으로 User와 Order를 함께 조회
•	N+1 문제 해결됨

✅ 3. Fetch Join 주의할 점
1. Fetch Join은 DISTINCT를 사용하지 않으면 중복이 발생할 수 있음

SELECT DISTINCT u FROM User u JOIN FETCH u.orders
•	User와 Order가 1:N 관계이므로, User가 중복 조회될 가능성이 있음.
•	DISTINCT를 추가하면 중복 제거 가능 (JPA가 중복된 엔티티를 자동으로 제거).

2.	Fetch Join은 여러 개를 사용할 수 없다
SELECT u FROM User u JOIN FETCH u.orders JOIN FETCH u.address
•	두 개 이상의 Fetch Join 사용 불가능! (JPA가 이를 허용하지 않음)
•	해결 방법: “Batch Fetch Size”를 활용하거나 DTO로 변환

3.	페이징 (setMaxResults, setFirstResult) 불가능
❌ em.createQuery("SELECT u FROM User u JOIN FETCH u.orders")
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();
•	Fetch Join은 DB에서 데이터를 한 번에 가져오므로, 페이징을 적용할 수 없음.
•	해결 방법: “Batch Fetch Size”를 활용하거나 DTO로 변환.

✅ 4. Fetch Join을 대체할 수 있는 방법

① Batch Fetch Size 설정 (Hibernate 옵션 사용)
• @BatchSize 또는 hibernate.default_batch_fetch_size를 설정하여 N+1 문제를 줄일 수 있음

@Entity
@BatchSize(size = 10)
public class User { ... }

📌 Hibernate 설정 추가 (application.yml)

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 10
•	default_batch_fetch_size: 10으로 설정하면 한 번의 IN 쿼리로 10개씩 가져옴

② DTO를 활용한 직접 조회 (Projection)

String jpql = "SELECT new com.example.dto.UserOrderDTO(u.name, o.status) " +
              "FROM User u JOIN u.orders o";
List<UserOrderDTO> results = em.createQuery(jpql, UserOrderDTO.class)
                               .getResultList();

Batch Fetch Size를 잘 활용하자

@OneToMany 를 사용하는 1:N 관계인 엔티티를 fetch join 하지 말자. distinct 문제도 있을 뿐더러 페이징을 한다면 페이징을 메모리에서 처리 하기 때문에 절대 쓰면 안된다.

1:N 관계인 엔티티를 사용하고자 할 때는 batch fetch size를 활용하자. 설정만 해두면 jpa가 알아서 in 쿼리로 db에서 객체를 가져온다.

profile
개발 블로그

0개의 댓글