Querydsl 연관관계가 없을 때 fetchJoin 사용 시 내부동작

yeahdy_:)·2024년 5월 24일

JPA

목록 보기
2/2
post-thumbnail

두 테이블의 서로 연관관계가 없는 테이블의 데이터를 조회할 때 Querydsl의 fetchJoin이 왜 동작하지 않는지 궁금했다.

Querydsl의 FetchJoin() 기능은 데이터베이스에 존재하는 기능이 아니라,

JPA를 사용하는 환경에서 연관된 엔티티를 한 번의 쿼리로 가져오기 위해 사용되는 JPA의 기능이라고 알고 있지만 호출하여 사용이 가능하다.

실제 동작은 fetchJoin 기능이 작동하진 않기 때문에 쿼리는 일반적인 JOIN 으로 실행되는데, 내부적으로 어떤 동작에 의해서 실행이 되지 않는지 찾아보았다.

배경

회원이 즐겨찾기 시 BookDetail테이블의 PK인 IDBookmark테이블의 BOOK_ID 칼럼에 들어간다. 1:N 관계이지만, 연관관계가 맺어지지 않은 상태이다.

BOOK_DETAILBOOK_MARK
IDID
CATEGORY_IDBOOK_ID
BOOK_NAMEUSER_ID
AUTHOR_IDDESCRIPTION

시나리오는 특정 카테고리의 책들을 즐겨찾기한 회원 목록을 보여줘야 한다.

연관관계가 없는 1:N 테이블 fetchJoin() 내부 동작

public List<BookmarkDto> findCategoryBookmark(String categoryId) {
    return queryFactory
            .select(Projections.fields(BookmarkDto.class
                    , bookDetailEntity.categoryId
                    , bookmarkEntity.bookId
                    , bookmarkEntity.userId
                    , bookmarkEntity.description
            ))
            .from(bookDetailEntity)
            .innerJoin(bookmarkEntity).on(bookDetailEntity.id.eq(bookmarkEntity.bookId))
            .fetchJoin()
            .where(bookDetailEntity.categoryId.eq(categoryId))
            .fetch();
}

BookDetailBookmark을 맵핑할 수 있는 칼럼인 book_id 를 통해 이너 조인을 했다. 이때 연관관계가 없는 두 테이블을 fetchJoin() 할 경우 어떻게 동작하는 지 확인할 것이다.

.innerJoin(bookmarkEntity).on()

  1. innerJoin() 를 보면 JPQL을 생성하는 queryMixin.innerJoin을 호출한다.
    참고로 Querydsl에서 SQL이 되는 과정 중 JPQL 로 변환한다.

  1. queryMixin.innerJoin() 를 보면 조인 타입으로 INNERJOIN이 있고, 이때 매개변수 target은 조인하는 테이블인 bookmarkEntity이다.

  1. metadata.addJoin 의 코드에서 안쪽 if 조건문을 보면
  • if (expr instanceof Path && ((Path)expr).getMetadata().isRoot())
    expr(target)이 Path 타입이고 메타데이터가 루트(독립적인 엔터티)인지 확인한다. 이 조건을 만족하면 target을 exprInJoins 목록에 추가해서 Join 쿼리문을 만든다.
  • else
    다른 엔터티와의 관계가 설정된 엔티티가 else 에 해당한다. 이 경우엔 validate(expr) 메서드를 호출하여 조인의 유효성을 검증한다.


    따라서 BookDetailBookmark은 연관 관계가 아니기 때문에 if (expr instanceof Path && ((Path)expr).getMetadata().isRoot())조건문에 부합하고 이때 inner join 절이 만들어진다.

    추가로 on() 절은 JPQL 로 변환 시 괄호 안의 조건과 함께 with 키워드로 변경된다.

.fetchJoin()

  1. fetchJoin() 를 보면 JPQL을 생성하는 queryMixin.fetchJoin을 호출하고 있다.

  1. queryMixin.innerJoin()를 보면 이때 addJoinFlag() 메소드를 통해 FETCH 키워드가 붙게 된다.

위 과정을 통해 만들어진 JPQL 문을 보면

JPQL

select 
	bookDetailEntity.categoryId,
	bookmarkEntity.bookId,
	bookmarkEntity.userId,
	bookmarkEntity.description
