ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA > Fetch Join , BatchSize
    BackEnd/JPA 2021. 4. 27. 21:18

     

    JPQL 에서 성능 최적화를 위해 제공하는 기능으로, 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회할 수 있다.

    (일반 조인 시 연관엔티티를 함께 조회하지 않고, 사용할 때 새로 select 해온다.)

     

    select m from Member m join fetch m.team
    
    =>
    SELECT M.* , T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID

    즉시 로딩과 유사하게 모든 데이터를 한 번에 Inner Join 으로 가져온다.

    Fetch Join 에서는 어떤 객체 그래프를 한번에 조회할 것인지 명시한 것이 다르다.

     

    2021.04.25 - [BackEnd/자바&스프링] - JPA > 즉시로딩 , 지연로딩 에서 언급한 N +1 문제를 해결할 수 있는 방법 중

    하나가 fetch join 문이다. 조인문으로 한번에 모든 데이터를 가져온다.

    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_ID4_0_0_,
                member0_.userName as userName3_0_0_,
                team1_.name as name2_3_1_ 
            from
                Member member0_ 
            inner join
                Team team1_ 
                    on member0_.TEAM_ID=team1_.id

    결과를 얻기 위한 코드에서도 Member 객체 하나로 Team 에 대한 데이터를 이용할 수 있다.

    for (Member m :resultList){
    	System.out.println("m.getUserName() = " + m.getTeam().getName()); 
    }

    명시적 조인과 fetch join 의 차이

     

    명시적 조회

    SELECT m,t from Member m join m.team t
    Hibernate: 
         select
                member0_.id as id1_0_0_,
                team1_.id as id1_3_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_3_1_ 
            from
                Member member0_ 
            inner join
                Team team1_ 
                    on member0_.TEAM_ID=team1_.id

    실행된 쿼리는 동일해 보인다.

    하지만 결과를 얻기 위해 작성해야 하는 코드는 좀 더 복잡하다.

                Object o = resultList.get(0);
                Object[] result = (Object[]) o;
                Member m = (Member) result[0];
                Team t = (Team) result[1];
                System.out.println("result[0] = " + m.getUserName());
                System.out.println("result[0] = " + m.getTeam().getName());
                System.out.println("result[1] = " + t.getName());

     

    명시적 조인은 결과에 Member,Team 타입 두 개가 있어 결과를 얻기위해 더 번거로운 작업이 필요한데 비해,

    fetch join을 사용하면 Member 객체 하나로 깔끔한 객체 그래프를 받을 수 있다. 

    fetch join의 핵심은 성능 최적화와 객체 그래프 이다.


    일대다 관계에서 데이터 중복

    컬렉션을 이용해 fetch join 을 하면 데이터가 중복 생성된다.

    Member (다) 과  Team (일) 의 관계에서, Team 의 members 컬렉션을 조회하는 경우이다.

    teamA 에 회원1, 회원2 있으면 

    teamA 은 하나인데, 회원쪽 데이터가 2개이니 팀을 조회한 row 결과가 2줄 나오게 된다. 

    아래와 같이 데이터 갯수가 잘못 나오는 것이다. 

    select t from Team t join fetch t.members; //teamA 에 회원2명 teamB에 회원1명
    
    
    select t from Team t; 의 데이터 수는 3개 !
    select t from Team t join fetch t.members; 의 데이터 수는 4개 ! 

    distinct 로 중복을 제거할 수 있는데, sql 레벨에서 distinct 로는 데이터가 제대로 제거되지 않는다.

    그래서 JPQL 의 distinct 는 Member 엔티티의 식별자를 이용해 한 번 더 중복을 제거해준다.

     

    다대일에서는 이런 중복이 일어나지 않는다.

     


    주의 사항 

     

    fetch join 대상에는 별칭을 가급적 사용하지 않도록 해야한다.

    아래와 같이 작성해서 팀과 연관된 회원 5명 중 1명만 불러온다고 가정한다.

    select t from Team t join fetch t.members m where m.username=....; 

    이러면 객체 그래프에서는 team 에 있는 나머지 4명의 회원에 대한 데이터가 누락이 되고 select 해온 t 에서는 

    1명에 대한 데이터 밖에 없기 때문에 잘못된 사용과 잘못된 결과를 낳을 수 있다.

     

    객체 그래프라는 것은 기본적으로 데이터를 모두 조회하는 것이 좋다.

    만약 이렇게 몇개의 데이터만 골라서 가져오고 싶은 경우에는 team 에서 member 를 조회하는 아래 fetch join 방식이 아니라 처음부터 필요한 member 를 조회하도록 짜고, team 의 member 를 모두 조회하는 객체 그래프는 따로 설계한다.

     

    fetch join 을 여러개 연결해서 사용하는 경우에는 별칭을 이용하는 경우가 생길 수 있지만, 그런 상황은 거의 발생하지 않고, JPA 의 설계 목적에 맞게 사용하도록 노력해야한다.

     

     

    둘 이상의 컬렉션은 페치 조인 하면 좋지 않다. 일대다도 데이터가 잘못 나올 수 있는데 이런 다대다 조합은 위험하다.

     

     

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

    일대일, 다대일 같은 단일 값 연관 필드들은 페치조인을 써도 페이징 API 에 문제가 없다.

    하이버네이트는 경로 로그를 남기고 메모리에서 페이징을 하는데 무척 위험하다.

     

    일대다 fetch join 에 페이징 == 사용하면 안됨!

    List<Team> result = 
        em.createQuery("select t from Team t join fetch t.members m ",Team.class)
        .setFirstResult(0)
        .setMaxResult(1)
        .getResultList();

    1. 이런 케이스는 쿼리를 뒤집어 사용할 수 있다.

    List<Member> result = 
        em.createQuery("select m from Member m join fetch m.team t",Member.class)
        .setFirstResult(0)
        .setMaxResult(1)
        .getResultList();

    2. @BatchSize 사용 

     

    - fetch join 을 제거 

    List<Team> result = 
        em.createQuery("select t from Team t",Team.class)
        .setFirstResult(0)
        .setMaxResult(1)
        .getResultList();

    이렇게만 하면 Lazy Loading 으로 team 에서 member 를 꺼내 쓸 때 member 를 조회하는 쿼리가 계속 나간다.(N+1)

     

    public class Team {
        @BatchSize(size=100)
        @OneToMany(mappedBy="team")
        public List<Member> members = new ArrayList<>();
        ...
    }

    지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.
    size는 IN절에 올수있는 최대 인자 개수를 말한다. 

    team 에서 member 를 가져올 때 100개씩 가져온다.

     

    select
            memberlist0_.TEAM_ID as TEAM_ID4_0_1_,
            memberlist0_.id as id1_0_1_,
            memberlist0_.id as id1_0_0_,
            memberlist0_.age as age2_0_0_,
            memberlist0_.TEAM_ID as TEAM_ID4_0_0_,
            memberlist0_.userName as userName3_0_0_ 
        from
            Member memberlist0_ 
        where
            memberlist0_.TEAM_ID in (
                ?, ?
            )

    혹은  @BatchSize 어노테이션 대신 아래와 같이 persistence.xml 에 설정을 추가해도 된다.

    <property name="hibernate.default_batch_fetch_size" value="100" />

     

    1. fetch join 으로 엔티티 조회해서 사용

    2. 애플리케이션에서 dto 로 변환해서 쓴다

    3. 처음부터 new dto 로 스위칭해서 가져온다


     

    www.inflearn.com/course/ORM-JPA-Basic/lecture/21742?tab=community&speed=1.25&q=170331

     

    자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 학습 페이지

    지식을 나누면 반드시 나에게 돌아옵니다. 인프런을 통해 나의 지식에 가치를 부여하세요....

    www.inflearn.com

     

    반응형

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

    OneToMany 단방향과 양방향 매핑  (0) 2022.07.23
    JPA 강의 포인트  (0) 2021.12.15
    JPA 경로표현식  (0) 2021.04.27
    JPA > JPQL, 프로젝션  (0) 2021.04.27
    JPA > 값 타입  (0) 2021.04.25
Designed by Tistory.