페치 조인
1. 엔티티 페치 조인
연관 관계 매핑할 때 가장 중요한 점은 OneToOne이나 ManyToOne은 지연 로딩 처리해줘야 한다는 점이였다. 지연 로딩을 하지 않고 즉시 로딩을 하게 되면 연관된 엔티티를 모두 가져오면서 디비 조회 낭비가 이루어지기 때문이다.
하지만 지연로딩으로 처리해도 문제가 발생한다.
아래의 코드를 보자.
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() + ", " + member.getTeam().getName());
//회원1, 팀A(SQL)
//회원2, 팀A(1차캐시)
//회원3, 팀A(SQL)
//최악의 경우엔 회원마다 쿼리나간다. 아주 성능떨어진다. 1+N 쿼리가 된다. 이건 즉시로딩이든 지연로딩이든 발생하니까 fetch join으로 하면 끝남
}
위의 코드를 보면 지연로딩으로 team을 가져오기 때문에 프록시(가짜 엔티티)를 가져오게 된다. 문제는 프록시이기 때문에 getTeam().getName()을 통해서 조회할때 쿼리가 한번 더 나간다는 점이다.
이게 몇개 없을 때는 문제가 되지 않지만 만약 10000개가 있다고 가정해보자. 분명 쿼리는 1개를 날렸는데 1+10000 쿼리가 나가는 것이다.
이게 바로 N+1 문제이다. 이를 해결하기 위해서는 명시적 조인에 fetch키워드만 넣으면 된다.
String query = "select m from Member m join fetch m.team";
이렇게 되면 더이상 team은 프록시가 아니게 된다. 지연로딩으로 설정을 해도 페치 조인이 우선권이 높다. 조회에 대해서 이슈가 많이 발생하므로 조회성에 많이 사용된다.
2. 컬렉션 페치 조인
String query = "select t from Team t join fetch t.members";
컬렉션 또한 페치 조인이 가능하다. 하지만 데이터베이스 입장에서는 결과가 뻥튀기가 일어난다. 회원 2명이 같은 팀 소속이라면 팀에 대해서 회원이 2명씩 2번 발생해서 결과가 2개가 나오는게 아니고 4개가 나온다.
이 뻥튀기를 방지하기 위해서는 select절에 distinct 키워드를 사용하며 된다.
String query = "select distinct t from Team t join fetch t.members";
이 distinct는 sql문법의 중복제거의 의미도 있지만, JPQL에서의 distinct는 추가적으로 애플리케이션에서의 엔티티 중복제거를 지원한다. 즉, 데이터를 퍼올릴 때, 아예 같은 키로 되어 있는 엔티티는 제거 시키고 데이터를 가져오는 것이다.
참고로,
String query = "select t from Team t join fetch t.members";
이 쿼리는 일반 조인이다. 엄밀히 말하면 프록시는 아닌데, 지연로딩으로 들어가기 때문에 한번에 멤버에 대한 값을 가져오지 않는다.
3. 페치 조인의 특징과 한계
-
페치 조인 대상에는 별칭을 줄 수 없다 : 하이버네이트는 가능하다. 하지만 가급적 사용하지 말자. 만약 일부만 가져오고 싶으면 페치조인 말고 그냥 단순 조회를 통해서 가져와라. 정합성 때문에 별칭줘서 where로 이런거 x 별도의 쿼리를 날려라. cascade걸려있거나 하면 날라갈 수도 있고 그렇다. 영속성 컨텍스트 입장에서는 정합성이 맞지 않으면 오류가 난다.
-
둘 이상의 컬렉션은 페치 조인 할 수 없다 : 정합성 문제. 일대다도 뻥튀기가 되는데, 자칫하면 뻥튀기에 뻥튀기가 될 수 있다.
-
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다 : 뻥튀기가 이루어져서 최소 2개를 가져와야 되는 상황에서 1개만 가져오기로 설정하면 짤린다. 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징가능하다. 하이버네이트는 경고 로그를 남기고 메모리에서 페이징하는데 매우 위험하다. 모든 데이터를 가져와서 메모리에 올려서 페이징 처리한다. 100000건이 있으면 다 메모리에 올리고 페이징처리한다. 정말 위험하다.
해결책 -> 컬렉션 페치 조인 말고, 엔티티 페치 조인으로 조인 방향을 뒤집어서 처리하자.
컬렉션 페치 조인의 경우에는 fetch조인으로 해결이 불가능하므로 betch size를 글로벌하게 줄 수 있다. batch size를 100으로 잡으면 170개가 있으면 100을 먼저 가져오고 더 필요하면 70을 마저 가져오는 방식이다. persistence.xml property를 넣으면 된다.
<property name="hibernate.default_batch_fetch_size" value="100"/>