컬렉션 (1대N 관계에서) Fetch Join은 뻥튀기 조회를 함
"select t From Team t join fetch t.members where t.name = "팀A";
아래 그림에서 보면 팀A에 속한 멤버들(Collection)을 조회하기 위해 위 구문을 날리면 같은 팀A가 회원수만큼 조회됨.
JPQL의 distinct 사용하여 팀 A 결과값의 중복을 제거할 수 있음
이 방식을 사용하면 SQL에 distinct를 추가하여도 팀A-회원1, 팀A-회원2로 DB에서 조회결과가 만들어져 distinct를 통한 중복 제거가 적용 되지 않기 때문에 어플리케이션에서 중복 제거를 진행함: 같은 식별자를 가진 Team 엔티티 제거
N대1 관계에서는 문제 X
일반조인은 JOIN은 하지만 연관 엔티티를 조회는 하지 않음
패치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시로딩처럼 동작)
패치 조인은 객체 그래프를 SQL 한 방 쿼리로 모두 조회해 오는 개념
1대N 관계에서 일반 조인 역시 뻥튀기는 발생
"select t from Team t join fetch t.members m where m.age > 10"
Team에 Members, Orders 두 컬렉션이 있다고 했을 때 둘 다 조인을 하면 데이터가 카타시안곱처럼 뻥튀기가 엄청나게 일어난 쿼리가 나갈 수 있고, 데이터 정합성도 보장하지 못한다.
일대일, 다대일 같은 단일 값 연관 필드들은 패치 조인해도 페이징 가능
하지만 일대다 관계의 연관 필드들은 데이터 뻥튀기가 발생하고 이 과정에서 페이징이 의도와 다르게 동작함.
예를 들어 아래 코드처럼 페이징 처리를 하여 페이지 0번에 1개 데이터를 불러온다면? 팀A 엔티티의 Members 필드에 회원 1만 가지고 있는 엔티티가 조회됨.
원래 페이징의 의도는 팀A의 모든 회원이 탐긴 팀1을 가져오는 것임. 만약 모든 팀 데이터를 페이징으로 불러온다고 하면 1pg - 팀A(팀A의 모든 회원), 2pg - 팀B(팀 B의 모든 회원).... 이런식으로 조회해야 함.
하지만 이렇게 동작하지 않음. 조인의 결과 테이블에 팀A - 회원1, 팀A 회원-2 두 ROW가 발생하기 때문에 이걸 가지고 페이징 처리해버림.(페이징이라는 건 철저하게 DB 중심이기 때문에, JPA 객체 그래프로 끌고와서 팀 단위로 끊어버리는 게 아니라 그림의 [TEAM JOIN MEMBER] 결과를 가지고 처리해 버리기 때문임!)
나아가 더 심각한 문제는 관계형 DB에서의 페이징으로 동작하지 않기 때문에 메모리로 데이터를 전부 조회해와서 페이징 처리를 진행함.
해결책?
1대다의 엔티티를 조회하는 것이 아니라 다대1의 관점으로 뒤집어서 페이징 처리(위 사례에서 팀을 조회하는 것이 아니라 멤버를 조회한다)
패치 조인을 제거하고 그냥 조회한 후에 지연로딩으로 연관 엔티티를 조회하기 위해 DB에 다시 접근, But 이는 N+1 문제 발생시킴. 즉 전체 Team 데이터를 조회하고, 각 팀의 멤버 필드를 조회하는 시점에 멤버들을 끌고 오기 위해 각 팀 ID를 가지고 팀 개수인 N번 만큼 DB에 접근해서 조회함.
이를 해결하기 위해 Batch Size를 설정하면 최적화가 가능함.
연관 필드에 @BatchSize를 설정하거나 글로벌 설정을 통해 fetch의 batch size를 결정하면 지연로딩했던 필드를 조회하는 시점에 쿼리를 날릴 때 in query를 통해 현재 접근한 팀의 멤버 뿐만 아니라 페이징에 필요한 다른 팀의 멤버들도 함께 조회해서 초기화를 해줌.
위 예시를 통해 보자면 아래 코드를 수행할 때 0페이지에 팀 2개를 조회해 오고 해당 팀의 멤버 필드에 접근할 때 최대 batch size만큼 멤버들을 모두 한 번에 조회해와서 초기화 해줌
반복문에서 첫 팀의 멤버들에 접근할 때 아래의 쿼리가 나가서 다른 팀의 멤버들도 최대 100개 in query에 포함시켜 조회해옴
출처: 자바 ORM 표준 JPA 프로그래밍 - 패치조인 1,2