JPA/JPQL

[JPA] 페치 조인 ( JOIN FETCH )

IT록흐 2023. 6. 15. 22:38
반응형

 

 

엔티티의 연관 엔티티를 로드하는 전략은 두 가지가 있다.

 

1. 지연로딩 ( Lazy )

2. 즉시로딩 ( Eager )

 

그러나 두 가지 전략은 문제가 있다.

 

즉시로딩은 연관된 엔티티를 로드하려고 SELECT문이 자동으로 하나 생성되어 실행된다. 지연로딩은 연관된 엔티티에 무언가가 접근하면 SELECT문이 자동으로 생성되어 실행된다. 이를 N+1문제라 한다. 개발자는 한 개의 쿼리를 만들었는데, 자동으로 N개의 쿼리가 생성되어 실행되는 것이다. 이처럼 N개의 쿼리문이 추가로 실행된다면 반복적으로 DB와 I/O가 발생하여 성능에 좋지 못한 영향을 준다. 

 

 

JOIN FETCH

 

페치 조인의 목적은 하나의 쿼리로 연관된 엔티티를 모두 로드하는 것이다. 이는 특정 로직에 필요한 '데이터'를 추출하려는 목적이 아니다. 엔티티가 연관 엔티티를 안정적으로 참조할 수 있도록 엔티티 생태계 구축이 목적이다. 그래야 N+1문제처럼 추가로 SELECT문이 실행되는 일이 없다. 이런 이유로 페치 조인에는 몇 가지 제약이 있다. 

 

1. 별칭을 줄 수 없다. 

SELECT distinct t 
FROM Team t 
JOIN FETCH t.members m -- ( X ) 별칭을 주면 안 된다. 

SELECT distinct t 
FROM Team t J
OIN FETCH t.members -- ( O )

 

별칭의 존재 이유는 '컬럼'에 접근하기 위함이다. 컬럼에 접근한다는 의미는 전체가 아닌 부분에 접근한다는 의미이다. 부분에 접근하면 WHERE절에 필터링을 걸 수 있다. 필터링을 걸면 전체가 아닌 일부 엔티티만 로드된다. 페치 조인은 '특정 조건'에 부합하는 엔티티를 추출할 목적으로 존재하지 않는다. 페치조인은 엔티티가 안정적으로 연관 엔티티에 접근할 수 있는 환경 마련이 목적이다. ( N+1문제 방지 ) 그러므로 페치 조인이 사용되면 페치 조인 대상 엔티티는 모두 로드되어야 한다. 

 

2. 일대다 연관관계에서 페이징을 걸 수 없다. 

 

일대다 관계에서 '다'를 JOIN FETCH로 가져오면 '페이징'이 안 된다. JOIN FETCH의 목적은 연관된 엔티티를 전부 메모리에 로딩하여 추가적인 쿼리실행을 막아 성능을 최적화하는데 있다. 그런데 페이징은 전체가 아닌 '부분'을 가져오는 기능이다. 다'의 부분만 로딩하면 추후에 추가적인 쿼리생성을 유발할 수 있다.

 

그래서 일대다관계에서 JOIN FETCH에 페이징을 걸면 JPA는 경고메시지를 출력한다. 

HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

 

일단 전부 메모리에 로딩하고 페이징을 하겠다는 의미이다. 페이징은 DB에서 원하는 부분만 추려서 메모리로 가져와야 하는데, 데이터 전부를 메모리로 가져온 다음에  페이징을 한다. 이는 OOM(Out Of Memory)를 유발할 수 있다. JOIN FETCH의 기능을 위해 발생하는 어쩔수 없는 트레이드-오프이다. 

 

3. 둘 이상의 컬렉션은 페치 조인 할 수 없다. 

 

이것도 전체를 가져오는 페치 조인의 특성과 관련있다. 조인 연산은 테이블을 합치는 연산이다. 이는 카테시안 곱(곱집합)으로 이루어진다. 

 

SELECT distinct t 
FROM Team t J
OIN FETCH t.members

 

Team 테이블과 Member 테이블이 있다. Team 테이블의 레코드 개수가 N이고 Member 테이블의 레코드 개수가 M이라면 카타시안 곱으로 나올 수 있는 경우의 수는 NxM이다. 다시 말해, 필터링하는 조인조건이 없다면 기하급수적인 경우의 수가 나온다는 말이다. 엔티티는 식별자가 있다. 식별자가 조인조건으로 활용되어 카타시안 곱의 경우의 수 중 조건에 맞는 경우만 필터링한다.

 

그러나 만약 컬렉션이 두 개면 어떻게 될까?

 

SELECT o FROM Order o
JOIN FETCH o.orderItems
JOIN FETCH o.otherCollection -- ( X )

 

NxMxK가 된다. 필터링되는 조인조건도 엔티티의 식별자뿐이니 경우의 수는 기하급수적으로 증가한다. JOIN FETCH는 연관된 엔티티 전부를 가져오는 전략이므로 컬렉션이 둘 이상을 같이 쓰면 과부하가 걸려 성능에 악영향을 준다. 이런 이유로 JPA는 페치조인에 컬렉션을 둘 이상 사용하는 것을 허용하지 않는다. 

 

SELECT o FROM Order o JOIN FETCH o.orderItems
SELECT o FROM Order o JOIN FETCH o.otherCollection

 

 

만약 두개의 연관 컬렉션을 모두 가져와야 한다면 2개의 SELECT문으로 분리해야 한다.

 

 


 

 

페치 조인은 특정한 비즈니스 로직을 위해 존재하는 개념이 아니다. JPA 프레임워크의 성능 향상을 위해 존재한다. 그러므로 비즈니스 로직을 위한 JOIN이 필요하면 일반 JOIN을 사용하면 된다. 별칭을 붙여 컬럼에 접근하여 WHERE 절로 필터링하여 필요한 데이터를 DTO에 담아 서비스 계층으로 넘기면 된다. 그게 아니라, 연관된 엔티티를 모두 로드하여 추가적인 쿼리문 생성을 방지하려는 목적이 있다면 페치 조인을 사용하면 된다. 

 

 


 

 

참고자료

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

 

 

 

반응형