본문 바로가기

JPA/JPQL

페치조인 특징과 한계

github 전체코드 주소(branch: fetchDetail)

별칭 사용

페치 조인 대상에는 JPA 표준 스펙 상 별칭 사용이 불가능하다.

하이버네이트에서는 사용이 가능하지만, 가급적 사용하지 않는 것이 좋다.(특히 where문에 사용 금지)

 

DB data

JpaMain.java - 조회 로직

String query = "select distinct c from Club c join fetch c.students s where s.id <= 3";
List<Club> clubs = em.createQuery(query, Club.class).getResultList();

for (Club c : clubs) {
  System.out.println("name = " + c.getName());
  System.out.println("student size = " + c.getStudents().size());
  System.out.println("================================");
}

JPQL을 통해 조회된 c1의 students 필드 size는 1이다.
where문에 적합한 id <= 3인 데이터는 하나이기때문이다.
하지만 JPA 관점에서 `club.getStudents()`는 해당 club에 속한 모든 student를 반환해주는 것이 바람직하다.

 

다음 예시를 통해 왜 그런지 살펴보자
club의 students 필드로부터 Student Entity를 얻어와서 메시지를 보내주는 메서드가 있다고 하자!
이 메서드는 club의 students 필드에 모든 학생이 포함되어 있다는 가정 하에 작성됐다.
즉, 특정 club의 모든 학생들에게 메시지를 보내주려는 의도로 작성된 메서드이다.
하지만 위와 같이 조회된 club이 해당 메서드의 매개변수로 넘어갈 경우,
메시지는 해당 club의 학생들 중 id <= 3인 학생에게만 보내진다.

 

이처럼 로직 작성 시 특정 엔티티의 컬렉션 연관필드에는 해당 엔티티와 연관된 모든 데이터가
저장되어있다고 가정하고 로직을 작성하게 된다.

따라서, 컬렉션 연관필드는 Entity와 연관된 모든 값을 가지고 있는 것이 바람직하다.
그리고 위처럼 특정 학생만 가져오고싶은 경우,
Club Entity를 대상으로 하는 쿼리에서 fetch join을 통해 students를 가져올 것이 아니라
Student Entity를 대상으로 하는 쿼리에서 조건문을 걸어서 가져오는 것이 바람직하다.

 

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

둘 이상의 컬렉션은 동시에 페치 조인하는 것이 불가능하다.

 

H2 Data

JpaMain.java - 조회 로직

String query = "select c from Club c join fetch c.students s join fetch c.rooms";
List<Club> clubs = em.createQuery(query, Club.class).getResultList();

for (Club c : clubs) {
  System.out.println("name = " + c.getName());
  System.out.println("student size = " + c.getStudents().size());
  System.out.println("================================");
}

두 개 이상의 컬렉션을 동시에 페치조인하면 다음과 같은 에러가 발생한다.

MultipleBagFetchException

페이징

1:1, N:1 같은 단일값 연관필드에 대해서는 페치조인 후에도 페이징이 가능하지만,

컬렉션을 페치조인하면 페이징 API를(setFirstResult, setMaxResults) 사용할 수 없다.
이유를 살펴보자!

Club과 Student join 후 Table

JpaMain.java - 조회 로직

//페이징
String query = "select c from Club c join fetch c.students";
List<Club> clubs = em.createQuery(query, Club.class)
                      .setFirstResult(0)
                      .setMaxResults(1)
                      .getResultList();

for (Club c : clubs) {
  System.out.println("name = " + c.getName());
  System.out.println("student size = " + c.getStudents().size());
  System.out.println("================================");
}

컬렉션 연관필드를 fetch join 후 페이징처리를 하는 것은 다음과 같은 문제가 있다.
Student와 Club join 실행 후 테이블을 보면 3개의 행이 있는데,
JPA에서 페이징 쿼리로 데이터를 하나만 들고 오면 맨 첫 행만 들고 오게 된다.
따라서 조회해온 c1의 students size는 1이 된다.
하지만 JPA는 컬렉션 연관필드에 모든 요소들을 저장하는 것을 지향한다.(위쪽의 별칭 부분 참고)

따라서 이를 방지하기 위해 하이버네이트는 경고로그를 남기고 메모리에서 페이징한다.(매우 위험)

 

경고 로그 생성 후 페이징처리

로그를 보면 페이징쿼리가 나가지 않은 것을 볼 수 있는데,
이는 모든 데이터를 조회 후 메모리에서 페이징을 한 것으로 성능이 좋지 않다.

방향 전환하여 쿼리 날리기

지금같은 경우는 join 방향을 뒤집어서  fetch join하여 이 현상을 해결할 수 있다.

 

JpaMain.java - 조회 로직

//방향 전환 페이징 쿼리
String studentQuery = "select s from Student s join fetch s.club";
List<Student> students = em.createQuery(studentQuery)
                            .setFirstResult(0)
                            .setMaxResults(1)
                            .getResultList();

limit을 통한 페이징 쿼리

BatchSize 어노테이션 활용하기

기존 JPQL에서 fetch join문 제거 후 @BatchSize(size = 100) 어노테이션 사용할 수도 있다.
해당 어노테이션은 Entity 조회 후 연관컬렉션필드를 가져오는 쿼리를 날려준다.

 

Club.java - BatchSize 설정

@OneToMany(mappedBy = "club")
@BatchSize(size = 100)
private List<Student> students = new ArrayList<>();


JpaMain.java - 조회 로직

String query = "select c from Club c";
List<Club> clubs = em.createQuery(query, Club.class)
                      .setFirstResult(0)
                      .setMaxResults(1)
                      .getResultList();

for (Club c : clubs) {
  System.out.println("name = " + c.getName());
  System.out.println("student size = " + c.getStudents().size());
  System.out.println("================================");
}

먼저 Club Entity가 페이징 쿼리를 통해 조회된다.
그리고 이와 연관된 Student Entity를 조회하는 쿼리가 한 번 더 나간다.
만약 @BatchSize를 사용하지 않는다면, 지연로딩 전략으로 인해

실제로 Student Entity를 사용하는 시점에 조회 쿼리가 나가게 된다.

참고강의: 배달의 민족 개발팀장 김영한 강사님의 JPA 강의

'JPA > JPQL' 카테고리의 다른 글

Bulk 연산  (0) 2021.10.16
JPQL 엔티티 조회  (0) 2021.10.15
N+1과 fetch join  (0) 2021.10.02
경로표현식  (0) 2021.10.02
JPQL 함수  (0) 2021.09.25