-
JPA > Fetch Join , BatchSizeBackEnd/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
반응형'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