[인프런 김영한] JPA - 페치 조인(fetch join), 컬렉션 페치조인

2021. 2. 27. 12:22프로그래밍 언어/Spring Framework

[인프런 김영한] JPA - 페치 조인(fetch join),  컬렉션 페치조인


해당 글은 인프런 김영한강사님의 영상을 보고 정리한 글입니다.

김영한 인프런 : www.inflearn.com/users/@yh

 

인프런 - 김영한의 강의들을 만나보세요.

우아한형제들 개발 팀장 (전: 카카오, SK플래닛) 저서: 자바 ORM 표준 JPA 프로그래밍

www.inflearn.com


실무에서 매우 자주 사용되며, 매우 중요하다

 

▣ 페치조인

* SQL 조인 종류가 X

* JPQ에서 성능 최적화를 위해 제공하는 기능

* 연관된 Entity나 컬렉션을 SQL 한번에 함께 조회하는 기능

* join fetch 명령어 사용

 

 

 

 

▣ Entity 페치 조인

회원을 조회하면서 연관된 팀도 함께 조회(SQL 한번에)

[JPQL]

SELECT *

FROM Member m

join fetch m.team

 

[SQL]

SELECT M.*, T.*

FROM Member m

inner join team t

on m.team_id = t.id

 

 

>> 페치조인 적용 전

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

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

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

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

// 실행문
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as team_id4_0_,
            member0_.username as username3_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=?
member = 회원1, 팀A
member = 회원2, 팀A
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
member = 회원3, 팀B

| 회원1을 조회할 때 실제 SELECT SQL문이 실행이 되었다.

회원2같은 경우에는 이미 1차 캐시에 저장되어 있기 때문에 SQL이 실행되는게 아닌, 1차캐시(영속성 컨텍스트)에서 가지고옴.

회원3같은 경우에는 팀A에 없기 때문에(영속성 컨텍스트에 없다.) SELECT 쿼리를 날려서 영속성 컨텍스트에 저장.

 

회원1, 팀A(SQL문)
회원2, 팀A(1차 캐시, 영속성 컨텍스트)
회원3, 팀A(SQL문)

총 쿼리가 3번 나간다

 

 

 

 

 

>> 페치조인 적용 후

// 이전 쿼리
// String query = "select m from Member m";

// 수정 쿼리
String query = "select m from Member m join fetch m.team";

 

// 실행문
Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.id as id1_0_0_,
            team1_.TEAM_ID as team_id1_1_1_,
            member0_.age as age2_0_0_,
            member0_.TEAM_ID as team_id4_0_0_,
            member0_.username as username3_0_0_,
            team1_.name as name2_1_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.TEAM_ID
member = 회원1, 팀A
member = 회원2, 팀A
member = 회원3, 팀B

 

Member의 Team에 Lazy를 적용해도 페치조인이 우선시 된다.

 

 

 

 

 

 


▣ 컬렉션 페치 조인

* 일대다 관계, 컬렉션 페치 조인

 

 

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

 

 

>> 컬렉션 페치조인 주의사항

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

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

String query = "select t from Team t join fetch t.members";

List<Team> resultList = em.createQuery(query, Team.class).getResultList();

for (Team team : resultList) {
  System.out.println("team = " + team.getName() + "| members = " + team.getMembers().size());
  for(Member member : team.getMembers()){
  	System.out.println(" -> member = " + member);
  }
}
        

// 실행문
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch t.members */ select
            team0_.TEAM_ID as team_id1_1_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_1_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as team_id4_0_1_,
            members1_.username as username3_0_1_,
            members1_.TEAM_ID as team_id4_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.TEAM_ID=members1_.TEAM_ID
team = 팀A| members = 2
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}
team = 팀A| members = 2
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}
team = 팀B| members = 1
 -> member = Member{id=5, username='회원3', age=0}

team = 팀A| members = 2
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}
team = 팀A| members = 2    -> 중복
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}
team = 팀B| members = 1
 -> member = Member{id=5, username='회원3', age=0}

 

원하는 데이터는 가지고 오지만 데이터가 뻥튀기가 될 수 있다. 

Team 입장에서는 팀A는 하나이지만 Member입장에서는 회원이 2명이기 떄문에 2줄이 될 수 있다.

만약 MEMBER가 100명 있으면 100번 출력한다.

 

 

 

 

▣ 페치조인과 DISTINCT

* DISTINCT는 중복 제거 명령

* JPQL의 DISTINCT는 2가지 기능을 제공

 - 1. SQL에 DISTINCT를 추가

 - 2. 애플리케이션에서 Entity 중복 제거

 

[JPQL]

SELECT distinct t

FROM Team t

join fetch t.members

where t.name = '팀A'

 

Hibernate: 
    /* select
        DISTINCT t 
    from
        Team t 
    join
        fetch t.members */ select
            distinct team0_.TEAM_ID as team_id1_1_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_1_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as team_id4_0_1_,
            members1_.username as username3_0_1_,
            members1_.TEAM_ID as team_id4_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.TEAM_ID=members1_.TEAM_ID

