📌 Fetch Join이란?
JPA에서는 fetch join이 존재합니다. 기존 SQL에서는 존재하지 않아서 생소할 수 있습니다.
fetch join은 JPQL에서 성능 최적화를 위해 제공하는 기능이며 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능입니다. 이해를 위해 Member와 Team을 예시로 들어보겠습니다.
📌 Member와 Team 연관관계

우선 join을 아예 사용하지 않고, Member를 조회를 해보겠습니다.
Join X 예시
String query = "select m from Member m";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
// Member의 Team은 지연로딩
for (Member member : result) {
System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}
// 쿼리
Hibernate:
/* select
m
from
Member m */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.type,
m1_0.username
from
Member m1_0
Hibernate:
select
t1_0.id,
t1_0.name
from
Team t1_0
where
t1_0.id=?
member.getTeam().getName() = teamA
member.getTeam().getName() = teamA
Hibernate:
select
t1_0.id,
t1_0.name
from
Team t1_0
where
t1_0.id=?
member.getTeam().getName() = teamB
먼저 Member를 조회해서 모든 Member들을 불러옵니다. 이후 Member들을 순회하면서 팀에 접근하게 됩니다.
이때 Team은 영속성 컨텍스트에 존재하지 않으므로 첫 번째 회원인 회원 1의 팀을 조회하는 쿼리를 날립니다. 이후 회원 2의 팀은 회원 1이 이미 조회하였기 때문에 1차 캐시에서 바로 가져옵니다.
회원 3의 팀은 영속성 컨텍스트에 존재하지 않으므로 조회하는 쿼리가 나가게 됩니다.
만약 회원마다 팀이 모두 다르다면, 회원마다 각자의 팀을 조회하는 쿼리가 나가게 됩니다. 이러한 상황을 N+1 문제라고 합니다.
이를 해결하기 위해 fetch join 을 사용합니다. fetch join을 적용하기 위해 쿼리를 다음과 같이 수정합니다.
Fetch Join 예시
String query = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
// Member의 Team은 지연로딩
for (Member member : result) {
System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}
// 쿼리
Hibernate:
/* select
m
from
Member m
join
fetch
m.team */ select
m1_0.id,
m1_0.age,
t1_0.id,
t1_0.name,
m1_0.type,
m1_0.username
from
Member m1_0
join
Team t1_0
on t1_0.id=m1_0.TEAM_ID
member.getTeam().getName() = teamA
member.getTeam().getName() = teamA
member.getTeam().getName() = teamB
Member를 조회할 때, Team을 join을 통해 조회하여 Team을 조회하는 쿼리가 추가되지 않았습니다.
📌 fetch join과 일반 join의 차이
얼핏 생각해보면 fetch join이 일반 join과 차이가 없다고 느낄 수 있지만 둘의 차이는 존재합니다.
일반 join 실행 시, 연관된 엔티티를 함께 조회하지 않습니다. 일반 join을 사용한 예시를 살펴보겠습니다.
일반 Join 예시
String query = "select m from Member m join m.team";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
// Member의 Team은 지연로딩
for (Member member : result) {
System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}
Hibernate:
/* select
m
from
Member m
join
m.team */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.type,
m1_0.username
from
Member m1_0
join
Team t1_0
on t1_0.id=m1_0.TEAM_ID
Hibernate:
select
t1_0.id,
t1_0.name
from
Team t1_0
where
t1_0.id=?
member.getTeam().getName() = 팀A
member.getTeam().getName() = 팀A
Hibernate:
select
t1_0.id,
t1_0.name
from
Team t1_0
where
t1_0.id=?
member.getTeam().getName() = 팀B
JPQL은 결과를 반환할 때 연관관계를 고려하지 않습니다. 단지 SELECT 절에 지정한 엔티티만 조회합니다. 즉 위 예시에서는 Member 엔티티만 조회하고, Team 엔티티는 조회하지 않습니다.
반면 fetch join은 사용할 때 연관된 엔티티도 함께 조회합니다. 즉 fetch join은 객체 그래프를 SQL 한 번에 조회하는 방식입니다.
일반 Join은 왜 쓸까?
위 예시들을 보면 join을 하더라도 JPQL은 SELECT절에 지정된 엔티티만 조회하기 때문에 팀에 대한 정보는 결국 다시 조회하는 쿼리가 나가게 됩니다. 일반 join을 사용하여 join 결과를 같이 가지고 나오기를 기대하였지만, 지연 로딩을 통한 조회와 똑같은 결과입니다. 그래서 주로 연관된 엔티티의 정보는 필요없지만, 검색 조건에 사용되는 경우 사용한다고 합니다.
📌 마무리
fetch join은 연관된 엔티티들을 SQL 한 번으로 조회하여 성능을 최적화할 수 있습니다. 엔티티에 @OneToMany(fetch = FetchType.LAZY) 가 적용되어 있다 하더라도 fetch join이 우선되어 적용됩니다.
따라서 실무에서는 전체적으로 지연 로딩을 설정한 후, 최적화가 필요한 곳에 fetch join을 적용시킨다고 합니다.