BackEnd/JPA

JPA > Fetch Join , BatchSize

ssseung 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

 

반응형