JPQL 페치 조인
페치 조인은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 쵝적화를 위해 제공하는 기능이다.
연관덴 엔티티나 컬렉션을 한번에 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.
JPQL이 무엇인지 모르겠다면 아래 블로그 글을 참고 바란다.
엔티티 페치 조인
JPQL에서는 조인은 별칭을 사용할 수 없지만 하이버네이트는 별칭은 허용한다.
페치 조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회해보자.
// JPQL
"SELECT m FROM Member m JOIN FETCH m.team"
// SQL
"SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID"
SELECT m으로 회원 엔티티만 조회했지만 연결된 TEAM 데이터도 조회된 것을 확인할 수 있다.
SQL을 확인해보면 회원과 팀을 함께 조회했기때문에 프록시가 아닌 실제 엔티티로 조회되므로 지연 로딩이 일어나지 않는다. 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
컬렉션 페치 조인
일대다 관계인 컬렉션을 페치 조인해보자
// JPQL
"SELECT t FROM Team t JOIN FETCH t.members WHERE t.name = '팀A'"
// SQL
"SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME = '팀A'"
마찬가지로 SELECT t로 팀만 조회했지만 연결된 MEMBER 데이터도 조회된 것을 확인할 수 있다.
위 엔티티 페치 조인과 같이 이유로 지연 로딩은 일어나지 않는다.
'팀A'는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해서 결과값은 2개가 조회된다.
하지만 TEAM의 정보를 조회하는데 MEMBER 데이터 때문에 똑같은 데이터가 중복되는것을 막아야 한다.
이럴때 DISTINCT를 사용한다.
페치 조인과 DISTINCT
SQL의 DISTINCT는 중복된 결과가 제거하는 명령어다.
JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.
위에 예제에서는 TEAM 데이터가 중복으로 2개 조회가 된다. DISTINCT를 추가해보자
"SELECT DISTINCT t FROM Team t JOIN FETCH t.members WHERE t.name = '팀A'"
DISTINCT를 추가해도 SQL에서는 MEMBER 데이터가 다르기 때문에 효과가 없다.
애플리케이션에서 DISTINCT 명령어를 보고 중복된 데이터를 걸러내서 중복된 결과값을 제거해준다.
페치 조인과 일반 조인의 차이
위에 예제를 페치조인을 사용하지 않고 일반 조인을 사용하면 어떻게 될까?
// JPQL
"SELECT t FROM Team t JOIN t.members WHERE t.name = '팀A'"
// SQL
"SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME = '팀A'"
팀과 회원 컬렉션을 조인했으므로 회원 컬렉션도 함게 조회할 것으로 기대하지만 실제는 회원 컬렉션은 전혀 조회되지 않는다.
JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
만약 회원 컬렉션을 지연 로딩으로 설정하면 아직 초기화 되지 않은 컬렉션 래퍼를 반환한다.
즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한번 더 실행한다.
반면 페치 조인을 사용하면 연관된 엔티티도 함께 조회한다.
페치 조인의 특징과 한계
페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다. @OneToMany(fetch = FetchType.LAZY)로 글로벌 로딩 전략을 설정해도 페치 조인을 우선한다.
최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다.
그래서 준영속 상태에서도 객체 그래프를 탐색 할 수 있다.
몰론 일부는 빠를 수 있지만 전체로 보면 사용하지 않는 엔티티도 자주 불러오게 되 성능에 악영향을 미칠 수 있다.
따라서 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
페치 조인에 한계는 다음과 같은 것들이 있다.
- 페치 조인 대상에는 별칭을 줄 수 없다.
문법을 보면 페치 조인에 별칭을 정의하는 내용이 없기 때문에 SELECT, WHERE 절 서브 쿼리에 페치 조인 대상을 사용할 수 없다. 하이버네이트를 포함한 몇몇 구현체들은 페치 조인에 별칭을 지원한다. 하지만 별칭을 잘못 사용하면 연관된 데이터 수가 달라져서 데이터 무결성이 깨질 수 있으므로 조심해서 사용해야 한다. 특히 2차 캐시와 함께 사용할 때 조심해야 한다. - 둘 이상의 컬렉션을 페치할 수 없다.
구현체에 따라 되기도하는데 컬렉션 * 컬렉션의 카테시안 곱이 만들어지므로 주의해야 한다.
하이버네이트를 사용하면 PersistenceException 예외가 발생한다. - 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 페치 조인을 사용해도 페이징 API를 사용할 수 있다.
하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기면서 메모리에 페이징 처리를 한다. 데이터가 적으면 상관없지만 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어서 위험하다.
페치 조인은 SQL 한 번으로 연관된 어러 엔티티를 조회할 수 있어서 성능 최적화에 상당히 유용하다. 그리고 실무에서 자주 사용하게 된다. 하지만 모든 것을 페치 조인으로 해결할 수 는 없다.
페치 조인은 객체 그래프를 유지할 떄 사용하면 효과적이다. 반면에 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 억지로 페치 조인을 사용하기보다는 여러 테이블에서 필요한 필드들만 조회해서 DTO로 반환하는 것이 더 효과적일 수 있다.