Skip to content

FetchJoin

fetch join은 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 페치 조인은 연관된 엔티티나 컬렉션을 한 번에 같이 조회해준다.

일반적으로 연관관계가 맺어진 엔티티를 조회할때, N+1문제를 해결하기 위해서 FetchType.LAZY를 사용하는 경우가 있다. 하지만 두 엔티티를 모두 조회해야하는 경우엔 두개의 쿼리를 날려야하기 때문에 N+1문제가 동일하게 발생한다. 이럴 때 fetch join을 사용하면 하나의 쿼리로 두 엔티티를 한 번에 조회할 수 있다.

1:N 조인에서 fetch join을 사용하면 결과의 갯수가 늘어날 수 있다.(‘1’쪽에서 조회해야하는 엔티티의 총 갯수가 아닌, ‘1’쪽의 엔티티 갯수만큼의 결과가 조회된다.) 이 경우 JPQL의 DISTINCT를 사용해서 결과의 갯수를 줄일 수 있다.

fetch join과 일반 join의 차이

select t
from Team t join t.members m
where t.name='팀A'

위와 같은 JPQL 쿼리를 실행할 경우, Member는 조회되지 않고 Team의 정보만 조회된다. (사실상 join하는 의미가 없다.)

select t
from Team t join fetch t.members m
where t.name='팀A'

fetch join으로 바꿔주면 출력되는건 Team의 정보 뿐이지만, 영속성 컨텍스트에는 member의 정보까지 전부 저장된다. fetch join은 일반 join과 다르게 연관된 엔티티를 함께 조회한다. (FetchType.LAZY를 설정한 경우엔 명시적으로 join한 엔티티만 조회한다.)

fetch join은 객체 그래프를 전부 조회하는 개념이라고 생각할 수 있다.

fetch join의 특징과 한계

1. fetch join은 전체를 끌고와야한다.

fetch join 대상에는 별칭을 줄 수 없고, 별칭으로 조건을 거는 것도 불가능하다. fetch join은 하나의 쿼리를 연관된 엔티티를 모두 가져올 때 사용해야 한다. WHERE절 등을 통해서 필터링한다는 것은 객체 그래프 전체를 가져온다는 fetch join의 의의와 반대되는 행위이다. 연관된 객체의 일부 정보만 필요하다면 별도의 쿼리를 실행해야한다.

2. 둘 이상의 컬렉션은 fetch join 할 수 없다.

위에서 말했다시피, 1:N 조인에서 fetch join을 사용하면 결과의 갯수가 늘어날 수 있다. 둘 이상의 컬렉션을 fetch join 한다면 1:N:M 관계가 되는데, 이렇게 되면 더 많은 양의 데이터가 반환되거나 데이터가 꼬일 수 있다.

3. 1 or N:M 관계에서 fetch join 결과로 페이징 API를 사용할 수 없다.

1:1, N:1 같은 단일 값 연관 필드들은 페이징이 가능하다. 왜냐하면 하나의 데이터에 연관 필드가 딱 하나씩 대입되기 때문이다. 하지만 N:M 이나 1:N 관계에서는 페이징을 사용한 경우 잘못된 데이터가 반환될 수 있다.

이런 상황이 있다고 가정해보자.

1. Team과 Member가 1:n 관계이고, Team 데이터가 3개, Member 데이터가 10개 있다.
2. Team을 기준으로 Member를 가져오면 총 10개의 행이 생긴다.

이때, Team을 기준으로 페이징을 한다면 결과값의 갯수가 일정하지 않기 때문에 페이징을 하는 의미가 없다.

그리고 행을 기준으로 페이징을 한다면 한 Team에 속해있는 유저가 페이지에 반만 포함되는 경우 잘못된 데이터가 출력된다.

그렇기 때문에 fetch join 결과로 페이징을 하고 싶으면 역으로 뒤집어서 조회하거나(1:n -> n:1) @BatchSize를 사용해서 총 결과값의 수를 제한하는 방법을 사용할 수 있다.

정리

fetch join은 객체 그래프를 유지하며 엔티티를 조회해야 할 때 사용할 수 있는 최적화 방법이다. N+1문제를 해결하기 위해 자주 쓰이지만, 모든 것을 해결할 수는 없다. 상황에 따라 적절히 사용하면 좋을 것 같다.