Querydsl 학습
스프링부트와 db를 사용한다면 대부분 JPA를 사용할 것이고 인터페이스에 쿼리를 사용했을 것이고, 더 나아가 Querydsl를 쓰는 사람들도 있으며 나 또한 포함되어 있다.
그런데 문득 왜 굳이 Querydsl를 쓰는 것이고 Querydsl를 쓰는 것이 효율적이라고 하는데 ‘왜’라는 것은 전혀 모르고 있었기에 깊이 한번 알아보려고 한다.
QueryDSL?
QueryDSL 은 SQL, JPQL 등 쿼리를 코드로 작성할 수 있게 만드는 프레임워크이다. JPA 뿐만 아니라 SQL, Mongodb, Lucence 등 다양한 언어에 대해서 서비스를 하고 있다.
Why QueryDSL?
보통 쿼리는 DB에서는 SQL방식으로, JAVA에서는 JPQL방식으로 객체(ex-엔티티)를 쿼리를 호출하는 객체지향 쿼리이다.
ex)
1
2
@Query("select m from Member m where m.age > 18")
//'m' 은 별칭이며 'Member'는 테이블명이 아닌 엔티티 객체 이름이기에 조금 신경써야 할 부분
SQL로 변환시 select * from Member(혹은 다른 테이블명) where age > 18;가 되어 나이가 18세를 초과되는 회원 데이터들만 조회가 되는 쿼리가 될 것이다.
그래서 굳이 QueryDSL를 사용해야 하나, JPQL를 그냥 사용하면 되지 않나 라는 의문이 들 수도 있는데 JPQL에는 명확한 단점이 존재한다.
JPQL 단점
쿼리를 문자열로 사용한다. -> 쿼리, 즉 조건이 복잡해져 여러 종류의 속성이 많아져 문자열이 길어진다면 오탈자가 생길 가능성과 관리의 어려움이 크다.
컴파일 단계에서 오류를 확인할 수 없고, 런타임 시 해당 쿼리가 실행되어야지만 오류를 확인할 수 있다.
반면, QueryDSL 은 코드로서 작성하기 때문에 컴파일 단계에서도 오류를 빠르게 발견할 수 있다.
QueryDSL 설정
내가 사용한 프레임워크/라이브러리의 환경이 이러하다.
- Spring Boot : 3.1.3
- JAVA : 17
- Gradle : 8.2.1
- QueryDSL : 5.0.0
build.gradle 의존성 및 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
buildscript{
ext{
queryDslVersion = "5.0.0"
}
}
dependencies {
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"
}
def querydslDir = "$buildDir/generated/querydsl"
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
tasks.withType(JavaCompile) {
options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
clean.doLast {
file(querydslDir).deleteDir()
}
자바 11 버전을 사용하다가 17버전으로 넘어가면서 추가되거나 변경되는 코드들 제법있어서 만약 환경이 바뀔 때마다 체크를 해줘야 할 것 같다.
QueryDSL 어떻게 쓰나?
QClASS 생성
QueryDSL은 엔티티 객체를 기반으로 QCLASS를 생성해, 이 클래스를을 중심으로 쿼리를 작성한다.
이 클래스들을 생성하기 위해서는 Gradle 옵션의 other 탭의 compileJava를 실행하면 QueryDSL은 @Entity어노테이션이 붙은 엔티티 객체 기반들로 QCLASS가 생성된다.
Repository와 QueryDSL 연동하여 사용
QueryDSL를 사용할 인터페이스 선언
“인터페이스 이름”+Impl 이름으로 클래스를 선언, 또한 QueryRepositorySupport라는 부모 클래스를 상속받도록 한다.
- 여기서 Impl를 선언할 때 상속을 먼저 받고 인터페이스를 받아야 한다. 그리고 QueryRepositorySupport 를 상속받으면 QueryDSL를 사용할 수 없는데 생성자에 super(엔티티.class)를 호출하면 가능하다.
- 여기서 Impl를 선언할 때 상속을 먼저 받고 인터페이스를 받아야 한다. 그리고 QueryRepositorySupport 를 상속받으면 QueryDSL를 사용할 수 없는데 생성자에 super(엔티티.class)를 호출하면 가능하다.
예시 코드
1
2
3
4
public interface BoardSearch {
Page<Board> searchAll(String[] types, String keyword, Pageable pageable);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {
public BoardSearchImpl(){
super(Board.class);
}
@Override
public Page<Board> searchAll(String[] types, String keyword, Pageable pageable){
QBoard board = QBoard.board;
JPQLQuery query = from(board); // select * from board
if((types != null && types.length > 0) && keyword != null){ //where 쿼리 조건 만들어주기
BooleanBuilder booleanBuilder = new BooleanBuilder();
for(String type : types){
switch (type){
case "t":
booleanBuilder.or(board.title.contains(keyword));
break;
case "c":
booleanBuilder.or(board.content.contains(keyword));
break;
case "w":
booleanBuilder.or(board.user.contains(keyword));
break;
}
}
query.where(booleanBuilder); //query에 위에 만든 조건 넣기
}
query.where(board.bno.gt(0L)); //id 값 > 0
this.getQuerydsl().applyPagination(pageable,query);
List<Board> list = query.fetch(); // fetch() -> JPQLQuery 실행
Long count = query.fetchCount(); // fetchCount() -> count 쿼리 실행
return new PageImpl<>(list, pageable, count);
}
}
JPQLQuery는 @Query로 작성했던 JPQL을 코드를 통해서 생성할 수 있게 한다. 이를 통해서 where이나group by 혹은 조인 처리 등이 가능하다.
또한 위의 getQuerydsl().applyPagination() 코드처럼 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 해주는 기능을 가지고 있다.
이 코드들을 실행하면 아래와 같은 쿼리들이 출력되면서 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Hibernate:
select
b1_0.bno,
b1_0.content,
b1_0.moddate,
b1_0.regdate,
b1_0.title,
b1_0.user
from
board b1_0
where
b1_0.title like ? escape '!'
and b1_0.bno>?
order by
b1_0.bno desc limit ?,
?
Hibernate:
select
count(b1_0.bno)
from
board b1_0
where
b1_0.title like ? escape '!'
and b1_0.bno>?
QueryDSL 그럼 마스터?
여태 QueryDSL를 사용하면서 이 코드들은 왜 입력하며, 어떠한 기능을 가지고 있는지 자세히 모르고 있어 좀 더 이해 할 수 있었지만, 현재 몇가지 쓰고 있는 코드들을 계속 써야 하나 의문이 들기도 하였다.
그 의문 중 하나 뽑자면 위에 쓴 QuerydslRepositorySupport는 페이징을 QueryDSL로 편리하게 사용할 수 있으며 몇 가지 장점들이 있지만, QueryDSL 3.x.버전을 대상으로 만들었기에 현재는 5.x버전을 쓰고 최신 버전으로 나온 기술들을 쓰지 못한다는 점과 많은 부분이 불편하다는 평가가 현재 많다.
또한 BooleanBuilder으로 쿼리 where 조건을 작성하고 있는데, 조건이 더 많아지면 보기 어렵다는 점이 있다고 한다.
이외에도 내가 아직 쓰지도 못한 기술들이 있고, 위에 문제들로 인해 사람들은 개선해서 사용하고 있기에 나 또한 개선하거나 새로운 기술들을 학습하고 구현해 봐야겠다고 생각한다.