✅ 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();
@OneToMany 를 사용하는 1:N 관계인 엔티티를 fetch join 하지 말자. distinct 문제도 있을 뿐더러 페이징을 한다면 페이징을 메모리에서 처리 하기 때문에 절대 쓰면 안된다.
1:N 관계인 엔티티를 사용하고자 할 때는 batch fetch size를 활용하자. 설정만 해두면 jpa가 알아서 in 쿼리로 db에서 객체를 가져온다.