본문 바로가기
자바/JPA

[JPA/Java] JPQL fetch join (패치 조인)

by drCode 2023. 5. 10.
728x90
반응형

JPQL에는 fetch 조인이라는게 있는데 이게 실무에서 정말정말 중요하다고 한다.

 

패치 조인(fetch)은

SQL 조인의 종류가 아닌데, JPQL에서 성능 최적화를 위해서 제공하는 기능이다.

연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능을 제공한다.

"join fetch" 와 같이 명령어를 사용한다.

패치 조인은 LEFT JOIN FETCH [OUTER 생략 가능] / INNER JOIN FETCH 와 같이 조인 경로를 쓰면 된다.

 

엔티티 패치 조인은

회원을 조회하면서 연관된 팀도 함께 조회한다. (SQL을 날리는 트랜잭션 한 번에)

SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT 된다.

 

[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

 

예시)

회원1,2는 팀A, 회원3은 팀B에 배치한다.
MEMBER 와 TEAM을 조인한 결과
이를 fetch join 한 결과를 도식화하면 위와 같다.

 

※ 엔티티 패치조인 케이스 (다대일)

 

패치 조인을 적용하지 않고 모든 멤버들을 조회할 경우는 아래와 같다.

jpql = "select m from Member m";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();

for (Member m : result) {
    System.out.println("m = " + m.getUsername() + ", " + m.getTeam().getName());
}

 

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_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_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
m = 회원1, teamA
m = 회원2, teamA
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
m = 회원3, teamB

 

쿼리는 총 3번 나갔다.

① Member 조회 ②  Team 조회 ③ Member 조회

 

Member 별 데이터 조회를 살펴보면

(1) 회원1, 팀A(SQL)
(2) 회원2, 팀A(영속성 1차 캐시)
(3) 회원3, 팀B(SQL)

 

팀이 다 달랐다면 이 이상의 조회 쿼리가 나갔을 수 있다.

 

회원 100명을 조회한다 할 때, → N + 1 (첫번째 쿼리)  ==> N+1 문제가 발생한다.

 

이는 성능을 떨어뜨릴 가능성이 높다.

 

Fetch Join 을 사용하면 조회 쿼리가 한번에 나간다.

// 패치 조인을 쓰면 N+1 문제가 해결됌
jpql = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();

for (Member m : result) {
    System.out.println("m = " + m.getUsername() + ", " + m.getTeam().getName());
}
Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.id as id1_0_0_,
            team1_.id as 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_.id
m = 회원1, teamA
m = 회원2, teamA
m = 회원3, teamB

Fetch Join을 사용하면 조회 성능이 올라간다.

 

 

※ 컬렉션 패치 조인 케이스 (일대다)

→ 데이터가 뻥튀기 될 가능성이 있음(중복건 조회 가능성 높음)

 

[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'

 

팀A는 2명의 회원을 가지고, 팀B는 1명의 회원을 가진다

 

TEAM 과 MEMBER를 조인한 결과
fetch join 도식화

jpql = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class).getResultList();

for (Team t : result) {
    System.out.print("t = " + t.getName() + ", " + t.getMembers().size());
    for (Member m : t.getMembers()) {
        System.out.print(" - " + m.getUsername());
    }
    System.out.println();
}
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch t.members */ select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
t = teamA, 2 - 회원1 - 회원2
t = teamA, 2 - 회원1 - 회원2
t = teamB, 1 - 회원3

teamA의 회원들이 중복되어 출력되었다.

 

이 현상은 distinct를 추가하면 된다

jpql = "select distinct t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class).getResultList();

for (Team t : result) {
    System.out.print("t = " + t.getName() + ", " + t.getMembers().size());
    for (Member m : t.getMembers()) {
        System.out.print(" - " + m.getUsername());
    }
    System.out.println();
}
Hibernate: 
    /* select
        distinct t 
    from
        Team t 
    join
        fetch t.members */ select
            distinct team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
t = teamA, 2 - 회원1 - 회원2
t = teamB, 1 - 회원3

SQL에서의 DISTINCT는 중복된 결과를 제거하는 역할을 하는데

 

JPQL 에서의 DISTINCT는 실행될 쿼리 SQL에 DISTINCT를 추가하고, 애플리케이션에서 엔티티 중복을 제거한다.

 

DISTINCT를 추가한 결과로 같은 식별자를 가진 Team 엔티티를 제거한다.

 

중복된 엔티티를 제거한다,

[DISTINCT 추가시 결과]

teamname = 팀A, team = Team@0x100

-> username = 회원1, member = Member@0x200

-> username = 회원2, member = Member@0x300

 

패치 조인과 일반 조인의 차이가 존재한다.

 

일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다.

 

[JPQL]

select t

from Team t join t.members m

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 한번에 조회하는 개념이다.

 

패치 조인은 연관 엔티티를 함께 조회한다.

예시)

 

[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'

 

 

728x90
반응형

댓글