≣ 목차
fetch join의 한계
1. fetch join 대상에는 별칭을 줄 수 없다??
JPA에서 select t from Team t join fetch t.members m 쿼리에서 마지막 조인 대상인 t.members m에 별칭을 주면 안 되는데, 이는 JPA 표준에서는 지원하지 않기 때문입니다. 하이버네이트와 같은 몇몇 구현체는 별칭을 지원하지만, 이로 인해 데이터 무결성이 깨질 수 있는 문제가 발생할 수 있습니다. 김영한님의 책에서도 별칭을 잘못 사용하면 연관된 데이터 수가 달라져 데이터 무결성이 깨질 수 있다고 언급하고 있습니다. 하이버네이트를 기준으로 별칭만 줄 경우 일대다, 다대일 관계에서는 문제없이 실행되며, 아래 두 쿼리는 실행했을 때 에러가 발생하지 않습니다.
String query1 = "select t from Team t join fetch t.members m";
String query2 = "select m from Member m join fetch m.team t";
문제가 발생하는 상황은 fetch join에 대상에게 ON 절, where 절을 사용하면 문제가 발생합니다.
String query = "select t from Team t join fetch t.members m ON m.name = 'name'";
해당 쿼리처럼 fetch join에 대상에게 on절을 사용하면 무조건 아래와 같은 에러가 발생합니다.
with-clause not allowed on fetched associations; use filters
그렇다면 왜 fetch join 대상에게 ON을 사용할 경우 무조건 에러가 발생할까?? 그 이유는 members 컬렉션이 전부 조회되지 않기 때문에 애초에 실행 불가능합니다.
그럼 왜 JPA에서는 별칭을 지원하지 않는데 몇몇 구현체는 별칭을 지원할까?? 이유는 fetch join 대상에 별칭을 사용할 수 있지만 문제가 발생할 수 있기 때문입니다. fetch join을 사용할 때는 연관된 모든 엔티티가 있을 것이라 가정하고 사용해야 합니다. 왜냐하면 fetch join은 연관된 엔티티를 한 번의 쿼리로 가져오기 위해 사용됩니다. 만약 fetch join을 사용하여 특정 엔티티를 가져오고, 그 엔티티와 연관된 다른 엔티티가 없거나 일부만 존재한다면, 예상치 못한 결과가 발생할 수 있습니다. 예를 들어, 특정 팀에 속한 멤버를 가져오려고 할 때, 해당 팀에 멤버가 없다면 결과가 비어 있을 수 있습니다.
예를 들어, Team과 Member 간의 1대N 관계에서 다음과 같은 코드가 있습니다
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("m1");
member1.setTeam(team);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("m2");
member2.setTeam(team);
em.persist(member2);
em.flush();
em.clear();
List<Team> result = em.createQuery("select t from Team t join fetch t.members m where m.username = 'm1'", Team.class)
.getResultList();
for (Team team1 : result) {
System.out.println("team1 = " + team1.getName());
List<Member> members = team1.getMembers();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
}
}
실행결과
team1 = teamA
member = m1
fetch join의 결과는 연관된 모든 엔티티가 있을 것이라 가정하고 사용해야 합니다. 하지만 fetch join에 별칭을 잘못 사용하여 컬렉션 결과를 필터링하면, 객체의 상태와 DB의 상태 일관성이 깨지게 됩니다. DB와 일관성이 깨지더라도 조회 용도로만 사용한다면 fetch join 대상의 별칭을 사용해도 되지만, 이 경우에도 2차 캐시 등에서 주의가 필요합니다.
Fetch join의 결과는 연관된 모든 엔티티를 조회해야 하지만, 조회 용도로만 사용하면 객체의 상태와 DB의 상태 일관성이 깨질 수 있습니다. 그럼에도 불구하고 WHERE 절은 사용이 가능합니다.
결론
fetch join 대상
- on절: 무조건 에러 발생
- where절: fetch join의 결과는 연관된 모든 엔티티를 조회해야 되어서 객체의 상태와 DB의 상태 일관성이 깨질 수 있지만 그럼에도 불구하고 조회 용도로만 사용한다면 where절을 사용해도 됩니다.
fetch join 대상 x
- on절: fetch join은 연관 객체(테이블) 자체의 데이터를 전부 조회 하는 것이기 때문에 on에 사용하는 것은 의도에 맞지 않습니다. 따라서 데이터를 필터링하고 싶으면 where를 사용하는 것이 맞습니다.
- where 절: 사용 가능
2. 둘 이상의 컬렉션은 페치 조인 할 수 없습니다
둘 이상의 컬렉션을 페치 조인하면 카테시안 곱이 발생할 수 있습니다. 즉, 각 컬렉션의 모든 조합이 결과로 반환되기 때문에, 데이터의 중복이 발생하고, 결과 값이 급격히 증가할 수 있습니다. 이러한 이유로 JPA에서는 둘 이상의 컬렉션을 페치 조인하는 것을 허용하지 않습니다.
아래 코드를 보면 Team엔티티가 members, sponsors 두 개의 컬렉션을 갖고 있습니다.
@Entity
public class Team {
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
@OneToMany(mappedBy = "team")
private List<Sponsor> sponsors = new ArrayList<>();
}
String query = "select t from Team t join fetch t.members join fetch t.sponsors ";
List<Team> resultList = em.createQuery(query, Team.class)
.getResultList();
fetch join을 두 개의 컬렉션에 사용하면 MultipleBagFetchException예외가 발생합니다.
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [jpql.Team.members, jpql.Team.sponsors]
해결방법
JPA에서 두 개의 컬렉션을 페치 조인할 때 Set 자료형으로 변경하고 @BatchSize 애노테이션을 사용하면 데이터 중복을 방지할 수 있지만, 여전히 카테시안 곱이 발생하는 문제는 해결되지 않습니다. 따라서 두 개의 컬렉션이 동시에 페치 조인되는 상황에서는 DTO를 사용하거나 직접 쿼리를 작성하는 방법이 필요합니다.
혹시 Set 자료형으로 변경한 후 @BatchSize를 사용하고자 하시는 분들은 해당 사이트를 참고해 주시기 바랍니다.
3. 컬렉션을 페치 조인하면 페이징 관련 기능을 사용할 수 없습니다
fetch Join + Pagination을 사용할 때 안되는 경우는 컬렉션이 ~ToMany 관계일 때 입니다. 이 경우, 하나의 부모 엔티티가 여러 자식 엔티티를 가질 수 있기 때문에 중복된 데이터가 발생합니다. 예를 들어, 부모 엔티티가 여러 자식 엔티티와 연결되어 있으면, 부모 엔티티가 중복되어 나타나게 되어 페이징을 적용할 때 정확한 결과를 얻기 어려워 JPA에서 firstResult/maxResults specified with collection fetch; applying in memory라는 경고 메시지가 발생합니다.
반면, ~ToOne 관계와 같은 단일 값 연관 필드는 페치 조인을 사용해도 페이징이 가능합니다. 이러한 문제를 해결하기 위해서는 DTO를 사용하여 필요한 데이터만 담아 페치 조인 없이 필요한 필드만 선택적으로 조회하고 페이징을 적용할 수 있습니다. 또 다른 방법으로는 페이징이 필요한 경우 페치 조인을 사용하지 않고 별도의 쿼리를 실행하여 부모 엔티티와 자식 엔티티를 각각 조회한 후, 애플리케이션에서 조합하는 방법이 있습니다.
참고
https://iseunghan.tistory.com/478
JPA - Fetch Join이 과연 만능인가? (N+1, Pagination)
들어가기 전 이전 시간에 알아봤던 N+1 해결법에 이어서 FetchJoin을 이용해서 해결할 수 있었습니다. 하지만 Fetch Join이라고 다 해결할 수 있는 것은 아닙니다. 이번 시간에는 Fetch Join을 사용했을
iseunghan.tistory.com
'JAVA > JPA' 카테고리의 다른 글
[JPA] Named 쿼리 (0) | 2024.12.03 |
---|---|
[JPA] 벌크(Bulk) 연산 (1) | 2024.12.02 |
[JPA] JPQL 페치조인(fetch join) (1) | 2024.11.28 |
[JPA] JPQL 경로 표현식 (0) | 2024.11.26 |
[JPA] JPQL 함수 정리 - Hibernate 6.x 버전 사용자 정의 함수 등록 (1) | 2024.11.26 |