들어가기
이 글은 Spring boot(3.0 이상), JPA에 QueryDSL을 사용하면서 새로 적용해 봤던 내용들을 정리한 것이다.
목차는 아래와 같다.
2. Spring boot에서 QueryDSL을 사용하기 위한 설정(Java 17, Spring boot 3.x.x를 기준)
본론
1. QueryDSL은 무엇이며 왜 사용하는가?
JPA를 사용할 때, 편의성을 위해 Spring Data JPA를 종종 사용하였다. 하지만 복잡한 쿼리를 작성해야 할 때면 @Query 어노테이션을 통해 JPQL을 직접 작성하곤 하였다. 로직이 복잡할수록 쿼리 문자열은 길어지며 가독성은 떨어진다. 문자열 중 잘못된 부분이 있을 경우, 런타임 시점에 에러가 발생하기 때문에 사전에 확인하기가 어렵다.
이런 문제들을 해결해주는 것이 QueryDSL이란 프레임워크이다.
QueryDSL은 정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해 준다.
아래의 예시를 봐보자.
public List<Article> findByTitleUnder3() {
BooleanExpression expression = article.title.length().loe(3);
return jpaQueryFactory.selectFrom(article).where(expression).fetch();
}
위의 코드는 단순히 메서드를 호출하는 것뿐이다. 하지만 이는 아래의 SQL문과 일치한다.
SELECT * FROM ARTICLE WHERE LENGTH(TITLE) <= 3;
이렇듯 문자열 형태의 SQL 쿼리를 문자열이 아닌 코드형태로 작성하기 때문에 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
또한 IDE의 도움을 받을 수 있으며 'WHERE'와 같은 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
또한 런타임 시에 주어지는 값에 따라 쿼리가 달라지는 동적 쿼리 작성에도 유용하다.
이제 이러한 QueryDSL을 Spring boot, JPA와 함께 사용해 볼 것이다.
참고로 QueryDSL을 통해 엔티티 값을 조회할 때, JPQL을 사용하는 것이므로 영속성 컨텍스트에 엔티티를 보관한다.
그렇기에 'Fetch Join'으로 연관된 테이블의 데이터를 함께 가져오는 것 또한 가능하다.
2. Spring boot에서 QueryDSL을 사용하기 위한 설정
jave version : 17
spring boot : 3.2.3
build.gradle
dependencies {
...
//Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
...
}
// QueryDSL
def querydslDir = 'src/main/generated'
//Q 클래스 생성 위치
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
tasks.withType(JavaCompile) {
options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
clean.doLast {
file(querydslDir).deleteDir()
}
QueryDSL로 쿼리를 작성할 때, 엔티티를 참고하여 생성된 Q 클래스를 사용하여 Type-Safe 한 방식으로 작성한다. sourceSets를 통해 프로젝트 내 Q 클래스 생성 위치를 지정할 수 있다.
아래와 같이 JPA를 사용할 때처럼 엔티티를 작성하면, 컴파일 시 Q클래스가 자동으로 생성, 수정된다.
@Entity
@Table(name = "article_table")
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "title")
private String title;
@Column(name = "content")
private String content;
@Column(name = "create_date")
private Date newDate;
public Article(String title, String content, Date newDate) {
this.title = title;
this.content = content;
this.newDate = newDate;
}
}
이후 Config 파일을 설정해 줌으로써 프로젝트 전역에서 JPAQueryFactory를 통해 QueryDsl을 사용할 수 있다.
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
그다음 @Repository를 통해 Bean으로 등록 후, 작성한 메서드를 사용할 수 있다.
...
import static khds.ecommerce.QArticle.article;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
...
@Repository
public class ArticleRepository {
private final JPAQueryFactory jpaQueryFactory;
public ArticleRepository(JPAQueryFactory jpaQueryFactory) {
this.jpaQueryFactory = jpaQueryFactory;
}
@Override
public List<Article> findByTitleUnder3() {
BooleanExpression expression = article.title.length().loe(3);
return jpaQueryFactory.selectFrom(article).where(expression).fetch();
}
간단한 사용예시
//Controller
@Autowired
private ArticleRepository articleRepository;
@GetMapping("/find")
public List<Article> find(){
List<Article> articles = articleRepository.findByTitleUnder3();
return articles;
}
3. Spring Data JPA와 같이 사용하는 법
만약 Spring Data JPA와 동시에 사용 가능한 Repository를 만들고 싶다면?
3개의 파일 작성이 필요하다. 우선 QueryDSL로 작성할 메서드를 담은 인터페이스를 정의한다.
이를 상속받는 구현체에 주요 로직을 작성할 것이다.
public interface ArticleCustomRepository {
List<Article> findByTitleUnder10();
}
그리고 일반적인 Spring Data JPA를 사용하기 위한 interface 정의를 정의한 후, QueryDSL을 사용하는 인터페이스를 상속받는다.
public interface ArticleRepository extends JpaRepository<Article, Long>, ArticleCustomRepository {
}
아래는 QueryDSL을 작성한 클래스이며 ArticleCustomRepository를 구현하고 있다.
...
import static khds.ecommerce.QArticle.article;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
...
@Repository
public class ArticleCustomRepositoryImpl implements ArticleCustomRepository {
private final JPAQueryFactory jpaQueryFactory;
public ArticleCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
this.jpaQueryFactory = jpaQueryFactory;
}
@Override
public List<Article> findByTitleUnder3() {
BooleanExpression expression = article.title.length().loe(3);
return jpaQueryFactory.selectFrom(article).where(expression).fetch();
}
}
작성한 메서드를 사용하는 간단한 API를 호출해 보자.
@RestController
@RequestMapping("/api")
public class TestController {
@Autowired
private ArticleRepository articleRepository;
//Spring Data JPA 메서드 사용
@GetMapping("/find1")
public List<Article> test1(){
List<Article> articles = articleRepository.findAll();
return articles;
}
//작성한 QueryDSL 메서드 사용
@GetMapping("/find2")
public List<Article> test2(){
List<Article> articles = articleRepository.findByTitleUnder3();
return articles;
}
}
findAll()
findByTitleUnder3()
4. DTO 객체로 값을 받는 법
복잡한 연산을 하게 되면, 엔티티가 아닌 원하는 값들로 이루어진 DTO 객체로 받고 싶을 수 있다.
그럴 경우엔 아래와 같이 표현할 수 있다.
...
import static khds.ecommerce.QArticle.article;
...
//ArticleJPAResponse는 DTO
@Override
public List<ArticleJPAResponse> findDtoByTitleUnder3() {
BooleanExpression expression = article.title.length().loe(3);
return jpaQueryFactory.select(
Projections.constructor(ArticleJPAResponse.class, article.id, article.title,
article.content))
.from(article).where(expression).fetch();
} // constructor는 생성자, field를 쓸 경우 필드에 직접 주입
혹은 DTO 객체에 아래와 같은 어노테이션을 생성자에 추가해 주면 DTO도 Q 객체로 생성된다.
@Getter
@NoArgsConstructor
public class ArticleJPAResponse {
private Long id;
private String title;
private String content;
@QueryProjection
public ArticleJPAResponse(Long id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}
}
아래와 같이 DTO 생성자를 통해 사용 가능하다.
...
import static khds.ecommerce.QArticle.article;
import khds.ecommerce.QArticleJPAResponse;
...
@Override
public List<ArticleJPAResponse> findDtoByTitleUnder3() {
BooleanExpression expression = article.title.length().loe(3);
return jpaQueryFactory.select(
new QArticleJPAResponse(article.id, article.title, article.content))
.from(article).where(expression).fetch();
}
간단한 테스트 코드를 실행해 보자.
@GetMapping("/find3")
public List<ArticleJPAResponse> find(){
List<ArticleJPAResponse> articles = articleRepository.findDtoByTitleUnder3();
return articles;
}
주의할 점은 DTO로 조회하게 되면 엔티티 조회와 다르게 영속성 컨텍스트에 등록이 안 되는 점을 기억하길 바란다.
참고
https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/
https://doing7.tistory.com/129
https://velog.io/@jinyeong-afk/%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-QueryDSL%EC%9D%B4%EB%9E%80
https://velog.io/@guns95/QueryDsl-Where-%EB%8B%A4%EC%A4%91-%EC%A1%B0%EA%B1%B4
'JPA' 카테고리의 다른 글
JPA를 활용한 연관관계 데이터 저장: getReferenceById()를 쓰는 이유 (0) | 2024.07.29 |
---|---|
JPA 영속성 컨텍스트는 어떻게 사용되는가 (0) | 2024.01.14 |
JPA 양방향 연관관계 일 때의 저장 및 연관관계 편의 메서드 (0) | 2021.12.14 |
JPA column에 list를 넣는 방법(String 변환, @ElementCollection) (0) | 2021.12.11 |
스프링부트 With JPA - mysql 연동 (0) | 2021.12.06 |