Home [Spring JPA] Fetch Join vs 일반 Join
Post
Cancel

[Spring JPA] Fetch Join vs 일반 Join

일반 Join

JPQL에서 사용하는 Join문법은 SQL과 매우 유사합니다.

1
2
select m from Member m 
    inner join m.team t

이런식으로 내부 조인을 할 수 있습니다. inner 키워드는 생략할 수 있습니다.

위의 jpql을 수행해보면 아래와 같은 쿼리가 나옵니다. (Mysql 기준)

1
2
3
4
5
6
7
8
9
select
    m1_0.id,
    m1_0.name,
    m1_0.team_id 
from
    Member m1_0 
join
    Team t1_0 
        on t1_0.id=m1_0.team_id

Member m을 프로젝션으로 설정하면 Member내의 모든 칼럼들을 자동으로 읽어오는 것을 볼 수 있습니다.

동시에 영속성 컨텍스트에도 자동으로 Member가 등록됩니다. 하지만 Member내에 지연로딩을 설정한 team은 따로 불러오지 않았습니다.

1
2
3
4
// 지연로딩이 설정된 team 멤버
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;

위와같이 team은 지연로딩이 설정되어있으므로 team을 사용할 때, team을 불러오는 쿼리가 나갑니다.

1
2
3
4
5
6
// join jpql 수행
Member result = em.createQuery("SELECT m from Member m inner join m.team t", Member.class)
                    .getSingleResult();

// 이때 team을 불러오는 쿼리가 나간다.
System.out.println("team name = " + result.getTeam().getName());

team을 불러오기위해 아래와 같은 쿼리가 나갑니다.

1
2
3
4
5
6
7
select
    t1_0.id,
    t1_0.name 
from
    Team t1_0 
where
    t1_0.id=?

그래서 결국 member의 team을 얻어오기 위해 2번의 쿼리를 수행한 셈입니다.

Fetch Join

fetch join은 SQL에는 없는 문법입니다. JPQL에서 제공하는 join입니다. 이 join은 위의 일반 join과 달리 Memeber뿐만 아니라 team까지도 영속성 컨텍스트에 저장합니다.

1
SELECT m from Member m join fetch m.team

문법은 위와 같이 join뒤에 fetch만 붙여주면 됩니다. 주의할점은 표준 jpql에서는 join의 대상이 되는 m.team에 별칭을 붙이면 안됩니다는 점입니다.

하이버네이트는 신기하게도 m.team에 별칭을 붙일 수 있지만, 향후 영속성 컨텍스트의 무결성을 보장할 수 없게 될 수도 있습니다. 그래서 사용하지 않는 게 좋습니다.

아무튼 위의 jpql을 실행해보면 아래와 같이 sql이 나갑니다.

1
2
3
4
5
6
7
8
9
10
select
    m1_0.id,
    m1_0.name,
    t1_0.id,
    t1_0.name 
from
    Member m1_0 
join
    Team t1_0 
        on t1_0.id=m1_0.team_id

일반 join과 달리 team의 정보까지도 가져오는 모습입니다. 실제로 영속성 컨텍스트에는 member뿐만 아니라 member와 연관된 team까지도 저장됩니다.

일반 Join으로 Fetch Join 흉내내기

여기서 드는 한 가지 의문이 있습니다. 일반 join할 때 프로젝션 부분에 member뿐만 아니라 team까지도 넣어주면 완전히 동일한 SQL이 실행되지 않을까?

만약 동일한 sql이 수행된다면, 결국 fetch join과 동일한게 아닌가?

1
2
3
4
// m과 t를 동시에 가져오기 위해 간편하게 tuple을 사용했습니다.
Tuple result = em.createQuery("SELECT m, t from Member m inner join m.team t", Tuple.class)
                    .getSingleResult();
System.out.println("team name = " + ((Member)result.get(0)).getTeam().getName());

위와 같이 join을 구성하였습니다. 프로젝션이 2개 이상이므로 tuple을 사용했습니다. Tuple도 가급적 사용을 지양하는 편이지만, 가벼운 실험이므로 그냥 사용했습니다.

수행된 SQL을 확인해보면 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
 select
    m1_0.id,
    m1_0.name,
    m1_0.team_id,
    t1_0.id,
    t1_0.name 
from
    Member m1_0 
join
    Team t1_0 
        on t1_0.id=m1_0.team_id

fetch join과 거의 비슷한 sql입니다. 다른 점은 member.team_id를 추가로 쿼리한 점입니다.

결과는 예상대로 fetch join을 사용했을 때와 같이 member와 team 둘 다 사용하고 있음에도 쿼리는 1번만 나갔습니다.

결론

물론 fetch join은 일반 Join으로 흉내낸 것과는 몇 가지 다른점이 있었습니다.

  • fetch join이 더 적은 칼럼을 조회합니다. 일반 join을 사용한 방법에서 추가로 member.team_id를 조회합니다. 물론 이러한 미묘한 차이는 성능에 큰 영향을 주지 않을 것 같습니다.
  • fetch join이 쿼리 결과를 더 간편하게 줍니다. fetch join은 member로 바로 받아서 team까지 조회해볼 수 있지만, 일반 join은 DTO를 따로 만들어 받거나, tuple등의 방법으로 받아야 합니다. 일반 join이 더 번거롭습니다.
  • fetch join의 JPQL이 더 간결하고 직관적입니다. 일반 join으로 fetch join을 흉내내보았지만, fetch join에 비해 오히려 더 길어지고 member를 기준으로 조회한다는 느낌이 덜 듭니다.

결론은 일반 join으로 연관된 entity까지 포함시킨 프로젝션을 조회하는 것 보다는, fetch join을 사용하는 게 더 낫다는 결론입니다.

This post is licensed under CC BY 4.0 by the author.