queryDsl 사용기

yeonseong Jo·2023년 9월 12일
0

SEB_BE_45

목록 보기
47/47
post-thumbnail

프로젝트를 진행하면서 spring data jpa만 주구장창 사용하다가
멘토님께서 queryDsl 사용을 권장하셨다.
(현업에서도 많이 사용한다고)
기억상 queryDsl에 대해 들어 본 적은 있었던거 같은데
이게 어떠한 건진 전혀 모르고 있었다.


queryDsl

Unified Queries for Java.
Querydsl is compact, safe
and easy to learn

공식 사이트 문구

쿼리를 문자열로 작성하는 대신,
자바 코드로 쿼리를 작성할 수 있도록 도와준다고 한다.
이를 통해 컴파일 시점에 오류, 오타, 문법 오류를 알 수 있고,
복잡한 쿼리를 JPQL보다 훨~~~씬 쉽게 작성할 수 있다고 한다.

공식 사이트를 가보니
sql은 물론이고 NOsql까지 지원하는 거 같다.

gradle 설정

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를 자동으로 만들기 위한 설정인거 같다.
(추후 좀 더 알아볼 예정)

Q class?

gradle 설정 이후 로드한 뒤,
intelliJ 기준으로
우측 사이드 바에 있는 gradle을 눌렀을 때
Tasks - other - compliQueryDsl을 찾아 실행 시키면

querydslDir의 경로에 서비스 내 구현한 entity들의
Q class가 생성이 된다.

생성된 Q class에 대해 간단히 살펴 보려 했는데,
뭐가 좀 많다..
다음 기회에

QueryDslConfig

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);
    }
}

CustomReporitory

진짜 마지막으로 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가..)

JpaRepository

public interface EntityRepository extends JpaRepository<Entity, long>, CustomReporitory {
}

fetchJoin

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()을 붙이면 되는데,
주의사항이 존재한다.

~ToMany, ~ToOne 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을 하려면

List -> Set

Entity의 ~ToOne관계의 필드에서
타입을 List가 아닌 Set으로 지정

query 분리

CustomRepository에서 fetchJoin을 분리하여 service에서 합침

OrderColumn 사용

Entity의 ~ToOne관계의 필드에서 @OrderColumn을 사용해 데이터를 불러올 때 정렬시켜
hibernate에서 ListType으로 인식시키게 함

이러한 방법이 존재한다.

아무리 봐도 첫 번째 방법은 별로인 거 같고,
fetchJoin을 분리하거나, OrderColumn을 사용하는 게 나을 거 같은데,

OrderColumn을 사용하는 방법 또한 가져올 때 정렬을 실행하는 것이기 때문에 이 또한 그닥 좋은 방법은 아닐거 같다.

그래서 query가 몇 번 더 발생하더라도
fetchJoin을 분리하여 가져오는 것이 더 낫지 않을까 싶다.


후기

후..
queryDsl 뭔가 엄청 쉬운데,
fetchJoin을 들어가면서 부터
신경쓸께 엄청 많아졌다.
물론 JPA만 사용할 때에 fetchJoin에 대해 잘 몰라 사용을 안했기 때문이겠지만..

그래도 나름 이해는 한 거 같고
궁금한 점이 있어 멘토님께 여쭤보고
답변을 받으면 잘 사용할 수 있을거 같다.

profile
뒤(back)끝(end)있는 개발자

0개의 댓글