본문 바로가기

개인적으로 공부한 것을 정리해 놓은 블로그입니다 틀린 것이 있으면 댓글 부탁 드립니다!


JPA

JPA 공부 18 - JPQL . 7 페치조인

반응형

 

fetch 조인?

 

SQL의 조인 종류가 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다 , 연관된 엔티티나 컬렉션을 SQL 한 번에 함께  조회하는 기능이다 join fetch 명령어를 통해 사용한다 . 

 

 

1. 엔티티 fetch join 

 

회원을 조회하면서 연관된 팀도 함께 조회 하고싶은 경우를 예로들면 

 

            1.
            
            // 만약 연관관계에 있는 Team이 모두 다르다면 쿼리가 n+1번으로 나간다.
            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() +" , "+"team="+member.getTeam().getName());
                //회원1 , 팀 1(sql)
                //회원2 , 팀 2(1차 캐시)
                //회원3 , 팀 3(sql)
            }
			
            2.
            
            //fetch join의 경우 해당 연관관계가 eagar로 동작한다 . 
            //진짜 데이터를 가져온다
            String query2 = "select m from Member m join fetch m.team";
            List<Member> result2 = em.createQuery(query2, Member.class)
                    .getResultList();
            for (Member member : result2) {
                System.out.println("member=" +member.getUsername() +" , "+"team="+member.getTeam().getName());
            }

 

1번의 경우 쿼리 3방  ,  2번의 경우 1방

 

연관관계를 지연로딩으로 했어도  , fetch조인이 우선된다. 

 

 

2.컬렉션 fetch join 

 

            //반대편 일다다에서 컬랙션조회를 할떄
            //컬렉션 페치조인의 주의점
            //일대다 관계에서의 DB 레코드가 뻥튀기 된다 .
            // team2 에 2명이 있으니 결과도 2개가 나온다 . team2에 관한 내용도 멤버의 수만큼 나온다.
           String query3 = "select t from Team t join fetch t.members";
            List<Team> result3 = em.createQuery(query3, Team.class)
                    .getResultList();
            for (Team team : result3) {
                System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
            }

            //위의 문제해결을 위해  JPQL이 distict 명령어를 제공한다 .
            //db의 distinct와는 조금 다르다 .
            //JPQL의 distinct는 sql에 distinct를 추가해서 날려주고
            //리턴 되는 엔티티의 중복을 한번더 체크해준다.
            String query4 = "select distinct t from Team t join fetch t.members";
            List<Team> result4 = em.createQuery(query4, Team.class)
                    .getResultList();
            for (Team team : result4) {
                System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
            }

 

JPQL의 DISTINCT는 같은 식별자를 가진 Team 엔티티를 제거한다 .

 

 

일반조인과 페치조인 차이 

 

일반 조인은 조인은하지만 데이터는 가져오지 않는다 . 페치 조인은 조인대상의 데이터까지 모두가져온다 . 

 

 

            System.out.println("==============일반조인=============");
            //일반조인
            String query5 = "select distinct t from Team t join t.members";
            List<Team> result5 = em.createQuery(query5, Team.class)
                    .getResultList();
            for (Team team : result5) {
                System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
            }

            System.out.println("==============페치조인=============");
            //페치조인
            String query6 = "select distinct t from Team t join fetch t.members";
            List<Team> result6 = em.createQuery(query6, Team.class)
                    .getResultList();
            for (Team team : result5) {
                System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
            }

일반조인 쿼리

일반 조인의경우 조인하여 멤버를 가져오지만 멤버의 데이터는 가져오지 않는다 . 지연로딩이 발생하여 루프를 돌며 member의 이름을 출력 할때마다 쿼리가나간다 .

 

페치조인쿼리

반면 페치조인은  조인을하면서 연관관계 맺은 데이터를 가져온다 . (Eagar처럼 동작한다)

N+1 문제는 페치조인으로 왠만하면 해결이 가능하다. 

 

 

페치 조인의 특징과 한계

-  페치 조인 대상에는 별칭을 줄 수 없다 .  (하이버네이트에서는 가능하나 가급적 사용하지 않는게 좋다 )

