본문 바로가기

Spring/JPA

JPA-Join Fetch

패치 조인

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

join fetch를 하면 원래 가져오려는 테이블과 패치 조인한 테이블을 조인한 테이블을 생성하고, 그 테이블을 가져온다.

 예를 들어 DB에 Team 엔티티가 하나 저장되어 있고, Team에는 두개의 Member 엔티티가 컬렉션 형태로 연관되어 있다고 하자.  (Team:Member = 일대다)

"select t from Team t join fetch t.members"로 Team 엔티티와 연관된 Member 컬렉션을 가져오려고 할 때, Team에 연관된 Member가 두개라서 총 두개의 row가 생성된다. 즉 데이터가 뻥튀기 될 수 있다. 

 

package jpql;

public class JpqlMain {
    static Scanner sc = new Scanner(System.in);
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member1 = new Member();
            Member member2 = new Member();
            Member member3 = new Member();
            member1.setUsername("회원1");
            member2.setUsername("회원2");
            member3.setUsername("회원3");

            Team teamA = new Team();
            Team teamB = new Team();
            Team teamC = new Team();
            teamA.setName("팀A");
            teamB.setName("팀B");
            teamC.setName("팀C");

            member1.changeTeam(teamA);
            member2.changeTeam(teamA);
            member3.changeTeam(teamB);
            em.persist(teamA);
            em.persist(teamB);
            em.persist(teamC);

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

            for (Member member : members) {
                //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
                System.out.println(
                        "username = " + member.getUsername() + ", "
                        + "teamName = " + member.getTeam().getName());
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback(); // 오류 발생 시 롤백
            e.printStackTrace(); // 에러 내용 출력
        } finally {
            em.close(); // 종료
        }
        emf.close();
    }
}

출력>>>
username = 회원1, teamName = 팀A
username = 회원2, teamName = 팀A
username = 회원3, teamName = 팀B

위의 쿼리문 처럼 패치 조인으로 Member 뿐만 아니라 연관된 m.team도 한번에 즉시로딩하여 끌고 올 수 있다.

 

N+1 문제

 각 Member 마다 서로 다른 Team N개가 연관되어 있다고 가정하자. (단, 이 경우 Member와 Team은 일대다 관계이면 fetch type은 Lazy이다.) 그 경우 Member를 find 문으로 조회해 올 때, 연관된 Team을 가져오게 된다.  이때 문제는 이들이 지연 로딩 관계이기 때문에 Team의 필드에 접근 하는 경우 그제서야 Team에 대한 select 쿼리가 나가게 된다.  

 

 정리하면 Member가 100개가 있고 각 Member마다 서로 다른 Team이 연관되어 있다하자. 처음에 Member를 조회하면 Team은 지연로딩이므로 Team에 대한 select 쿼리문이 나가지 않는다. 그러다 Team의 필드에 접근시 지연로딩으로 Team에 대한 select 문이 나가게된다. 이때 Team이 100개이므로 최대 100개의 Team에 대한 select 쿼리가 발생할 수 있다.

  • 1 : Member에 대한 select 문
  • N : Team에 대한 select 문

이처럼 지연로딩에 의해서 굉장히 많은 쿼리가 발생할 수 있는데 이를 1+N 문제라고 한다. 이를 join fetch를 통한 즉시로딩으로 해결한다.

 

컬렉션 패치 조인

package jpql;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;

public class JpqlMain {
    static Scanner sc = new Scanner(System.in);
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member1 = new Member();
            Member member2 = new Member();
            Member member3 = new Member();
            Member member4 = new Member();
            member1.setUsername("회원1");
            member2.setUsername("회원2");
            member3.setUsername("회원3");
            member4.setUsername("회원4");
            em.persist(member4);

            Team teamA = new Team();
            Team teamB = new Team();
            Team teamC = new Team();
            teamA.setName("팀A");
            teamB.setName("팀B");
            teamC.setName("팀C");

            member1.changeTeam(teamA);
            member2.changeTeam(teamA);
            member3.changeTeam(teamB);
            em.persist(teamA);
            em.persist(teamB);
            em.persist(teamC);

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

            for (Team team : teams) {
                //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
                System.out.println("teamName = " + team.getName() + " team.members.size= "+team.getMembers().size());

            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback(); // 오류 발생 시 롤백
            e.printStackTrace(); // 에러 내용 출력
        } finally {
            em.close(); // 종료
        }
        emf.close();
    }
}

출력>>>
teamName = 팀A team.members.size= 2
teamName = 팀A team.members.size= 2
teamName = 팀B team.members.size= 1

 출력을 보자. 지금 쿼리문으로 Team을 가져왔는데 팀A, 팀B, 팀C가 출력되는게 아니라 팀A가 두번, 팀B가 출력되었다.

현재 팀과 멤버의 연관관계는 다음과 같다.

 

테이블의 모습
빨간색이 출력 결과

왜 TEAM 테이블을 조회했는데 TEAM 테이블이 출력되는게 아니라 teamA 두번, teamB 한번이 출력되나면 jpql의

"select t from Team t join fetch t.members"는 Team 중 member가 존재하는 놈들만 조회를 하기에 위의 빨간색 쳐놓은  내용이 조회되기 때문이다.

 

join fetch 사용

 fetchType이 Lazy로 설정되어 있다고 하더라도 join fetch 문을 이용해서 한번에 가져올 수 있기 때문에 기본적으로 모든 xxxToOne은 Lazy로 설정해두고 동시에 여러값을 가져올 필요가 있는 경우에만 선택적으로 join fetch로 값을 한번에 가져오면 된다.

'Spring > JPA' 카테고리의 다른 글

JPA- DISTINCT  (0) 2023.07.10
JPA- 벌크연산  (0) 2023.06.19
JPA- 경로 표현식  (0) 2023.06.18
JPA- 조건식의 사용  (0) 2023.06.18
JPA-타입 표현  (0) 2023.05.20