[JPA] JPQL사용중 LAZY fetch 가 안될 때

글쓴이 Engineer Myoa 날짜

발단

복잡한 관계 매핑이나 Native SQL에 가까운 쿼리를 유도해야할때에, JPA Repository 만으로 해결이 힘든 경우가 있다. (보통 가능은 하나, 코드가 지저분해진다)

위와 같은 상황에서 비즈니스 로직을 작성하던 중 있던 일이다.  ORM에서 1:M의 OneToMany 관계를 매핑할때는 보통 지연 가져오기를 Fetch 전략으로 선택한다. (Lazy fetching, FetchType.LAZY)

JPA Repository 빈을 가지고 findBy ~~ 하면 이 Lazy fetching이 잘 작동한다. 하지만 JPQL에서는 의도대로 동작하지 않았다.

분석

QueryDSL을 프레임워크를 사용한 JPQL 로직에서 다음과 같은 순서로 이어진다.

1. QClass 엔티티 설정

2. queryDsl 객체로부터 select 절 생성 -> JPQLQuery 객체 리턴

3. JPQLQuery 객체의 fetchResults() 함수 호출 -> QueryResults<Class> 객체 리턴

4. QueryResults<Class> 객체의 getResults() 함수 호출 -> List<Class> 객체 리턴

5. List<Class> 객체를 파라미터로 던져 PageImpl을 통한 Pagination 구현

 

문제는 4번에서 발생한다. 각 객체들이 가지고 있는 OneToMany 관계의 자식 객체들을 전부다 가져온다. 다시 말해, N+1 쿼리 문제가 발생하게 된다.

 

이 문제를 해결하기 위해 어노테이션이나 ORM적으로 고민을 많이 했다. 그러다 문득, SQL 관점에서 해결할 수 있을 것 같다는 생각이 들었다.

해결방법

위 문제에 대한 해결방법은 다음과 같다.

1. OneToMany에 대한 자식객체를 투영(프로젝션)하지 않는다.

너무 심플한 방법이었다.

예를들어 특정 기간내에 parentEntity 들을 조회하는 JPQLQuery 객체를 만든다면,

from(parentEntity)
.where(parentEntity.regDtime.between(~~~, ~~~))

라고 사용했지만,

다음과 같이 투영할 칼럼들을 명시적으로 지정해주는 것이었다.

getQuerydsl().createQuery(parentEntity)
.select(parentEntity.parentColumn1,
parentEntity.parentColumn2)
.where(parentEntity.regDtime.between(~~~, ~~~))

해결됐다! N+1로 DB 질의를 낭비하던 문제가 사라졌다.

하지만 이 방법의 문제는 return의 타입토큰이 List<Tuple> 이었다. 헉..

이렇게 되면 object mapper로 결과를 또 매핑해주어야 하는 지저분함이 발생하기 때문에 좀 더 머리를 굴렸다.

 

찾다보니, .select() 로 칼럼들을 지정할때에 어떤 객체를 베이스로 할지 지정할 수 있었다. 사실 createQuery시에 QClass 엔티티객체를 넣기 때문에 알아서 인식할거라고 생각했는데 생각과 달랐다.

따라서 최종적인 해결방법은 다음과 같다.

getQuerydsl().createQuery(parentEntity)
.select(Projections.fields(ParentEntity.class,
parentEntity.parentColumn1,
parentEntity.parentColumn2)
.where(parentEntity.regDtime.between(~~~, ~~~))

 

 

추가

기존 코드와 개선한 코드에는 차이가 있다.

기존에는 from함수를 사용해 JPQLQuery를 받아왔지만

개선쿼리에는 from절을 직접 사용하지 않고 getQuerydsl().createQuery(path) 를 이용했다.

 

이 이유는 QuerydslRepositorySupport 클래스를 열어 보면 알 수 있는데,

단일 path(사용할 엔티티라고 생각하면 된다)가 입력될 경우 이 from 함수를 호출하게 된다.

	protected <T> JPQLQuery<T> from(EntityPath<T> path) {
		return getRequiredQuerydsl().createQuery(path).select(path);
	}

이미 select 함수를 체이닝하여 원하는 결과가 나오지 않을 것 같아, createQuery까지만  QClass 엔티티를 할당해주고 나머지 투영할 칼럼들을 직접 선택하였다.

 

일반 from절을 사용했을 때 다시 select 를 했을 때 overwritten이 되는지에 대해 beta 배포전까지 알아보려고 한다.

JPQLQuery의 구현체인 HibernateQuery 클래스의 select 함수를 보면 setProjection으로 되어있기 때문에 addProjection이 아니겠다만, 이런 중요한 사항은 직접 시도해보고 결과를 기록하는쪽이 좋다고 생각한다.


4개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다