프로젝트를 진행하면서 spring data jpa만 주구장창 사용하다가
멘토님께서 queryDsl 사용을 권장하셨다.
(현업에서도 많이 사용한다고)
기억상 queryDsl에 대해 들어 본 적은 있었던거 같은데
이게 어떠한 건진 전혀 모르고 있었다.
Unified Queries for Java.
Querydsl is compact, safe
and easy to learn
쿼리를 문자열로 작성하는 대신,
자바 코드로 쿼리를 작성할 수 있도록 도와준다고 한다.
이를 통해 컴파일 시점에 오류, 오타, 문법 오류를 알 수 있고,
복잡한 쿼리를 JPQL보다 훨~~~씬 쉽게 작성할 수 있다고 한다.
공식 사이트를 가보니
sql은 물론이고 NOsql까지 지원하는 거 같다.
jdk11, spring boot 2.7x 버전 기준으로
...
plugins {
...
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
...
dependencies {
...
implementation 'com.querydsl:querydsl-jpa:5.0.0'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0'
...
}
...
def querydslDir = "/src/main/queryDsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
dependencies를 통해 버전을 설정하고,
def querydslDir
아래 부터는
queryDsl을 사용할 때 필요한 Q class
를 자동으로 만들기 위한 설정인거 같다.
(추후 좀 더 알아볼 예정)
gradle 설정 이후 로드한 뒤,
intelliJ 기준으로
우측 사이드 바에 있는 gradle을 눌렀을 때
Tasks - other - compliQueryDsl을 찾아 실행 시키면
querydslDir
의 경로에 서비스 내 구현한 entity들의
Q class가 생성이 된다.
생성된 Q class에 대해 간단히 살펴 보려 했는데,
뭐가 좀 많다..
다음 기회에
queryDsl을 사용하기 위해서는
JPA에서 EntityManagerFactory가 필요한 것 처럼
JPAQueryFactory가 필요하다.
어플리케이션을 실행할 때 하나만 생성되어야 하고,
또 이 JPAQueryFactory는 entityManager를 필요로 하기 때문에
Configuration을 통해 Bean으로 설정하여
entityManager를 주입받아 싱글톤으로 존재하게 한다.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManger;
@Bean
public JPAQueryFactory jpaQueryFacotry() {
return new JPAQueryFactory(entityManager);
}
}
진짜 마지막으로 queryDsl을 사용할 때
단일로도 사용할 수 있다고 하지만,
JpaRepository와 같이 사용할 수도 있다고 한다.
우선 interface로 각 entity에 맞게
CustomRepository를 생성하고,
public interface EntityCustomRepository {
Optional<Entity> findEntityFetchJoin(long id);
List<Entity> findAllEntityFetchJoin();
}
구현 클래스를 생성한다.
이때 구현 클래스 이름은 반드시 Impl로 끝나야 한다.
아직 이유는 잘..
import static ~.~.~.QEntity.entity
@RequiredArgsConstructor
@Repository
public class EntityCustomRepositoryImpl implements EntityCustomRepository {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Optional<Entity> findEntityFetchJoin(long id) {
return Optional.ofNullable(
jpaQueryFactory.selectFrom(entity)
.leftJoin(entity.childEntity).fetchJoin()
.where(entity.id.eq(id))
.fetchOne()
);
}
@Override
public List<Entity> findAllEntityFetchJoin() {
return jpaQueryFactory.selectFrom(entity)
.leftJoin(entity.childEntity).fetchJoin()
.leftJoin(entity.parentEntity).fetchJoin()
.fetch();
}
}
오 이렇게 보니
구현이 약간 django의 ORM과 유사하다!!
(그래도 직관적인건 django가..)
public interface EntityRepository extends JpaRepository<Entity, long>, CustomReporitory {
}
inner던 outer던 right던 left던 join 이후
fetchJoin을 하지 않으면,
연관관계 매핑이 되어있는 컬럼의 Fetch 전략이 무엇이든 간에
N+1 문제를 야기한다.
이는 queryDsl만 적용되는 문제가 아니다.
N+1 문제란?
N+1 문제는 하나의 쿼리를 보냈을 때
연관관계 매핑에 의하여
추가적인 쿼리를 실행하게 되는것으로
심각한 성능 저하를 유발할 수 있다.
fetchJoin, EntityGraph, BatchSize 등으로 해결할 수 있다.
fetch 전략이 eager, lazy에 상관없이
N+1문제는 발생할 수 있으며
eager는 항상 발생하고,
lazy는 개발자가 어떻게 하느냐에 달려있다.
fetch 전략이나 N+1문제에 대해서는 다음 블로깅 때 더 자세히 다루도록 하겠다.
여튼,
fetchJoin이 뭐냐 하면
이 N+1 문제를 해결할 수 있는 방법중 하나로
조인하는 연관 관계 데이터를 즉시 로딩하는 방법이다.
eager
전략과는 다르다.
fetchJoin은 queryDsl에만 있는것은 아니다.
queryDsl에선 단순히 **Join
뒤에 .fetchJoin()
을 붙이면 되는데,
주의사항이 존재한다.
일단 ~ToOne
연관 관계 매핑에선
fetchJoin을 원하는 만큼 실행할 수 있다고 한다.
하지만, ~ToMany
관계에선
query한번당 딱 한 번만 사용할 수 있는데,
만약 횟수를 초과하면,
MultipleBagFetchException
에러가 발생한다.
출처에 의하면
~ToOne
의 경우 데이터를 가져오면 연관 관계의 데이터는 하나이고,
~ToMany
의 경우 연관 관계의 데이터는 하나 이상이기 때문에
List 형태로 받아온다.이때 hibernate는 List 데이터를 unordered list로 간주하고
BagType으로 인식한다고 한다.hibernate에서 오직 1개의 BagType만 fetchJoin을 허용하기 때문에 queryDsl에서 또한 한 번만 가능하다.
queryDsl <- jpa <- hibernate
그래서 하나의 entity에 ~ToMany
관계가 많을 때
fetchJoin을 하려면
Entity의
~ToOne
관계의 필드에서
타입을 List가 아닌 Set으로 지정
CustomRepository에서 fetchJoin을 분리하여
service
에서 합침
Entity의
~ToOne
관계의 필드에서@OrderColumn
을 사용해 데이터를 불러올 때 정렬시켜
hibernate에서 ListType으로 인식시키게 함
이러한 방법이 존재한다.
아무리 봐도 첫 번째 방법은 별로인 거 같고,
fetchJoin을 분리하거나, OrderColumn을 사용하는 게 나을 거 같은데,
OrderColumn을 사용하는 방법 또한 가져올 때 정렬을 실행하는 것이기 때문에 이 또한 그닥 좋은 방법은 아닐거 같다.
그래서 query가 몇 번 더 발생하더라도
fetchJoin을 분리하여 가져오는 것이 더 낫지 않을까 싶다.
후..
queryDsl 뭔가 엄청 쉬운데,
fetchJoin을 들어가면서 부터
신경쓸께 엄청 많아졌다.
물론 JPA만 사용할 때에 fetchJoin에 대해 잘 몰라 사용을 안했기 때문이겠지만..
그래도 나름 이해는 한 거 같고
궁금한 점이 있어 멘토님께 여쭤보고
답변을 받으면 잘 사용할 수 있을거 같다.