📌 사용 계기
데이터 검색 성능 개선을 해보는 프로젝트에서 검색 상태에 따라 다른 JPQL문을 생성할 필요가 있어 QueryDSL을 적용하게 되었다.
📍QueryDSL이란?
JPQL문을 문자열이 아닌 자바 코드로 작성할 수 있도록 빌더 역할을 하는 오픈 소스
📍QueryDSL 특징
- 쿼리를 코드로 작성해 컴파일 시 오류 발견이 가능하다.
- 코드 기반에 단순하여 사용이 쉽고 가독성이 좋음
- 자동 완성 등 IDE의 도움 가능
- 동적 쿼리 구현 가능
📍적용 방법
- build.gradle
buildscript { // gradle로 task를 수행할 때에 사용되는 설정
ext { // build.gradle에서 사용하는 전역 변수 설정
queryDslVersion = "5.0.0" // 변수 queryDslVersion에 5.0.0 대입
}
}
plugins { // gradle task의 집합
//....//
id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10' // querydsl plugin 적용
}
dependencies {
//....//
// querydsl 의존성 추가
// buildscript에서 적용한 변수 queryDslVersion이 여기에 적용
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
}
// 변수 선언, 그 변수에 폴더 경로 저장
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true // JPA 사용여부 설정
querydslSourcesDir = querydslDir // 사용할 경로 설정
}
sourceSets { // build시 사용할 sourceSet 추가
main.java.srcDir querydslDir
}
compileQuerydsl { // queryDSL 컴파일시 사용할 옵션 설정
options.annotationProcessorPath = configurations.querydsl
}
configurations { // build 옵션
compileOnly {
extendsFrom annotationProcessor
}
// querydsl이 compileClasspath를 상속하도록 설정
querydsl.extendsFrom compileClasspath
}
- config
@Configuration
public class QuerydslConfig {
@PersistenceContext
// @PersistenceContext로 주입받은 Entity Manager은 Proxy로 감싸진다.
// EntityManager 호출시 마다 Proxy를 통해 EntityManager을 생성하여 Thread-Safe를 보장
private EntityManager entityManager;
@Bean
// JPAQuery를 생성해주는 factory클래스
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
- Q-Class
QueryDSL에서는 Entity로 설정된 클래스에 Q모델의 쿼리 타입 클래스를 미리 생성하고 메타데이터로 사용하여 쿼리를 메소드 기반으로 작성한다.
생성하는 방법은 Tool Window Bar (사이드에 보이는 메뉴명)에서 Gradle > other > compileJava 클릭
📍 QueryDSL 구현 방법 (총 3가지)
- 방법 1 ) QuerydslRepositorySupport를 상속 받아 사용하는 방법
- 구현코드
- JpaRepository를 상속 받는 ProductRepository 인터페이스 하나
import com.example.showmethemany.domain.Products; import org.springframework.data.jpa.repository.JpaRepository; public interface ProductRepository extends JpaRepository<Products, Long> { }
- QuerydslRepositorySupport를 상속하고 Super생성자에 Entity를 지정해서 넣은 클래스 하나(ProductRepositorySupport)
import com.example.showmethemany.domain.Products; import com.querydsl.jpa.impl.JPAQueryFactory; import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; import org.springframework.stereotype.Repository; import static com.example.showmethemany.domain.QProducts.products; @Repository public class ProductRepositorySupport extends QuerydslRepositorySupport { private final JPAQueryFactory queryFactory; public ProductRepositorySupport(JPAQueryFactory queryFactory) { super(Products.class); this.queryFactory = queryFactory; } public Products findByProductId (Long productId) { return queryFactory.select(products) .from(products) .where(products.id.eq(productId)) .fetchOne(); } }
- JpaRepository를 상속 받는 ProductRepository 인터페이스 하나
- 테스트코드
import com.example.showmethemany.domain.Products; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class ProductRepositorySupportTest { @Autowired ProductRepository productRepository; @Autowired ProductRepositorySupport productRepositorySupport; @Test @DisplayName("QueryDSL Support 상속 구현 방식 테스트") void findByProductId() { //given Long productId = 1L; //when Products products = productRepositorySupport.findByProductId(productId); //then assertEquals(26910, products.getPrice()); } }
- 결과
그러나 이 구현 방법의 문제점은
QuerydslRepositorySupport(코드상 ProductRepositorySupport)와 JpaRepository(코드상 ProductRepository)가 기능을 나눠 가져 항상 2개를 의존성으로 받아야 하는 문제가 있다
- 구현코드
- 방법 2 ) CustomRepository 상속과 구현이 필요한 방법
- 구현코드
- CustomRepository 인터페이스 하나
import com.example.showmethemany.domain.Product public interface ProductRepositoryCustom { Products findByProductId (Long productId); }
- CustomRepository 의 구현체 하나
import com.example.showmethemany.domain.Products; import com.querydsl.jpa.impl.JPAQueryFactory; import javax.persistence.EntityManager; import static com.example.showmethemany.domain.QProducts.products; public class ProductRepositoryImpl implements ProductRepositoryCustom { private final JPAQueryFactory queryFactory; public ProductRepositoryImpl(EntityManager em) { this.queryFactory = new JPAQueryFactory(em); } @Override public Products findByProductId(Long productId) { return queryFactory.select(products) .from(products) .where(products.id.eq(productId)) .fetchOne(); } }
- JpaRepository 에 CustomRepository를 상속함으로써 CustomRepositoryImpl도 사용 가능
import com.example.showmethemany.domain.Products; import org.springframework.data.jpa.repository.JpaRepository; public interface ProductRepository extends JpaRepository<Products, Long>, ProductRepositoryCustom { }
- CustomRepository 인터페이스 하나
- 테스트코드
@SpringBootTest class ProductRepositoryTest { @Autowired ProductRepository productRepository; @Test @DisplayName("QueryDSL CustomRepository 상속 방식 테스트") void findByProductId() { //given Long productId = 1L; //when Products products = productRepository.findByProductId(productId); //then assertEquals(26910, products.getPrice()); } }
- 결과
그러나 이 구현 방법의 문제점은
하나의 Repository당 interface하나와 Impl하나 총 2개를 더 만들어줘야 한다. (총 3개)
- 구현코드
- 방법 3 ) 상속과 구현 없이 가능한 방법
- 구현코드
- JPAQueryFactory를 주입 받은 Repository class 하나
JPAQueryFactory만 주입 받으면 QueryDSL을 사용할 수 있고 특정 Entity 지정하지 않아도 여러 Entity를 대상으로 사용할 수 있다.
import com.example.showmethemany.domain.Products; import com.querydsl.jpa.impl.JPAQueryFactory; import org.springframework.stereotype.Repository; import static com.example.showmethemany.domain.QProducts.products; @Repository public class ProductRepository { private final JPAQueryFactory queryFactory; public ProductRepository(JPAQueryFactory queryFactory) { this.queryFactory = queryFactory; } public Products findByProductId (Long productId) { return queryFactory.select(products) .from(products) .where(products.id.eq(productId)) .fetchOne(); } }
- JPAQueryFactory를 주입 받은 Repository class 하나
- 테스트코드
@SpringBootTest class ProductRepositoryTest { @Autowired private ProductRepository productRepository; @Test @DisplayName("QueryDSL 상속/구현 없는 방식 테스트") void findById() { //given Long productId = 1L; //when Products products = productRepository.findByProductId(productId); //then assertEquals(26910, products.getPrice()); } }
- 결과
이 구현 방법은 다른 경우보다 부담이 덜된다는 장점은 있지만 기본 JpaRepository와는 별개가 되기 때문에 기존 Repository의 메서드를 참조할 수 없게 된다. QueryDSL만 사용하게 되면 JPA로 간단히 사용했던 메소드를 구현해 줘야 한다는 단점이 있다. 그래서 프로젝트에는 QueryDSL과 Spring Data JPA를 같이 쓰는 방향으로 진행했다.
- 구현코드
'Programming > Spring' 카테고리의 다른 글
스프링 핵심 원리 - 객체 지향 설계와 스프링 (0) | 2023.03.16 |
---|---|
WebRTC 시그널링 서버 (Spring Boot로 구현) (0) | 2023.01.12 |
Query DSL (0) | 2022.12.15 |
스프링 부트(Spring Boot) 다운 그레이드 (0) | 2022.12.07 |
ORM / JPA / Hibernate / Spring Data JPA (0) | 2022.12.05 |