QueryDSL 이란
SQL, JPQL 등을 코드로 작성할 수 있도록 해주는 빌더 오픈소스 프레임워크이다.
사실, QueryDSL 이 JPA에서만 사용하는 프레임워크로만 알 수도 있지만 공식 사이트를 보면 JPA 뿐만 아니라 SQL, MongoDB, Lucenece 등 다양한 언어에 대해서 서비스를 제공한다.
QueryDSL JPA
QueryDSL JPA 는
- SQL, JPQL 을 코드로 작성할 수 있도록 해주는 빌더 API 이고,
- Entity 클래스와 매핑되는 QClass 라는 객체를 사용해서 쿼리를 실행한다.
QClass 란?
QueryDSL은 컴파일 단계에서 엔티티를 기반으로 QClass를 생성하는데 JPAAnnotationProcessor 가 컴파일 시점에 작동해서 @Entity 등의 어노테이션을 찾아 해당 파일들을 분석해서 QClass를 만든다.
QClass 는 Entity와 형태가 똑같은 Static Class 이다.
QueryDSL은 쿼리를 작성할 때 QClass 를 기반으로 쿼리를 실행한다.
JPQL 이란?
JPA에서 지원하는 다양한 쿼리 방법 중 가장 단순한 조회 방법으로, SQL의 경우에는 DB 테이블을 대상으로 쿼리를 질의하지만, JPQL은 엔티티 객체를 대상으로 쿼리를 질의한다.
QueryDSL JPA를 사용해야 하는 이유
● JPQL은 쿼리를 여전히 문자열로 입력하기 때문에 오타가 발생하거나 관리하기 어렵다.
➡️ QueryDSL은 쿼리를 문자열로서가 아니라 코드를 통해서 작성하기 때문에 오타가 날 확률이 적어지고, 객체 지향적으로 개발할 수 있다.
● JPQL은 컴파일 단계에서 오류를 확인할 수 없고, 런타임 시 해당 쿼리가 실행되어야지만 오류를 확인할 수 있다.
➡️ QueryDSL은 코드로써 작성하기 때문에 컴파일 단계에서도 오류를 빠르게 발견할 수 있다.
예를 들어
회원(member)과 포인트(point)를 조인해서 가져와야 할 때
JPQL 의 경우에는
String jpql = "select * from Member m join Point p on p.member_id = m.id"
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
이런식으로 쿼리를 작성해야 하지만,
QueryDSL을 사용했을 때에는
return jpaQueryFactory
.from(member)
.join(member.point, point)
.fetch();
이런식으로 코드를 사용해서 나타낼 수 있다.
오타가 나더라도 컴파일 단계에서 오류를 확인할 수 있고, 코드로서 작성하기 때문에 더욱 객체 지향적으로 개발할 수 있다.
build.gradle 에 QueryDSL 세팅하기
dependencies {
...
implementation 'com.querydsl:querydsl-jpa:jakarta'
implementation 'com.querydsl:querydsl-apt'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}
def querydslSrcDir = 'src/main/generated'
sourceSets {
main {
java {
srcDirs += [ querydslSrcDir ]
}
}
}
compileJava {
options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}
tasks.withType(JavaCompile) {
options.generatedSourceOutputDirectory = file(querydslSrcDir)
}
clean {
delete file(querydslSrcDir)
}
dependencies
implementation 'com.querydsl:querydsl-jpa:jakarta'
➡️ QueryDSL을 사용하기 위한 라이브러리
➡️ QuerydslRepositorySupport, QuerydslPredicateExecutor 등 QueryDSL 내부 클래스는 사용 가능하지만, 실제로 쿼리를 위해 사용되는 QClass는 생성되지 않는다.
implementation 'com.querydsl:querydsl-apt'
➡️ QClass를 생성하기 위한 라이브러리
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
➡️ QueryDSL JPa의 경우에는 QClass를 활용해서 동작하는데 @Entity 어노테이션을 선언한 클래스를 탐색하고, QClass를 생성한다.
QClass를 생성하는 방법에는 크게 두 가지로 나뉜다.
1. com.ewerk.gradle.plugins.querydsl
-> 2018년 이후 업데이트 된 적이 없다.
2. annotationProcessor
이번의 경우에는 2번을 선택하였다.
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
➡️ 이클립스 재단으로 자바가 이관되면서 java -> jakarta 로 이름이 변경되었다.
➡️ Q파일을 찾지 못해서 발생하는 오류인 java.lang.NoClassDefFoundError (javax.annotation.Entity / javax.annotation.Generated) 에러 발생 대응을 위한 코드 이다.
sourceSets
def querydslSrcDir = 'src/main/generated'
sourceSets {
main {
java {
srcDirs += [ querydslSrcDir ]
}
}
}
gradle build 시 QClass 소스도 함께 build 하기 위해서 sourceSets에 해당 위치를 추가해준다.
compileJava
compileJava {
options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}
해당 내용을 명시해주지 않으면 Q 파일 내 Generated 를 import 할 때 자바 9 에만 있는 import javax.annotation.processing.Generated 로 import 해준다.
그렇기 때문에 다른 버전에서도 사용할 수 있도록 java.annotation.Generated 로 import 하도록 설정하는 코드이다.
task.withType
tasks.withType(JavaCompile) {
options.generatedSourceOutputDirectory = file(querydslSrcDir)
}
➡️ annotationProcessorGeneratedSourcesDirectory 를 사용할 수도 있는데, Gradle 공식 문서에 해당 기능은 deprecated 되었고, 추후 major version 에선 삭제할 것이라고 되어있기 때문에 generatedSourceOutputDirectory 를 사용한다.
➡️ generatedSourceOutputDirectory 는 annotation processors에서 생성한 소스 파일을 저장할 디렉토리를 지정해준다.
➡️ 이 코드를 통해 위에서 선언한 querydslSrcDir 변수의 src/main/generated 에다가 annotation processors가 만든 QClass들을 저장해준다.
clean
clean {
// clean 실행 시 생성된 QClass 삭제
delete file(querydslSrcDir)
}
➡️ build clean 시에 생성되었던 QClass를 모두 삭제한다.
실행하기
- 만들어진 QClass 삭제하기 : Gradle > build > clean
- QClass 만들기 : Gradle > build > compileJava
src > main > generated 내에 QClass 가 만들어졌으면 queryDSL 설정이 정상적으로 된 것이다.
QueryDSL 사용하기
(1) QuerydslConfig 파일 설정하기
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
@PersistenceContext
- EntityManager를 빈으로 주입할 때 사용하는 어노테이션
- 빈으로 주입받을 때 EntityManager의 경우 @Autowired가 아니라 해당 어노테이션으로 주입
JPAQueryFactory를 Bean으로 등록해서 프로젝트 전역에서 사용할 수 있도록 한다.
(2) QueryDSL 사용 Repositorty 구조 잡기
Spring에서 QueryDSL을 사용하는 방법에는 총 3가지 방법이 있다. (Spring Data Jpa Custom Repository / QueryRepositorySupport / JPAQueryFactory) 이 있다.
1. Spring Data JPA Custom Repository 사용
Spring에서 QueryDSL과 JPARepository를 함께 사용하려면 각 역할 별 파일을 두 개 만들어주거나, 하나의 파일이 두 개의 의존성을 가져야 한다. 이러한 경우 Spring Data JPA 에서 제공하는 ⭐Spring Data Custom Repository ⭐ 를 사용하면 된다.
해당 방식을 통해서 CustomRepository를 JPARepository 상속 클래스에서 함께 상속 받아서 사용할 수 있다.
구현된 Repository 구조는 아래와 같다.
Repository(interface) 가 JpaRepository(interface), CustomRepository(interface)를 다중 상속 받고
➡️ CustomRepository 인터페이스에 선언되어 있는 메서드에 대한 구현은 RepositoryImpl 에서 한다.
➡️ 그리고 사용자는 Repository 인터페이스를 DI 받아서 사용한다.
❓RepositoryImpl 은 Repository를 직접적으로 구현하지 않는데 어떻게 Repository에서 사용가능할까 ❓
💡 사용자 정의 구현 클래스인 경우 JPA 가 파일명이 repository interface 이름 + Impl 인 클래스를 찾아서 interface에 JpaRepository를 Injection 할 때에 Impl 객체를 삽입해주기 때문에 사용할 수 있다,
따라서 반드시 구현체 파일명이 interface 명 + Impl 이여야 한다.
위와 같은 방식의 경우 총 3개의 repository 관련 파일이 만들어 진다.
첫번째, Repository 파일 (interface)
JpaRepository 와 Custom interface를 상속한 파일 ⭐ 사용자는 해당 파일을 DI 받아서 사용한다.⭐
public interface ContentRepository extends JpaRepository<Content, Long>, ContentRepositoryCustom {
}
두번쨰, RepositoryCustom 파일 (interface)
QueryDSL로 커스텀해서 사용할 메서드 선언하는 파일
public interface ContentRepositoryCustom {
}
세번째, RepositoryImpl 파일 (class)
RepositoryCustom interface 에 선언한 메서드 구현하는 파일
@RequiredArgsConstructor
public class ContentRepositoryImpl implements ContentRepositoryCustom {
private final JPAQueryFactory queryFactory;
}
단점 : QueryDSL을 사용하는 경우 만들어야 하는 파일이 엄청 많아지고, 그에 따른 관리 포인트가 늘어난다.
2. QueryRepositorySupport 상속하기
첫번째 방식처럼 여러 파일을 만들어서 상속받는 방식이 아니라 QuerydslRepositorySupport만 상속하여 구조를 단순화 하는 방식이다.
@Repository
public class ContentRepositorySupport extends QuerydslRepositorySupport {
private final JPAQueryFactory queryFactory;
public ContentRepositorySupport(JPAQueryFactory queryFactory) {
super(Content.class);
this.queryFactory = queryFactory;
}
}
단점
- 매번 Support를 상속 받고, super 생성자에 Entity를 등록해줘야 한다.
- Entity를 save, delete 하는 repository와 query하는 repository가 나뉘어 진다.
3. JPAQueryFactory 사용하기
JPAQueryFactory만 있으면 QueryDSL 기능을 모두 사용할 수 있다.
2번째 방식처럼 super와 생성자를 사용하지 않아도 된다. 사용하고자 하는 QClass를 import 해서 사용하는 방식
@RequiredArgsConstructor
@Repository
public class ContentQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Content> findByTitle(String title) {
return queryFactory
.selectFrom(content)
.where(content.title.eq(title))
.fetch();
}
}
단점
- Entity 를 save, delete 하는 repository 와 query 하는 Repository 가 나뉘어 진다.
'SpringBoot' 카테고리의 다른 글
Spring WebSocket (0) | 2024.07.16 |
---|---|
JPA Entity에 @Setter를 지양하는 이유 (0) | 2024.06.29 |
SpringSecurity Bcrypt 를 이용한 비밀번호 암호화 (0) | 2024.06.26 |
@Controller와 @RestController 차이점 (0) | 2024.06.25 |
RedirectAttributes (0) | 2024.05.02 |