JPA

[JPA] Fetch Join이란?

tudamoa 2025. 6. 25. 01:57

✅ Fetch Join이란?

fetch join은 JPQL에서 연관된 엔티티를 한 번의 쿼리로 함께 조회하는 기능이다.
JOIN과 달리, 연관 엔티티를 즉시 로딩해서 실제 객체로 채워준다.

 

일반적인 JOIN은 연관 관계를 단순히 결합하는 데 그치지만

FETCH JOIN은 연관된 엔티티를 즉시 로딩(EAGER) 하도록 만들어 실제 객체로 채워준다.

📌 예시

// 일반 JOIN (연관된 team은 프록시 객체)
@Query("SELECT m FROM Member m JOIN m.team t")

// Fetch JOIN (연관된 team을 함께 조회하여 채워 넣음)
@Query("SELECT m FROM Member m JOIN FETCH m.team")

 

✅ 왜 써야 할까? → N+1 문제 해결

@ManyToOne(fetch = LAZY)나 @OneToMany 관계에서 루프를 돌며 연관 데이터를 접근하면 N+1 문제가 발생한다.

fetch join은 단 한 번의 SQL 쿼리로 모든 데이터를 가져오기 때문에 지연 로딩으로 발생하는 불필요한 추가 쿼리 N번을 없애준다.

 

 

🔍 SQL 동작 비교

👎 일반 JOIN (지연 로딩)

-- 1차 조회
SELECT * FROM member;

-- 루프 돌며 발생
SELECT * FROM team WHERE id = 1;
SELECT * FROM team WHERE id = 2;
...

👍 FETCH JOIN 사용 시

-- 한 번의 쿼리로 모든 정보 조회
SELECT m.*, t.* 
FROM member m 
JOIN team t ON m.team_id = t.id;

 

❗ fetch join + 컬렉션(@OneToMany) 주의사항

컬렉션에 대해 fetch join을 하면 조인으로 인해 부모 엔티티가 중복 조회될 수 있다.

 

만약, 이런 식으로 조회를 한다면

@Query("SELECT m FROM Member m JOIN FETCH m.articles")
List<Member> findAllWithArticles();

 

SQL 결과는 다음처럼 나올 수 있다.

m.id | m.name | a.id | a.title
-----|--------|------|--------
1    | Alice  | 101  | 글 A
1    | Alice  | 102  | 글 B

 

-> member가 중복되어 저장되는 상황이 발생한다.

 

✅ 해결 방법:

  • distinct 사용 (JPQL 수준에서 중복 제거):
@Query("SELECT DISTINCT m FROM Member m JOIN FETCH m.articles")
  • 또는 Java 코드에서 Set 등으로 중복 제거

 


 

✅ JOIN vs FETCH JOIN의 차이

항목 JOIN FETCH JOIN
목적 SQL처럼 단순히 조인만 함 조인 + 연관 엔티티를 즉시 로딩
연관 객체 채움 여부 ❌ 프록시 객체 (Lazy 상태 유지) ✅ 실제 객체로 즉시 로딩
N+1 해결 여부 ❌ 해결 안 됨 ✅ 해결 가능
영속성 컨텍스트 ❌ 이용 안 함 ✅ 이용 함

 


 

✅ Fetch Join – 장점, 단점


✅ 장점

장점 설명
N+1 문제 해결 가장 확실한 방법. 연관 엔티티를 조인하여 한 번에 로딩함
성능 향상 연관 객체를 미리 로딩해서 추가 쿼리 없이 사용할 수 있음
쿼리 제어 가능 JPQL 안에서 where, order by, join 조건 등을 함께 조합 가능
예측 가능 언제 어떤 연관 객체까지 로딩되는지 JPQL에서 직접 명시함

 

 

❗ 단점

