본문 바로가기
Back-end/JPA 개념

31. JPQL - Fetch Join 개념

by devraphy 2022. 5. 4.

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

댓글