fetch join

Posted by sJun on November 09, 2020 ·

fetch join이란?

  1. SQL의 조인과 아무런 관련이 없다.
  2. JPQL에서 성능 최적화를 위해 사용.
  3. 연관된 엔티티와 컬렉션을 한 번에 조회할 수 있는 기능.

Member 내부 Team 객체가 존재할 때

select m from Member m join jetch m.team;

으로 한번에 조회가 가능.

사용하는 이유?

package com.practice.jpql_sample;

import com.practice.jpql_sample.domain.Member;
import com.practice.jpql_sample.domain.Team;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceContext;
import java.util.List;

@SpringBootTest
@Transactional
public class TestCode {

    @PersistenceContext
    EntityManager em;


    @Test
    @Rollback(value = false)
    void o(){
        Team team1 = new Team();
        team1.setName("teamA");

        Team team2 = new Team();
        team2.setName("teamB");

        Member member1 = new Member();
        member1.setUsername("kim");
        member1.setTeam(team1);

        Member member2 = new Member();
        member2.setUsername("park");
        member2.setTeam(team2);

        Member member3 = new Member();
        member3.setUsername("Lee");
        member3.setTeam(team2);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);

        em.persist(team1);
        em.persist(team2);

        em.flush();
        em.clear();

        String query = "select m From Member m";
        List<Member> findMembers = em.createQuery(query, Member.class)
                .getResultList();

        for (Member findMember : findMembers) {
            System.out.println("findMember = " + findMember.getUsername() + " | " + findMember.getTeam().getName());
        }
    }
}

다음과 같은 코드가 있다.

member1 / member2 / member3 은 각각 team1이나 team2에 소속되는 테스트 코드고 그걸 조회한다.

이제 쿼리문이 총 몇 개 나가는지 살펴보자

Hibernate: 
    select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id5_0_,
        member0_.type as type3_0_,
        member0_.username as username4_0_ 
    from
        member member0_
Hibernate: 
    select
        team0_.team_id as team_id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
findMember = kim | teamA
Hibernate: 
    select
        team0_.team_id as team_id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
findMember = park | teamB
findMember = Lee | teamB

총 3개가 나간다.

for문 첫번째에서 Member에 대한 전체적인 순회를 해야되므로 DB에 있는 모든 Member 테이블을 영속성 컨텍스트에 넣는다.

그 후 team의 이름을 찾기 위해서 member1과 연관된 팀에 해당하는 테이블을 가져온다. (참고로 여기서 Member와 Team의 연관관계 속 fetch전략은 LAZY다.)

왜? em.flush()와 em.clear()로 영속성 컨텍스트를 완전히 비워버렸기 때문에.

member2도 똑같다. 여기서 member는 영속성 컨텍스트에 담겼지만 member2에 연관된 teamB는 없다.

teamB를 또 불러온다.

“Lee” 는 이미 teamB와 member가 영속성 컨텍스트에 있기때문에 쿼리문이 따로 필요하지 않다.

그럼 한 번에 불러오는 EAGER 전략은 어떤가?

Hibernate: 
    select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id5_0_,
        member0_.type as type3_0_,
        member0_.username as username4_0_ 
    from
        member member0_
Hibernate: 
    select
        team0_.team_id as team_id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
Hibernate: 
    select
        team0_.team_id as team_id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
findMember = kim | teamA
findMember = park | teamB
findMember = Lee | teamB

순서만 바뀌었다 뿐이지 쿼리문 3번 쓰는건 똑같다. EAGER는 순수 member만 조회해도 연관관계에 있는 team을 무조건 전부 가져오기 때문에 LAZY 보다 더 좋지 않다.

자 이제 join fetch를 써보자

String query = "select m From Member m join fetch m.team";

뒤에 join fetch m.team만 붙이면 된다.

Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        team1_.team_id as team_id1_3_1_,
        member0_.age as age2_0_0_,
        member0_.team_id as team_id5_0_0_,
        member0_.type as type3_0_0_,
        member0_.username as username4_0_0_,
        team1_.name as name2_3_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.team_id
findMember = kim | teamA
findMember = park | teamB
findMember = Lee | teamB

놀랍게도 쿼리가 3개나 나가던게 join fetch로 한 번에 조회가 끝난다.

join fetch 키워드를 사용하자 hibernate가 inner join을 사용하는 것을 볼 수 있다.

여기서 team은 더이상 프록시가 아니다. inner join으로 모든 데이터를 한 번에 받아왔기 때문에 원본 객체이다.

이 예제는 단순해서 고작 쿼리 3개에서 하나로 줄였을 뿐이지만 만약에 팀이 100개가 있고 멤버의 팀이 모두 다르다면?

조회만 하려고 했는데 쿼리문만 101개가 나갈 것이다.

[(기존 멤버 1) + (팀 100). 보통 N + 1 문제라고 한다.]

그래서 조회 성능의 최적화를 위해서라면 fetch join은 필수적이다.