단점 설명
페이징 불가 (컬렉션 조인 시) @OneToMany나 @ManyToMany에 fetch join을 걸면 Pageable 동작 안 함 (JPA가 count 쿼리 못 만듦)
중복 데이터 발생 가능 컬렉션(fetch join) 시 부모 엔티티가 조인 결과로 여러 번 중복 조회됨 → DISTINCT 필수
데이터 폭발 가능성 조인 대상이 많아질수록 결과 행 수가 많아져 메모리 사용량 급증
동적 쿼리 어려움 복잡한 조건부 fetch join 조합은 QueryDSL 등 추가 도구 없이는 불편
테스트 및 디버깅 어려움 내부적으로 로딩되는 연관 객체를 명확히 구분해 보기 어렵기도 함

 

 

✅ 페이징 시 Fetch Join 오류 상황 발생 예시

📌 기본 페이징 동작 방식

JPA에서 Pageable을 사용할 경우 JPA는 내부적으로 두 가지 쿼리를 자동 생성한다.

  1. 데이터 조회 쿼리 (실제 결과 가져옴)
  2. 총 개수 조회 쿼리 (count(*)로 전체 데이터 수 계산)
-- 1. 데이터 조회
select * from team limit 10 offset 0;

-- 2. 전체 개수 조회
select count(*) from team;

 

 

✅ 그런데 컬렉션 fetch join을 사용하면?

예를 들어 아래처럼 팀과 멤버를 일대다 관계로 가져온다고 가정해보자

@Entity
public class Team {
    @OneToMany(mappedBy = "team")
    private List<Member> members;
}
@Query("SELECT t FROM Team t JOIN FETCH t.members")
Page<Team> findAllWithMembers(Pageable pageable);

 

💥 이 경우 문제가 발생할 수 있다. → 예외가 터짐

 

❗ 왜 예외가 발생할까?

  1. fetch join은 조인 결과의 row 수가 늘어남
    • 팀 1개 + 멤버 3명 → 결과 row 3개
    • 팀 10개 + 멤버 100명 → 결과 row 100개
    • → Team 엔티티는 중복돼서 여러 row로 나타남
  2. JPA는 이 데이터를 Page<Team>으로 매핑하려고 하는데,
    • 중복된 팀을 제거해야 함
    • 하지만 JPQL의 fetch join은 하이버네이트가 count 쿼리를 제대로 생성하지 못함
  3. 결과
    • org.hibernate.loader.MultipleBagFetchException 발생
    • 또는 count 쿼리가 실패하거나, 잘못된 값 반환
    • 즉, 정확한 페이징이 불가능

 

✅ 해결 방법

방법 설명
@EntityGraph 페이징 가능한 fetch join 대체 수단 (연관 엔티티를 그래프 형태로 미리 로딩)
@BatchSize 지연 로딩 그대로 두고, Hibernate가 연관 데이터 한번에 IN 쿼리로 가져오게 함

 

📌 예시 – EntityGraph 사용

@EntityGraph(attributePaths = "members")
Page<Team> findAll(Pageable pageable);

 

위에 처럼 하면 Team만 페이징해서 가져오고 members는 한꺼번에 조인해서 로딩한다.
정상적으로 페이징 + 연관 엔티티도 로딩 가능

 

@EntityGraph는 내부적으로 연관 엔티티를 fetch join처럼 한 번에 불러오면서도
페이징을 위해 필요한 count 쿼리는 건드리지 않기 때문에 안전하게 사용할 수 있는 방법이다.

 

 

 

 

 

 

 

reference

https://hstory0208.tistory.com/entry/JPA-JPQL%EC%9D%98-fetch-join%ED%8E%98%EC%B9%98-%EC%A1%B0%EC%9D%B8%EC%9D%B4%EB%9E%80https://velog.io/@j3beom/JPA-JPQL-Fetch-Join

'JPA' 카테고리의 다른 글

[JPA] QueryDSL 이란?  (0) 2025.06.28
[JPA] 다중성이란?  (0) 2025.06.26
[JPA] N+1 문제 원인 & 해결  (0) 2025.06.24