from BookDetailEntity bookDetailEntity
inner join fetch BookmarkEntity bookmarkEntity 
with bookDetailEntity.id = bookmarkEntity.bookId

이너 조인과 fetch 키워드가 있고 onwith 키워드로 변경되었다. with키워드로 변경된 이유는 문법적 차이로 Hibernate는 with 키워드를 사용하여 조인 조건을 지정하기 때문이다.

네이티브 SQL

select 
	bookDetailEntity.categoryId,
	bookmarkEntity.bookId,
	bookmarkEntity.userId,
	bookmarkEntity.description
from BookDetailEntity bookDetailEntity
inner join BookmarkEntity bookmarkEntity 
on (bookDetailEntity.id = bookmarkEntity.bookId)

하지만 실행된 네이티브 SQL문은 fetch키워드가 없어져 있다.(별칭 생략)

fetch 키워드가 사라진 이유는 뭘까?

먼저 fetch 키워드는 JPQL에서 연관된 엔티티나 컬렉션을 한 번에 가져오기 위한 명령어로 사용되지만, SQL 표준에는 JPQL의 fetch 키워드와 같은 기능이 없다.

먼저 JPA 구현체는 JPQL의 fetch키워드를 사용하여 생성된 쿼리를 분석하고, 필요한 조인을 추가하여 관련 데이터를 한 번에 가져올 수 있도록 네이티브 SQL을 생성한다.(위 디버깅 과정) 이때 fetch 키워드가 제거되고 조인 조건만 포함한 SQL을 생성한다.

결과적으로 JPQL의 fetch 키워드는 JPA 레벨에서의 데이터 로딩 전략을 지정하는 것이고, 네이티브 SQL로 변환될 때는 SQL 표준 문법에 맞게 조인 구문으로 변환된다. 이 과정에서 fetch 키워드는 사라지지만, JPA는 여전히 연관된 엔티티를 즉시 로딩하게 되는 것이다.

이유

첫번째로

querydsl 의 공식문서를 보면 연관관계가 되어 있는 테이블 간에 Join을 사용하고 on() 메소드를 사용하고 있지 않다.

만약 Join 시 `on()`을 사용할 경우 조건절과 같이 사용하고 있다.

즉, 연관관계인 CatKitten을 조인하되 Kitten bodyWeight가 10.0보다 작은 경우는 제외된다는 것을 의미하는 조건절이다.

따라서 innerJoin().on()을 사용하여 연관관계가 없는 테이블 간에 조인을 수행하는 경우 단순히 on() 절의 조건에 맞는 네이티브 SQL을 생성하는 것이다.
그렇기 때문에 JPA에 의해 처리되는 연관된 엔티티의 Fetch 전략을 사용할 수 없다.

두번째로

JPA의 구현체인 Hibernate의 공식문서를 보면

"fetch" 조인은 부모 객체와 연관된 속성이나 컬렉션의 값들을 단일 select 쿼리로 한 번에 초기화하는 것을 허용합니다. 이는 컬렉션의 경우에 특히 유용합니다. 이는 매핑 파일에서의 outer join과 lazy 선언을 효과적으로 무시하고 연관 및 컬렉션에 대한 조인을 수행합니다. 이를 통해 데이터베이스에서 부모 객체와 그에 속한 연관된 객체 또는 컬렉션을 한 번의 쿼리로 모두 가져올 수 있습니다.

fetch Join에 대해 데이터베이스에서 연관된 객체 또는 연관된 컬렉션을 즉시 로딩한다고 말한다. 따라서 연관관계가 없으면 fetch Join 을 사용할 수 없는 것이다.

정의대로 fetchJoin은 일반적으로 연관관계가 명확히 정의된 엔티티에서 사용되어야 한다. 연관관계가 맺어지지 않은 엔티티에 fetchJoin()사용 시 혼란이 생길 수도 있기 때문에 fetchJoin() 을 제거하는 것이 적절하다.

참고: http://querydsl.com/static/querydsl/5.0.0/reference/html/ch02.html#d0e317
https://docs.jboss.org/hibernate/orm/3.3/reference/ko-KR/html/queryhql.html#queryhql-joins

profile
기억하기 위해 기록하고 있습니다. 포스트 중 잘못된 정보가 있다면 코멘트 남겨주세요🐰

0개의 댓글