본문 바로가기

JPA

[JPQL] 페치 조인 - Fetch Join! (JPA의 N+1 문제 해결법)

실무에서 정말 정말 중요한 Fetch Join...에 대해 학습하자! 

(복습하고 또 복습하기)

Fetch Join

연관된 엔티티나 컬렉션을 한 번에 함께 조회하는 기능 ( = 즉시 로딩) ➡️ 성능 향상!

  • 일반적인 SQL 조인 종류가 아니다!
  • JPQL에서 제공하는 성능 최적화를 위한 Join 기능
  • join fetch

Fetch Join 사용법

  • JOIN FETCH 명령어로 사용
#1. OUTER JOIN (OUTER은 생략 가능!)
LEFT [OUTER] JOIN FETCH + 조인 경로

#2. INNER JOIN (INNER 생략 가능!)
[INNER] JOIN FETCH + 조인 경로

예시

상황)

회원(다) - 팀(일) 의 연관관계를 맺은 두 엔티티가 있다.

페치 조인을 통해, 회원 엔티티를 조회 시, 연관된 팀도 한 번에 조회할 수 있다.

# JPQL
select m from Member m join fetch m.team

위 JPQL의 실제 SQL문은 아래와 같다.

# SQL
select 
	M.*, T.* 
from 
	member m 
inner join 
	Team t 
	on m.Team_id = t.id

페치 조인 안쓰면?

만약, 아래와 같이 페치 조인을 안쓰면, 어떻게 될까?

// 페치 조인 X
...
String query ="select m from Member m ";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();

for (Member m : resultList) {
	System.out.println(m.getTeam());
}
...

기본적으로, JPA에서 Fetch 전략은 지연로딩을 권장하고, 대부분 지연로딩으로 설정한다.

지연로딩 전략에서 위 처럼 페치 조인 없이 실행할 경우, Member.getTeam()을 실행할 때, 다른 팀을 조회할 때마다 쿼리문이 날라간다(N + 1 문제 발생).

하지만, 지연로딩 상황에서 아래와 같이 페치 조인을 사용한다면,

//페치 조인 사용
String query ="select m from Member m join fetch m.team";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();

 

아래와 같이 한 번에 연관된 엔티티도 가져온다!

대부분의 지연로딩 전략이 권장되므로, 페치 조인을 잘 활용하자!

컬렉션 패치 조인

위의 코드에서 Member가 아닌, Team을 기준으로 일 대 다 상황에서도 패치 조인을 할 수 있다.

코드 사용 예시

// JPQL
select t from Team t join fetch t.Members

// 실제 동작 SQL
SELECT
	t.*, m.*
FROM
	Team t
INNER JOIN 
	Member m
	ON t.id = m.team_id

상황

회원1 - 팀A

회원2 - 팀A

회원3 - 팀B

예제 코드

String query ="select t from Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class).getResultList();

System.out.println("---------------------");
for (Team s :
	resultList) {
	System.out.println(s + " >> " + s.getMembers());
}
System.out.println("---------------------");

결과

주의할 점

분명, Team은 "팀A"와 "팀B" 총 2개인데, 회원의 수만큼 총 3번의 조회가 이루어졌다.

즉, 팀A가 중복되어 저장되있다.

 ❇️ ("일:다" 조인만 중복이 일어나고, "다:일" 조인은 중복이 없다 !) 

컬렉션 페치 조인에서 중복 제거

JPQL의 DINSTINCT 명령어를 사용하여, 위와 같은 중복을 제거할 수 있다.

(JPQL DISTINCT의 기능 = SQL의 DISTINCT + ENTITY 중복 제거)

코드 예시

String query ="select DISTINCT t from Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class).getResultList();

System.out.println("---------------------");
for (Team s :
        resultList) {
    System.out.println(s + " >> " + s.getMembers());
}
System.out.println("---------------------");

출력

페치 조인과 일반 조인의 차이

일반 조인은 연관 엔티티를 가져오지 않는다.

➡️ 즉, 페치 없이 조인만 사용할 경우, 처음 말했던, N+1 문제가 일어난다.

 

페치 조인의 한계

1. 페치 조인 대상에는 별칭을 못 준다

(하이버네이트는 가능하지만, 권장 ❌)

# JPQL
select t from Team t join fetch t.members as m # "as m"처럼 별칭 ❌

2. 둘 이상의 컬렉션은 페치 조인 불가

  ( 데이터가 어마어마하게 뻥튀기 될 수 있다...)

 

3. 컬렉션을 페치 조인 ➡️ 페이징 API 불가 (setFirstResult & setMaxResult )

(왜? 데이터 중복때문에 뻥튀기 되니까... )

  3.1 일대일, 다대일 같은 단일 값 연관 필드들은 가능

  3.2 distinct를 사용해도, DB에서 넘어오는 데이터들은 중복이 완전히 제거되지 않기 때문에, 위험하다.

  3.3 사실 가능은 하지만, 경로 로그를 남기며, 메모리에서 동작하고 매우 위험하다 !!

  ➡️ @BatchSize 애너테이션을 활용하자 !

  ➡️ 혹은 아래와 같이 글로벌 세팅으로 설정해주자.

<persistence.xml>

<property name="hibernate.default_batch_fetch_size" value="2000"/>

강의

https://www.inflearn.com/course/lecture?courseSlug=ORM-JPA-Basic