N+1, Fetch Join, JPQL @Query 사용 후기
QueyrDSL?
JPA를 쓰면서 내가 가져오고 싶은 객체에 연관된 객체 자체를 한번의 쿼리에 모두 담아오고 싶을 때는 JPQL +Fetch Join 이나 EntitiyGraph를 통해 가져올 수 있다. 하지만 Spirng Data JPA만 사용해서는 조건에 맞는 연관된 객체를 찾기엔 JPQL문 자체가 길어지고, 오타 혹은 문법적인 오류를 포함할 가능성이 높아지고, 이후 엔티티 변경 시에도 JPQL도 영향을 받을 확률이 있다.
QueryDSL
이런 문제를 해결해주는 QueryDSL은 정적 타입을 이용해서 쿼리를 생성해주는 프레임워크이다.
장점으로는
- 코드로 쿼리를 작성하기에 컴파일 시점에 문법 오류를 쉽게 확인할 수 있고 가독성이 좋다.
- 동적인 쿼리 작성이 편리하다.
- 변화하는 도메인 모델(Entity)의 변경사항이 쿼리에 직접 반영되어 type-safe하다.
단점으로는
- 엔티티들을 찾아 QueryDSL에서 사용할수 있는 클래스로 만드는 플러그인 설정이 빌드툴 및 IntelliJ 버젼에 따라 약간씩 다르다
설정
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.11'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
def querydslDir = "$buildDir/generated/querydsl"
// JPA 사용 여부와 사용할 경로를 설정
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
// build 시 사용할 sourceSet 추가
sourceSets {
main.java.srcDir querydslDir
}
// querydsl 컴파일시 사용할 옵션 설정
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
// querydsl 이 compileClassPath 를 상속하도록 설정
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
기본적으로 QueryDSL은 프로젝트 내의 @Entity 어노테이션이 선언된 클래스를 탐색하고 JPAAnnotationProcessor를 사용해 Q 클래스를 생성한다. 생성된 Q 클래스를 프로젝트 코드 내에서 import해서 사용할 수 있게까지 설정을 해줘야 한다.
Repository에서 Querydsl 사용하기
Querydsl 간단 사용, Repository 사용하기 - by Tecoble, QueryDslConfig 설정파일 등록
별도의 Configuration 에서 EntityManager로 JPAQueryFactory를 빈으로 등록하지 않아도 QuerydslRepositorySupport 추상 클래스를 Repository 구현체에서 상속받아 사용할 수 있다.
Repository Extension
QueryDSL 을 사용할 Reposiotry에 사용할 RepositoryExtension 인터페이스를 만들고 Repository에 extends
@Transactional(readOnly = true)
public interface TeamRepositoryExtension {
Page<Team> findByKeyword(String keyword, Pageable pageable);
}
@Transactional(readOnly = true)
public interface TeamRepository extends JpaRepository<Team, Long> , TeamRepositoryExtension{
// 생략 ..
}
Repository Extension Impl
ReposiotryExtension의 네이밍은 Repository와 달라도 되지만 Repository Extension과 Repository Extension Impl의 네이밍은 같아야 JPA가 인식한다.
public class TeamRepositoryImpl extends QuerydslRepositorySupport implements TeamRepositoryExtension{
/**
* Creates a new {@link QuerydslRepositorySupport} instance for the given domain type.
*/
public TeamRepositoryImpl() {
super(Team.class);
}
@Override
public Page<Team> findByKeyword(String keyword, Pageable pageable) {
QTeam team = QTeam.team;
// 팀과 연관관계 맺은 지역정보, 태그에서 키워드에 맞는 조건 검색 쿼리
JPQLQuery<Team> query = from(team).where(team.published.isTrue()
.and(team.title.containsIgnoreCase(keyword))
.or(team.tags.any().title.containsIgnoreCase(keyword))
.or(team.zones.any().localNameOfCity.containsIgnoreCase(keyword)))
.leftJoin(team.tags, QTag.tag).fetchJoin()
.leftJoin(team.zones, QZone.zone).fetchJoin()
.distinct();
JPQLQuery<Team> teamJPQLQuery = getQuerydsl().applyPagination(pageable, query);
QueryResults<Team> fetchResults = teamJPQLQuery.fetchResults();
return new PageImpl<>(fetchResults.getResults(), pageable, fetchResults.getTotal());
}
}
Querydsl 사용시 join문에는 fetchJoin을 해야 Lazy 로딩이 발생하지 않는다.
정리
데이터베이스에 관한 많은 부분이 추상화된 JPA와 그를 돕기 위한 QueryDsl은 적절히 사용하지 않으면 성능저하로 이어진다. 찾아오는 객체의 데이터의 조건으로만 찾아올 수 있다면 JPARepository의 Method Naming을 통해, 찾아오려는 객체와 연관관계 객체에 조건을 걸어야할 때에 QueryDsl을 사용해야한다는 걸 기억하자.
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard
https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/
http://querydsl.com/static/querydsl/latest/reference/html/