String query = "select  t from Team t join fetch t.members as tm"; //주석을 주면 안된다.
            List<Team> result = em.createQuery(query, Team.class)
                    .getResultList();
            for (Team team : result) {
                System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
            }

 

기본적으로 연결된 것을 모두 끌어오는 것이기 떄문에 페치조인 되는 대상에는 별칭을 주지 않는 것이 관례이다.
t.members 에서 필터링을 해서 원하는 만큼만 가져오고싶다면? 아예 팀이아닌 멤버에서 조회를 해야한다.

 

- 페이징 API를 사용할 수 없다.

 

컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다 일대다 관계에서  컬렉션을 조회하면 데이터가 뻥튀기 되는데 페이징을 해버리면 데이터가 맞지 않아 문제가 생길 수 있다 .

 

이를 이해하기 위해선 왜 일대다 관계에서 데이터가 뻥튀기 되는지를 이해하는 것이 중요하다.

일대다 관계에서 다쪽은 1의 고유키를 외래키로 공유한다. 

ex) 팀 2에 멤버가 3명이면 3명은 모두 같은 팀의 아이디를 외래키로 갖고 있다. 

 만약 팀을 기준으로 멤버를 탐색한다면 ? SQL은 아래와 같을 것이다. 

SQL = SELECT * FROM TEAM  t JOIN MEMBER m ON m.team_id = t.id;

조인 조건에 해당하는 멤버는 3명이다  멤버 3명모두 팀의 id를 team_id(외래키) 로 갖고 있다.

이떄문에 team(1) 을 기준으로 멤버(N)를 조인하여 데이터를 가져올 때  다시말해 일대다의 관계에서 

데이터를 조회하면 레코드 수는 N 개로 뻥튀기된다. 

 

 

 해결방법

 

 

            //1. 반대로 접근한다 .
            //멤버로 조회하여 멤버의 팀을가져온다
            //다대일로 연관관계가 바뀌기 때문에 페이징이 가능해진다.
            String query1 = "select  m from Member m  join fetch m.team";
            List<Member> result1 = em.createQuery(query1, Member.class)
                    .setFirstResult(0)
                    .setMaxResults(4)
                    .getResultList();
            for (Member member : result1) {
                System.out.println("team->"+member.getTeam().getName()+", member->"+member.getUsername());
            }

 

첫번쨰 방법은 반대로 접근하는 것이다  멤버를 통해 팀을 가져오고 그 팀에서 멤버를 꺼내는 방식으로 사용한다.

다대일이 되기 때문에 페이징에 문제가 없다 

 

 

String query3 = "select  t from Team t";
            List<Team> result3 = em.createQuery(query3, Team.class)
                    .getResultList();
            System.out.println("result3.size() = " + result3.size());

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

 

두번쨰 방법은 팀으로 일단 받아온 후에 엔티티에 @BatchSize를 주는 것이다 . 

위의 코드를 보자 JPQL은 team 만 가져오도록 되어 있다 이대로만 실행하면 팀을 조회하는 쿼리가 하나 나가고

각 팀안에 멤버를 찾는 쿼리가 레이지 로딩될 때마다 한번씩 나간다 . N+1 문제가 발생하는 것이다. 

 

 

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Team {

   @Id @GeneratedValue
   private Long id;

   private String name;

   @BatchSize(size = 100)
   @OneToMany(mappedBy = "team")
   private List<Member> members;

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

 

team에 batchsize를 줘서 멤버를 찾을때  팀을 in절로 묶어 in절 안에 팀에 해당되는  멤버를 지정한 수 만큼 가져올 수 있다 . 

 

 

 

어노테이션이 아닌 글로벌 셋팅으로도 지정할 수 있다 . 

 

 

모든 것을 페치 조인으로 해결할 수는 없다 , 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다 .

 

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

 

반응형

'JPA' 카테고리의 다른 글

JPA 공부 20 - JPQL . 9 벌크 연산  (0) 2021.06.08
JPA 공부 19 - JPQL . 8 Named 쿼리  (0) 2021.06.08
JPA 공부 17 - JPQL . 6 경로 표현식  (0) 2021.06.07
JPA 공부 16 - JPQL . 4 JOIN  (0) 2021.06.06
JPA 공부 15 - JPQL . 3 페이징  (0) 2021.06.06