-> SQL입장에서는 SQL에 DISTINCT를 추가하지만 데이터가 조금 다르므로 SQL결과에서 중복제거 실패

, 이해가 잘 안될수도 있는데 쿼리만으로는 데이터가 줄여지지 않는다.

--> 그래서 JPA에서는 같은 식별자를 가진 Team Entity를 제거

 

 

 

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

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

String query = "select DISTINCT t from Team t join fetch t.members";

List<Team> resultList = em.createQuery(query, Team.class).getResultList();

for (Team team : resultList) {
  System.out.println("team = " + team.getName() + "| members = " + team.getMembers().size());
  for(Member member : team.getMembers()){
  	System.out.println(" -> member = " + member);
  }
}

// 실행
team = 팀A| members = 2
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}
team = 팀B| members = 1
 -> member = Member{id=5, username='회원3', age=0}

 

* 일대다는 중복이 있지만 다대일은 중복이 없다.

 

TEAM -> MEMBER (중복생성)

MEMBER -> TEAM (중복X)

 

 

 

 

[페치 조인과 일반 조인의 차이]

* 일반 조인 실행시 연관된 Entity를 함께 조회하지 않음.

 

[JPQL]

SELECT t

FROM Team t

join t.members

where t.name = '팀A'

 

[SQL]

SELECT T.* 

FROM TEAM T

INNER JOIN MEMBER M

ON T.ID = M.TEAM_ID

WHERE T.NAME = '팀A'

 

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

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

// 일반조인인
String query = "select t from Team t join t.members";

List<Team> resultList = em.createQuery(query, Team.class).getResultList();

for (Team team : resultList) {
  System.out.println("team = " + team.getName() + "| members = " + team.getMembers().size());
  for(Member member : team.getMembers()){
 	 System.out.println(" -> member = " + member);
  }
}

//실행문
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        t.members */ select
            team0_.TEAM_ID as team_id1_1_,
            team0_.name as name2_1_ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.TEAM_ID=members1_.TEAM_ID
Hibernate: 
    select
        members0_.TEAM_ID as team_id4_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as team_id4_0_1_,
        members0_.username as username3_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀A| members = 2
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}
team = 팀A| members = 2
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}
Hibernate: 
    select
        members0_.TEAM_ID as team_id4_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as team_id4_0_1_,
        members0_.username as username3_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀B| members = 1
 -> member = Member{id=5, username='회원3', age=0}

 

 

 

▣ 일반조인문 특징

Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        t.members */ select
            team0_.TEAM_ID as team_id1_1_,
            team0_.name as name2_1_ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.TEAM_ID=members1_.TEAM_ID

| select절에서 Team만 가지고 온다.

일반조인할 때 연관된 Entity를 함께 조회하지 않기 떄문.

중요! : 해당경우도 데이터 뻥튀기가 된다.

 

 

또한 지연로딩으로 인해 Member의 데이터가 로딩이 안되어서

실제 사용할 때 select문이 출력된다.

 

페치조인 , 일반조인 차이점

일반조인 : JPQL은 결과를 반환할 때 연관관계 고려 X, SELECT절에 지정한 Entity만 조회할 뿐

페치조인 : 페치조인을 사용할때만 연관된 Entity도 함께 조회(즉시로딩), 페치조인은 객체 그래프를 SQL 한번에 조회하는 개념

 

 

 

 

 


▣ 페치 조인의 특징과 한계

* 페치 조인 대상에는 별칭을 줄 수 없다.

 - 하이버네이트는 가능하지만 가급적 사용을 권장하지 않는다.

* 둘 이상의 컬렉션은 페치조인을 할 수 없다.

 

* 컬렉션을 페치조인하면 페이징 API를 사용할 수 없다.

 - 일대일, 다대일같은 단일 값 연관 필드들은 페치조인해도 페이징 가능

 - 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험하다)

String query = "select t from Team t join fetch  t.members m";

List<Team> resultList = em.createQuery(query, Team.class)
  .setFirstResult(0)
  .setMaxResults(1)
  .getResultList();
  
// 실행문
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch  t.members m */ select
            team0_.TEAM_ID as team_id1_1_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_1_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as team_id4_0_1_,
            members1_.username as username3_0_1_,
            members1_.TEAM_ID as team_id4_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.TEAM_ID=members1_.TEAM_ID
team = 팀A| members = 2
 -> member = Member{id=3, username='회원1', age=0}
 -> member = Member{id=4, username='회원2', age=0}

| 'WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!' 경고 로그.

또한 페이징 쿼리(ex, limit 등)가 없으며 데이터를 다 불러온다.

 

* 연관된 Entity들을 SQL 한번으로 조회 - 성능 최적화

* Entity에 직접 적용하는 글로벌 로딩 전략보다 우선시됨.

 - 실무에서 글로벌 로딩 전략은 모두 지연로딩

* 최적화가 필요한 곳은 페치조인 적용

* 페치조인은 객체 그래프를 유지할 때 사용하면 효과적

* 여러 테이블을 Join해서 Entity가 가진 모양이 아닌, 전혀 다른 결과를 내야 하면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 변환하는 것이 효과적