[QueryDSL] SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list

이명규·2024년 9월 5일
0

쿼리 DSL 을 사용하다가 N+1 문제가 발생하였다
원인은 fetchJoin 을 실행하지 않고 엔티티의 연관관계 엔티티를 바로 가져오기 때문에 발생한 이슈였다

원인을 알고있으니 바로 수정후 쿼리를 실행해보았다

그러나 또 다른 이슈가 발생하였다

조금 찾아보니

fetch된 엔티티의 소유자가 select 리스트에 포함되지 않아서 발생한 문제이며 쿼리에서 fetch join을 사용하면서 fetch된 연관관계의 주인이 쿼리의 select 리스트에 포함되지 않은 경우 발생하는 Hibernate의 SemanticException 이라고 한다.

그래서 쿼리 DSL 코드를 쭉 훑어보니 현재 pagination 쿼리를 사용하고 있는 API 이며, 관련해서 total 개수를 가져오기 위해 쿼리를 사용할 때 fetch join 을 하지만 사실 개수만 조회하기 때문에 select 절에는 join 된 엔티티가 포함되지 않는 쿼리가 있었고 이 부분이 범인이었다

val total =
    queryFactory
        .select(task.count())
        .from(task)
        ...
        .join(task.orderDetail, orderDetail)
        .fetchJoin()
        .where(task.routing.eq(routing))
        .fetchOne() ?: 0L

아래처럼 수정하니 정상 동작하였다

 val total =
     queryFactory
         .select(task)
         .from(task)
         .join(task.orderDetail, orderDetail)
         .where(task.routing.eq(routing))
         .fetchCount()

사실 total 쿼리에 대해서는 fetch join 이 굳이 필요하지 않았다
이외에도 찾아보니

QueryProjection 을 이용하여 DTO 를 사용할때도 fetch join 을 쓰는것은 같은 예외가 발생하게 된다

fetch join 을 사용하는 이유는 엔티티 상태에서 엔티티 그래프를 참조 및 엔티티 그래프를 효율적으로 로딩하기 위해 사용하는 것이며, 따라서 엔티티가 아닌 DTO 상태로 조회하는 것은 불가능하다고한다.

그러면 Projection 만 사용할 때의 동작을 알아보자

Projection만 사용할 때의 동작

  • Projection만 사용하고 fetch join을 하지 않으면, QueryDSL이 자동으로 필요한 필드에 대해서만 join을 수행, 하지만 이는 fetch join이 아니라 일반적인 join으로 실행된다

  • QueryDSL은 Projection에서 요청한 필드들을 분석하여 필요한 join을 자동으로 생성

  • 이 join은 필요한 데이터만 가져오는 일반 join이며, 연관 엔티티 전체를 즉시 로딩하는 fetch join과는 다르다

  • 이는 fetch join과 달리 연관 엔티티 전체를 메모리에 로딩하지 않음, Projection을 사용할 때는 fetch join이 필요하지 않으며, QueryDSL이 필요한 데이터만을 효율적으로 가져오도록 쿼리를 최적화

또한 Projection을 사용할 때는 N+1 문제에 주의해야 한다, 연관 엔티티의 데이터를 가져올 때 추가적인 쿼리가 발생할 수 있기 때문.

번외로 일반 Join 과 Fetch Join 의 비교

  • 일반 Join: 연관된 엔티티의 데이터를 가져오지만, 실제 엔티티 객체로 변환하지 않는다
  • Fetch Join: 연관된 엔티티를 함께 조회하여 영속 상태의 엔티티 객체로 변환함
    • Fetch Join을 사용하면, 연관된 엔티티들이 모두 영속 상태로 로딩되어 엔티티 그래프를 완전히 구성, 이는 N+1 문제를 해결하고, 성능을 최적화하는 데 도움이 됨

DTO 는 단순히 데이터 전송 객체이며 엔티티가 아니기 때문에 Fetch Join 의 개념이 적용되지 않음
(엔티티간의 관계를 유지하지 않음)

profile
개발자

0개의 댓글

관련 채용 정보