티스토리 뷰
안녕하세요. Fulfillment Engineering 팀의 입사한 지 1년이 얼마 지나지 않은 싱싱한(?) 주니어 개발자 백정현입니다.
최근 들어 JAVA를 기반으로 한 Spring boot + JPA 또는 Spring Data JPA를 이용한 프로젝트가 많이 보입니다.
JPA는 쿼리를 지원하는 다양한 방법들이 있는데, 그중 QueryDSL에 대해서 살포시 찔러보도록 하겠습니다.
QueryDSL은 무엇인가요?
QueryDSL은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해 주는 오픈소스 프레임워크입니다.
쿼리를 문자열로 작성하거나 작성하는 것이 아닌, QueryDSL이 제공하는 Fluent API를 이용해 코드 작성의 형식으로 쿼리를 생성할 수 있게 도와줍니다.
Gradle 설정
QueryDSL은 JPA 표준이 아니기 때문에 별도로 라이브러리를 추가해주어야 합니다.
저는 다음과 같이 설정해주었습니다.
이번 글에서 QueryDSL 5.0.0을 사용하고자 하는 이유는 나중에 아래에서 설명드리겠습니다. :)
// QueryDSL 5.0이상 부터는 아래의 옵션을 추가해주시면 됩니다.
plugins {
...
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
...
}
...
dependencies {
// querydsl 추가
implementation "com.querydsl:querydsl-jpa:5.0.0"
implementation "com.querydsl:querydsl-apt:5.0.0"
...
}
// Qtype 생성 경로
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
빌드 설정도 끝이 나고 나니 다음과 같은 의문이 듭니다.
"별도 라이브러리도 추가해야 하니 번거로운데.. 위에서 이야기한 쿼리를 지원하는 다른 방법을 쓰는 건 어떤가요?"
네, 실제로 사용하는 JPA에서 쿼리를 지원하는 방식은 크게 다음과 같이 정리됩니다.
- JPQL
- Criteria api
- Native Query
- QueryDSL
그럼에도 불구하고 QueryDSL가 쓰이는 장점들을 예시를 통해 확인해봅시다.
사전 준비
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
private String team;
public Member(String username) {
this.username = username;
}
public Member(String username, int age, String team) {
this.username = username;
this.age = age;
this.team = team;
}
}
예시로 사용하고자 하는 Member Entity를 준비합니다.
QueryDSL로 쿼리를 작성할 때, QType을 이용해 쿼리를 Type-Safe 하게 작성할 수 있습니다.
QType을 만들어 봅시다.
Qtype 파일을 만들기 위해 Gradle > Tasks > other > complieQuerydsl을 클릭해줍니다
그러면 오른쪽 사진처럼 build > generated > 경로에 Q{Entity명}.java 로 Qtype이 생성됩니다.
※ 경로는 Gradle에 querydslDir로 설정해두었습니다.
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
String FE = "Fulfillment Engineering";
String IE = "Item Engineering";
Member member1 = new Member("BaekJungHyun", 28, FE);
Member member2 = new Member("Employee1", 20, FE);
Member member3 = new Member("Employee2", 30, IE);
Member member4 = new Member("Employee3", 40, IE);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
QueryDSL을 실행해보기 위해 Entity들에 대한 값들을 테스트 코드 이전에 넣어주는 코드를 작성했습니다.
예시 1) 컴파일 시점에 타입 체크가 가능합니다.
@Test
public void jpqlTest() {
String queryString =
"select m from Member m " +
"where m.username = :username";
Member findMember = em.createQuery(queryString, Member.class)
.setParameter("username", "BaekJungHyun")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("BaekJungHyun");
}
@Test
public void jpqlTest() {
String queryString =
"select m from Member m " +
"where mmmm.username = :username"; // 수정한 위치 (m -> mmmm)
Member findMember = em.createQuery(queryString, Member.class)
.setParameter("username", "BaekJungHyun")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("BaekJungHyun");
}
위 코드는 정상 동작하는 JPQL의 예시입니다.
여기서 아래쪽 코드처럼 JPQL을 일부러 오동작시키도록 수정해보았습니다.
정상적으로(?) 오동작하는 것을 확인할 수 있었습니다.
다만 여기서 주목할 점은 Test 하는 메서드를 직접적으로 실행했을 때 오동작이 나타난다는 점입니다.
@Test
public void querydslTest() {
QMember m = QMember.member;
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("BaekJungHyun"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("BaekJungHyun");
}
@Test
public void querydslTest() {
QMember m = QMember.member;
Member findMember = queryFactory
.select(m)
.from(m)
.where(mmmm.username.eq("BaekJungHyun")) // 수정한 위치 (m -> mmmm)
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("BaekJungHyun");
}
이번에는 QueryDSL로 구성된 테스트 케이스를 고장 내고 나서 컴파일을 시켜보았습니다.
JPQL처럼 문자가 아닌 코드로 쿼리가 작성되어, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다는 것을 확인했습니다.
예시 2) 동적 쿼리를 직관적으로 확인하기 쉽다.
@Test
public void querydslTest2() {
QMember m = QMember.member;
String usernameCondition = "BaekJungHyun";
int ageCondition = 25;
List<Member> findMember = queryFactory
.select(m)
.from(m)
.where(usernameEq(usernameCondition), ageGoe(ageCondition))
.fetch();
assertThat(findMember.size()).isEqualTo(3);
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond != null ?member.username.eq(usernameCond) : null;
}
private BooleanExpression ageGoe(Integer ageCond) {
return ageCond != null ?member.age.goe(ageCond) : null;
}
@Test
public void querydslTest2() {
QMember m = QMember.member;
String usernameCondition = null;
int ageCondition = 25;
List<Member> findMember = queryFactory
.select(m)
.from(m)
.where(usernameEq(usernameCondition), ageGoe(ageCondition))
.fetch();
assertThat(findMember.size()).isEqualTo(3);
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageGoe(Integer ageCond) {
return ageCond != null ? member.age.goe(ageCond) : null;
}
동적 쿼리를 위해 Where를 사용해서 나이는 25세보다 많다는 조건으로 공통되지만,
왼쪽은 nameCondition에 어떠한 값이 들어간 상태이고, 오른쪽에는 null값이 들어가는 상태로 Select 하는 쿼리입니다.
값이 들어가 있는 상태를 가진 곳에서는 where 조건에 username에 대해서 조건으로 처리하고,
null값이 들어간 곳에서는 쿼리문을 살펴보았을 때 username에 대해 조건이 빠진 것을 확인할 수 있습니다.
QueryDSL에서는 이렇게 값이 null 처리가 되어있을 때를 판단해서 동적으로 쿼리를 만들어줍니다.
위 사진처럼 동적 쿼리가 제대로 동작한 것을 확인할 수 있습니다.
QueryDSL 5.0.0에서
QueryDSL 4.x.x가 가장 많이 쓰이지만, Release 하면서 가장 영향력 있게 변했다고 생각하는
fetchResults와 fetchCount가 deprecated 된 점에 대해서 알아보겠습니다.
fetchResults()는 "QueryResults.getOffset() 또는 QueryResults.getLimit()에 의존하지 않는 경우에는 fetch()를 대신 사용해야 성능이 더 우수합니다. 또한 모든 방언에 대해 카운트 쿼리를 제대로 생성할 수 없습니다."
fetchCount()는 "일부 QueryDSL 모듈이 카운트 쿼리를 사용하여 fetchCount를 최적화하지는 않을 수 있습니다."
fetchResults()의 경우 QueryResults를 사용하고, 여기서 count쿼리를 사용합니다.
이때 모든 dialect에서 완벽하게 지원이 안 되는 상태입니다.
즉, 두 가지 모두 Count와 관련된 내용으로 인해서 deprecated 되었다고 생각하면 됩니다.
실제로 복잡한 쿼리의 경우에는 정상적으로 동작하지 않는 경우가 있습니다.
기존에 사용하고 있던 fetchResults()들이 있다면 QueryResults를 사용하기 때문에 List타입으로 변경하고,
fetchCount()를 fetch().size()로 Count에 대한 쿼리를 하나 작성해서 진행하는 것이 좋습니다.
References
http://querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single/#intro
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/dashboard
https://github.com/ewerk/gradle-plugins/tree/master/querydsl-plugin
'Backend' 카테고리의 다른 글
KafkaItemReader 적용기 (0) | 2022.08.31 |
---|---|
Spock in Maven (0) | 2022.08.26 |
객체는 어떻게 식별하고 구현해야 할까? (0) | 2022.08.17 |
Java Generic 을 파헤쳐보자 - 심화편 (0) | 2022.08.10 |
상품 브랜드 태그 API 의 과거와 현재 (1) | 2022.03.10 |