본문 바로가기
데이터베이스/JPA

JPA JPQL 페치 조인(fetch join)

by jeonghaemin 2021. 2. 21.
728x90

JPA에서 일반적인 조인을 사용하면 연관된 엔티티는 함께 조회하지 않는다. 그렇기 때문에 N+1 문제가 발생할 수 있는데, 페치 조인을 사용하면 연관된 엔티티를 한 번의 쿼리로 모두 가져올 수 있다.

참고로 N+1 문제란 하나의 쿼리를 날리는데 조회되는 결과의 개수만큼의 쿼리가 추가적으로 나가는 것을 말한다.

Member 엔티티와 Team 엔티티가 1:N 단방향 연관 관계를 가지는 상황을 예로 들어보자.

Member 엔티티

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String name, Team team) {
        this.name = name;
        this.team = team;
    }

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", team=" + team +
                '}';
    }
}

Team 엔티티

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    //컬렉션 페치 조인 설명을 위해 양방향 연관 관계 설정
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

일반적인 조인을 사용

만약 다음과 같이 Member1은 Team1에 소속, Member2는 Team2에 소속되어 있는 상황에서 Member에 Team을 조인하여 조회한다면?

public class App
{
    public static void main( String[] args ) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();

        tx.begin();

        try {
            Team team1 = new Team("Team1");
            em.persist(team1);

            Team team2 = new Team("Team2");
            em.persist(team2);

            Member member1 = new Member("member1", team1);
            em.persist(member1);

            Member member2 = new Member("member2", team2);
            em.persist(member2);

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

            //일반적인 inner join.
            List<Member> resultList = em.createQuery("select m from Member m join m.team",                    Member.class).getResultList();

            for (Member member : resultList) {
                System.out.println(member);
            }

            tx.commit();
        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

실제 JPA에서 생성하는 쿼리문을 보면 Member를 조회하는 쿼리 1개를 날렸을 뿐인데, Team1과 Team2를 조회하는 쿼리 2개가 추가로 나가게 되는 1+N 문제가 발생하게 된다.

즉, 만약 Member를 조회하는데 연관된 팀이 1000개라면? 1000개의 추가 쿼리가 나가게 되는 것이다.

Hibernate: 
      select
        member0_.id as id1_0_,
        member0_.name as name2_0_,
        member0_.team_id as team_id3_0_ 
    from
        Member member0_ 
    inner join
       Team team1_ 
    on member0_.team_id=team1_.team_id
Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.team_id=?

Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.team_id=?

페치 조인을 사용하여 1+N 문제를 해결

페치 조인을 사용하면 쿼리 1개로 연관된 엔티티까지 한 번에 가져올 수 있다.

사용 방법은 join 키워드를 사용하는 곳에 join fetch 키워드를 사용하면 된다.

//페치 조인을 사용하여 조회.
List<Member> resultList = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();

페치 조인을 사용하여 실행한 결과 쿼리 1개로 한번에 조회된 것을 볼 수 있다.

Hibernate: 
    /* select m from Member join fetch m.team */ 
        select
       member0_.id as id1_0_0_,
       team1_.team_id as team_id1_1_1_,
       member0_.name as name2_0_0_,
       member0_.team_id as team_id3_0_0_,
         team1_.name as name2_1_1_ 
     from
         Member member0_ 
       inner join
       Team team1_ 
     on member0_.team_id=team1_.team_id

컬렉션 페치 조인

이번엔 1:N 관계 Team에서 Member를 페치 조인해보도록 하자.

JPQL : select  t from Team t join fetch t.members

SQL : 
        select
            team0_.team_id as team_id1_1_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_1_0_,
            members1_.name as name2_0_1_,
            members1_.team_id as team_id3_0_1_,
            members1_.team_id as team_id3_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
         on team0_.team_id=members1_.team_id

DB 입장에서는 페치 조인도 결국 inner join을 사용하는 것이기 때문에 1:N 관계를 페치 조인하게 되면 중복이 발생하게 된다.

이 경우 distinct 키워드를 사용하여 중복을 제거할 수 있다. JPQL에서 disticnt를 사용하면 SQL에서의 disticnt 기능뿐만 아니라, 엔티티의 중복까지 제거해준다.

select distinct t from Team t join fetch t.members

페치 조인 주의사항

  • 페치 조인 대상에는 별칭을 줄 수없다. 하이버네이트 구현체에서는 사용이 가능하지만 가급적 사용하지 않는 것이 좋다.
  • 2개 이상의 컬렉션을 한 번에 페치 조인할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.

컬렉션을 페치 조인하면 DB 입장에서는 중복된 결과를 얻기 때문에 정확한 페이징을 하기 어렵다. 그래서 컬렉션을 페치 조인하게 되면 다음과 같은 경고를 출력하고 JPA는 모든 행을 메모리로 올려서 직접 페이징을 한다.

WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

전체 행의 개수가 10000개라면, 10000개를 한 번에 메모리에 올려서 페이징을 처리하는 것이다.

참고

댓글