Programming/JPA

JPA - 페치 조인 (1)

잇(IT) 2023. 7. 19. 13:56

- 페치 조인(fetch join)

1. SQL 조인 종류가 아니다.

2. JPQL에서 성능 최적화를 위해 제공하는 기능이다.

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

 

- 엔티티 페치 조인

1. Member 엔티티를 조회하면서 연관된 팀 Team 엔티티도 함께 조회가 가능하다. (SQL 한 번에)

//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

- 현재 위와 같이 회원 2명이 팀 하나에 같이 소속되어 있는 상황이다.

- 회원 1,2,3과 팀 A,B를 조인하면 위와 같이 3개의 튜플이 생성된다.

- 기존의 조인을 확인해보면 Team의 경우 LAZY 즉, 지연로딩으로 설정되어 있기 떄문에 Team 조회가 필요하기 전까지 쿼리가 실행되지 않고 프록시만 생성되어 있는 상태다.


* (flush와 clear를 통해 DB에 값은 저장하였지만 영속성 컨텍스트는 비워둔 상태이다.)

String query = "select m From Member m";

            List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();

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

- 기존의 Member를 조회하여 Team 정보를 가져올 때는 지연 로딩에 의해 member가 먼저 select 된 후 team을 select 하는 것을 볼 수 있다.

- 또한, 영속 엔티티에 없는 또 다른 team 엔티티가 조회 될 경우 조회하는 시점에 team 엔티티가 select 쿼리를 보내는 것을 확인 할 수 있다.


- fetch 조인 사용

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

//            String query = "select m From Member m";

            List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();

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

- fetch join을 사용하면 호출 시점에 Team 엔티티를 조인하여 함께 조회한다.

- 한번에 3개의 튜플이 함께 조회된다.


String query = "select t From Team t";

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

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

- JPQL에서 Team을 조회했기 때문에 select로 team을 우선 조회한다.

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

1. 현재 팀 A,B가 영속성 컨텍스트에 저장되어 있는 상태이다.

2. for문에 의해 첫번째 팀 A에 대한 이름과 크기를 가져온다. 팀 A에는 회원 1,2 두명이 있기 때문에 List의 크기 2가 반환된다.

3. 중첩 for문을 확인해보면 Team A에 대한 member 객체를 조회하는 것이기 때문에 팀 A에 속한 member에 대한 정보만 표시한다.

1. 마찬가지로 select문이 또 실행된 이유는 현재 현재 팀 B에 속한 member에 대한 엔티티 정보가 없기 때문에 또 한 번 호출된 것이다.


- 컬렉션 페치 조인

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

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

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

- 다음은 Team 엔티티에 속한 members 컬렉션을 fetch join 하는 코드이다.

- fetch join의 경우 Team 엔티티를 조회함과 동시에 Member 엔티티도 join으로 함께 조회하기 때문에 한번에 결과값이 전부 출력되는 것을 확인 할 수 있다.


            String query = "select t From Team t join t.members";

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

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

 

- 일반 조인의 경우 select가 데이터가 필요한 member에 따라 계속해서 select를 호출하는 것을 확인 할 수 있다.


- 위에서 1:N 일대다 조인의 데이터를 조회했을 때 같은 데이터가 2번 나오는 것을 확인 할 수 있다.

- JPA는 DB에서 값을 가져오기 전까지는 DB에 어떤 데이터가 들어있는지 알 수 없기 때문에 쿼리를 날려 DB가 보내주는 정보를 그대로 사용할 수 밖에 없다.

- 현재 팀 A에는 2명의 회원이 존재하기 때문에 join을 하게되면 최종적으로 두 줄의 데이터가 생성되고 해당 데이터를 DB로 부터 받는다.

- 결과적으로 위와 같이 JOIN의 결과로 팀A의 회원 2명인 두 줄의 데이터가 생성된다.

 String query = "select t From Team t join fetch t.members";
            System.out.println("===========================================111");
            List<Team> result = em.createQuery(query, Team.class)
                    .getResultList();
            System.out.println("===========================================222");
            for (Team team : result) {
                System.out.println("===========================================333");


                System.out.println("team = " + team.getName() + "| members = " + team.getMembers().size());
                System.out.println("===========================================444");

                for (Member member : team.getMembers()) {
                    System.out.println("-> member = " + member);
                }
            }

1. 위의 조인한 결과로 두 줄의 데이터가 나온 것을 기반으로 첫번재 줄의 팀A를 호출 하였을 때 영속 컨텍스트에 팀A에 대한 데이터가 저장된다.

2. 두번째 줄 팀A를 조회할 때는 이미 영속 컨텍스트에 같은 주소값의 데이터가 있기 때문에 별도로 가져오지 않고, 같은 주소값을 사용하여 팀A를 조회한다.

3. for문에서 result의 값을 가져올 때 DB로부터 가져온 테이블의 행은 2개기 때문에 같은 값이더라도 2번을 호출해야 한다.

4. 결과적으로 같은 팀A라도 회원 1,2 중 어떤 값을 가져오기 위한 것인지 JPA는 알지 못하기 때문에 같은 결과 값을 두번 호출하게 된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처 : 인프런 - 김영한 (실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화)

728x90