0. 개요
- 이번 포스팅은 JPQL의 주요 기능 중 하나인 Fetch Join에 대해서 알아보자.
- Fetch Join을 설명하기 위해 다음 Entity를 예시로 사용할 예정이다.
@Entity
public class Player {
@Id @GeneratedValue
private Long id;
private String name;
private String position;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// Getter & Setter 생략
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Player> players = new ArrayList<>();
// Getter & Setter 생략
}
1. Fetch Join 개념
a) Fetch Join이란?
- SQL에서 사용할 수 있는 Join이 아니라 JPQL의 성능 최적화를 위해 제공되는 기능이다.
- 연관된 Entity나 컬렉션을 조회할 때 발생하는 다수의 쿼리문을 하나의 쿼리문으로 해결한다.
- 즉, Fetch Join은 Join을 할 때 연관된 Entity를 포함하여 Join을 수행한다.
- 이는 일반적인 Join과의 차이점이다.
b) 언제 , 왜 사용할까?
- 다음과 같은 JPQL을 실행해보자.
SELECT p FROM Player p;
- 위의 JPQL을 실행하면 다음과 같은 쿼리가 발생한다.
- Player Entity는 @ManyToOne 관계를 갖는 Team Entity의 참조 필드를 가지고 있다.
- 분명 Team 참조 필드를 가지고 있는데, 위의 쿼리에서는 Player 테이블만 조회한다.
- 그 이유는 Player Entity에서 Team 참조 필드를 지연 로딩으로 처리했기 때문이다.
- 그렇다면 다음과 같은 JPQL을 실행해보자.
String query = "select p from Player p";
List<Player> resultList = em.createQuery(query, Player.class).getResultList();
for (Player player : resultList) {
System.out.println("player = " + player.getName() + " team = " + player.getTeam().getName());
}
- 앞서 Player Entity 내부에는 Team 참조 필드를 지연 로딩으로 처리했다고 하였다.
- 그러므로 Team 객체의 정보를 요청할 때에만 Team 테이블을 조회하는 쿼리가 발생한다.
- 위의 사진에서 볼 수 있듯이, Player 테이블을 조회한 후 Team 테이블을 2번 조회한다.
- 더불어, Team 테이블이 조회될 때마다 결과가 따로따로 출력되는 것을 확인할 수 있다.
- 왜 이렇게 되는 것일까?
- 현재 DB에 저장되어 있는 Player 테이블의 데이터를 보자.
- 최초에 Player 테이블이 조회된다. 이때 박지성, 호날두, 손흥민 순서로 데이터를 가져온다.
- 첫 번째 Team 테이블 조회 시, 박지성이 사용되고 박지성의 Team 정보인 맨유를 가져온다.
- 여기서 첫 번째로 조회된 Team 객체(= 맨유)는 영속성 Context의 1차 캐시에 저장된다.
- 두 번째 Team 테이블 조회 시, 호날두가 사용된다. 그러나 호날두는 박지성과 같은 Team 정보를 가진다.
- 그러므로 Team 테이블을 조회하지 않고, 영속성 Context에 저장되어 있는 데이터(= 맨유)를 반환한다.
- 세 번째 Team 테이블 조회 시 손흥민이 사용된다. 손흥민의 Team 정보는 1차 캐시에 없는 데이터다.
- 그러므로 Team 테이블을 조회하고, 새로운 Team 정보(= 토트넘)를 영속성 Context에 저장과 동시에 반환한다.
c) N + 1 문제
- 여기서 Fetch Join을 사용하는 이유에 대해서 확인할 수 있다.
- 이전에 N + 1 문제에 대한 해결책으로 Proxy 객체를 사용한 지연 로딩을 알아보았다.
- 그러나 지연 로딩이 온전한 해결책은 아니다.
- 지연 로딩 처리된 데이터를 조회하더라도 매번 다른 데이터가 검색되면 매번 새로운 쿼리문이 발행되기 때문이다.
- 즉, 영속성 Context에 존재하지 않는 데이터를 조회하는 경우 매번 새로운 쿼리문이 발행된다.
ex) 100명의 선수가 다른 팀 정보를 가진다면 100(= Team 조회) + 1(= Player 조회) 개의 쿼리가 발생된다.
- 이와 같은 이유로, N + 1개의 쿼리를 하나의 쿼리로 처리할 수 있는 Fetch Join이 필요하다.
2. Fetch Join 사용 방법
a) Fetch Join 사용 예시
- Fetch Join은 다음과 같은 문법으로 사용할 수 있다.
- Fetch Join을 이용하면 위에서 보았던 예시를 다음과 같이 한 줄의 JPQL로 작성할 수 있다.
SELECT p FROM Player p JOIN FETCH p.team;
- 위의 JPQL은 다음과 같은 쿼리문으로 번역된다.
SELECT p.*, t.* FROM Player p INNER JOIN Team t ON p.team_id = t.id;
- 이를 실행해보면 다음과 같은 쿼리문을 발행한다.
String query = "select p from Player p join fetch p.team";
List<Player> resultList = em.createQuery(query, Player.class).getResultList();
for (Player player : resultList) {
System.out.println("player = " + player.getName() + " team = " + player.getTeam().getName());
}
- 이처럼 Fetch Join을 사용하면 하나의 쿼리문으로 N + 1 문제없이 결과를 출력하는 것을 확인할 수 있다.
- 참고로, 지연 로딩 설정을 해도 Fetch Join을 사용하면 Fetch Join이 우선순위를 갖는다.
b) INNER / OUTER(= LEFT, RIGHT)
- Fetch Join은 JOIN을 기반한 기능이다. 그러므로 INNER JOIN을 default로 사용한다.
- 더불어, OUTER JOIN 또한 사용할 수 있다.
- 간단한 예시를 통해 표현 방법에 대해 알아보자.
SELECT p FROM Player INNER JOIN FETCH p.team; // INNER 키워드 생략 가능
SELECT p FROM Player LEFT OUTER JOIN FETCH p.team; // OUTER 키워드 생략 가능
SELECT p FROM Player RIGHT OUTER JOIN FETCH p.team; // OUTER 키워드 생략 가능
3. 일반 Join과 Fetch Join의 차이점
- 일반 Join의 경우, Join을 수행할 때 연관 관계를 포함하지 않는다.
- Fetch Join의 경우, Join을 수행할 때 연관 관계를 포함한다.
- 일반 Join의 경우, SELECT 절에 지정된 Entity만을 조회한다.
- Fetch Join의 경우, 연관된 Entity를 함께 조회한다.
- Fetch Join은 즉시 로딩으로 동작한다. 여기에도 차이점이 있다.
- 일반적인 즉시 로딩은 연관된 Entity 조회 시, 추가적인 쿼리문이 발행된다. (N + 1 문제 발생)
- Fetch Join의 경우, 하나의 쿼리문을 사용하여 연관된 Entity를 함께 조회한다. (N + 1 문제 없음)
'Back-end > JPA 개념' 카테고리의 다른 글
33. JPQL - Fetch Join의 한계 (0) | 2022.05.06 |
---|---|
32. JPQL - Collection Fetch Join과 Distinct (0) | 2022.05.05 |
30. JPQL - 경로 표현식과 묵시적 JOIN (0) | 2022.05.03 |
29. JPQL - 사용자 정의 함수 (0) | 2022.05.02 |
28. JPQL - case 식 (0) | 2022.04.28 |
댓글