🚨개발 초보자가 쓴 블로그 글입니다. 개인 공부 후에 남기는 기록용 글이며, 잘못된 정보가 있을 수 있습니다. 틀린 부분이 있다면 언제든지 답글 남겨주세요!!
Querydsl을 선택하게 된 계기
스프링 프로젝트를 진행하면서 JPA에서 기본적으로 제공하는 메소드 쿼리보다 조건이 더 많고 다양한 형태의 쿼리를 전송해야 하는 상황이 많았다. 복잡한 조회 쿼리들은 직접 JPQL을 String으로 작성하였는데, 가독성이 매우 떨어지고 조금 더 길어지면 괄호를 몇 개 닫아야 하는지 등을 파악하는 것도 어렵게 되었다. 그런 이유로, 나는 단순히 긴 쿼리를 가독성 있게 작성하기 위해서 Querydsl을 공부하기 시작했다. 관련 개념들을 조금씩 읽으면서, 생각보다(?) 더 중요한 프레임워크라고 느끼게 되었다.
Querydsl이란?

querydsl은 자바 애플리케이션에서 '타입 안정성'을 가진 도메인 특화 언어이다. 도메인 특화 언어란, 간단히 말해 어플리케이션에서 쿼리를 작성할 때, db의 테이블을 중심으로 작성 하는 것이 아니라 엔티티를 중심으로 작성하는 언어다. 그래서 MySQL, PostgreSQL, MongoDB 등 다양한 DBMS와 독립적인 쿼리를 작성할 수 있다. 이것의 대표적인 사례가 JPQL이다. 도메인 특화 언어이기 때문에 querydsl은 중요한 특성을 가지는데, 앞에서도 잠깐 언급한 '타입 안정성' 이다.
타입 안정성이란?
JPQL과 같은 문자열로 작성하는 쿼리는 실행후, 런타임 시에만 오류가 감지될 수밖에 없다. 지금의 쿼리가 맞는지 틀린지 확인하기 위해 매번 실행버튼을 눌러봐야 하는 불편함이 있다. 그래서 Querydsl은 도메인 특화 언어답게, 작성된 엔티티 필드들의 타입까지 확인한다. 따라서 런타임 시가 아닌 ide에서 작성하는 컴파일 시점에 곧바로 확인할 수 있다는 큰 장점을 가진다. 이것이 가능한 이유는 QClass이다. QClass는 엔티티 클래스의 메타 정보를 담고 있는 클래스이다. Member.java 가 있다면, QMember.java 가 있는 식이다. QMember.java를 통해 엔티티 정보를 확인하기 때문에 타입 안정성을 제공하는 것이 가능하다.
그렇다면 왜 Member.java를 그대로 쓰지 않고 QClass를 생성하는 것일까? QClass는 필드를 static으로 정의한다. 따라서 한 번 생성하여 전역에 걸쳐 사용하고, static이기 때문에 새로운 객체를 생성할 필요 없이 쿼리에서 자주 사용되는 필드들에 바로 접근할 수 있고, querydsl에서 제공하는 다양한 함수들도 이용할 수 있어 편리하다.
그림에서 보듯, 개발자가 querydsl로 쿼리를 작성하면, querydsl이 jpql을 생성하고, 이것을 jpa가 sql로 변환하여 db에 실행명령을 내린다. 이후 해당 querydsl을 거쳐 원하는 정보를 얻을 수 있게 된다.
➡️내용 참고 출처 : https://velog.io/@pms000723/QueryDSL%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0
Spring - QueryDSL이란?
QueryDSL을 사용하는 이유문자가 아닌 코드로 쿼리를 작성할 수 있어 컴파일 시점에 문법 오류를 확인할 수 있다.인텔리제이와 같은 IDE의 자동 완성 기능의 도움을 받을 수 있다.복잡한 쿼리나 동
velog.io
spring 프로젝트에 querydsl 적용하기 - gradle 사용
1. build.gradle 설정
이 블로그에 나온 걸 그대로 따라서 querydsl을 적용하기 위해 build.gradle을 수정했다.
➡️참고한 블로그 : https://dev.gmarket.com/33
주니어 개발자의 QueryDSL 찔러보기
안녕하세요. Fulfillment Engineering 팀의 입사한 지 1년이 얼마 지나지 않은 싱싱한(?) 주니어 개발자 백정현입니다. 최근 들어 JAVA를 기반으로 한 Spring boot + JPA 또는 Spring Data JPA를 이용한 프로젝트가
dev.gmarket.com
build.gradle
// dependency
...
// 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'
...
// 1. Q클래스가 생성될 경로 지정
// 블로그에서 작성한 대로 실행하면 Gradle 버전이 달라서 deprecated 때문에 오류가 발생한다.
// "$buildDir" 대신에 다음과 같이 사용하면, [프로젝트명]/build/generated 경로에 'querydsl [main]'
// 이라는 폴더가 생기고, 여기에 Q클래스들이 생성된다.
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
// 2. querydsl 플러그인의 설정
// jpa = true 는 JPA를 기반으로 한 Querydsl 사용을 활성화한다.
// querydslSourcesDir 변수에 querydslDir을 대입해서 Q클래스 파일이 생성될 위치를 지정한다.
querydsl{
jpa = true
querydslSourcesDir = querydslDir
}
// 3.Gradle이 Q클래스를 소스코드로 인식하도록 Q클래스 경로를 sourcesSets에 추가한다.
// Q클래스는 빌드 후 생성되므로, 이를 프로젝트의 클래스 경로에 추가해서 컴파일 시 활용될 수 있게 한다.
sourceSets {
main.java.srcDir querydslDir
}
// 4. compileQuerydsl 어노테이션 프로세서가 실행될 수 있도록 설정
// querydsl은 엔티티 클래스에서 어노테이션 프로세서를 통해서 Q클래스를 생성하므로,
// 이를 Gradle 컴파일 과정에 포함한다.
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
// 5. configuration 설정
// compileOnly에 어노테이션 프로세서가 컴파일에 필요한 라이브러리를 사용할 수 있게 연결
// querydsl가 엔티티 클래스의 컴파일된 버전을 사용할 수 있게 하기 위해 프로젝트의 컴파일
// 클래스 경로를 참조하도록 설정한다.
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
2. Qclass 생성
이전에 만들어놓은 프로젝트가 있어서 이어서 작업했다. 만들어놓은 Member.java 는 다음과 같은 식이다.

그리고 지마켓 블로그에서 하는 대로 Gradle > Tasks > other > compileQuerydsl 을 클릭하면, build.gradle에서 설정해놓은 대로 build > generated 경로에 'querydsl [main]' 이라는 파일이 생성된다. Member.java 파일을 만들었던 것처럼 똑같은 경로의 파일들이 생기고, 타고타고 들어가다 보면 같은 경로에 QMember.java 라는 파일이 새로 생성되어 있음을 확인할 수 있다.

QMember.java를 보면 대략 이렇게 생겼다.

경로 타입
다른 내용들은 더 공부해서 나중에 배우고, 여기서 하나만 보자면 'StringPath', 'NumberPath' 와 같은 것들이다. 그 용례를 가만히 보다 보면 엔티티의 컬럼을 표현하는 Q 클래스만의 방식이긴 한 것 같은데, 왜 String이나 Integer가 아니라 StringPath, NumberPath와 같은 새로운 객체를 쓰는 걸까?
이것은 QueryDSL에서 제공하는 '경로 타입' 이라고 한다. 이것들은 SQL 쿼리를 자바 코드로 작성할 수 있게 해주는 과정에서 데이터베이스 테이블의 컬럼을 표현할 수 있게 해준다. 개발자가 Querydsl을 사용해서 자바 코드를 작성할 때, 이런 식으로 작성한다.
QMember.member.accessToken.eq("exampleToken");
QMember.member.accessId.gt(100L);
accessToken 이라는 컬럼에 접근하고 싶다면, 파일 시스템의 파일 path에 접근하는 것처럼 QMember.member.accessToken 으로 접근하는 것이다. 그리고 querydsl에서 제공하는 타입 안정성에 맞게 경로 타입의 종류도 StringPath, NumberPath, DatePath/DateTimePath, BooleanPath 등으로 여러 가지가 있어 MySQL의 경우 각각 VARCHAR/TEXT, INTEGER/BIGINT, DATE/TIMESTAMP, BOOLEAN 의 타입에 대응된다. 각 경로 타입별로 다양한 함수가 있어서, StringPath의 경우 eq() 함수라던가, NumberPath의 경우 gt(), avg() 등의 함수 등을 사용해서 다양한 쿼리를 수월하게 만들어낼 수 있다.
3. repository 생성 및 쿼리 작성
미리 사용하던 MemberRepository 인터페이스가 있었지만, 그것은 이미 JPARepository를 상속받아 함수형 쿼리로 작성하는 많은 함수가 있었다. 나는 복잡한 쿼리만 querydsl로 작성하고 싶어서 (블로그에 쓴 쿼리는 연습용이라 복잡하지 않다) 이렇게 MemberDslRepository 인터페이스를 따로 만들었다. (이 이름이 맞는 것 같지는 않다. 항상 새로운 객체나 함수 이름을 작성할 때는 고민이 된다. <클린코드> 라는 책에 잘 나와있다는데 사놓고 읽지를 않고 있다..ㅎㅎ)
MemberDslRepositoy Interface
public interface MemberDslRepository {
boolean existsMemberByEmail(String email);
List<Member> findMemberWithRadius(double latitude, double longitude, double radius);
}
impl을 작성하기 전에 QueryDslConfig를 작성해서 JPQL 쿼리를 생성할 수 있는 JPAQueryFactory를 bean으로 설정했다.
QueryDSLConfig.java
@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {
private final EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
그리고 MemberDslRepositoryImpl 클래스를 다음과 같이 만들었다.
MemberDslRepositoryImpl.java Class
// Member.class의 queryDsl respositoryImpl - member 관련 복잡한 DB 쿼리 사용
@Repository
@RequiredArgsConstructor
public class MemberDslRepositoryImpl implements MemberDslRepository{
private final JPAQueryFactory query;
@Override
public boolean existsMemberByEmail(String email) {
QMember member = QMember.member;
return query.selectOne()
.from(member)
.where(member.email.eq(email))
.fetchFirst() != null;
}
// member의 location_point와 매개변수 latitude, longitude 로 구성된 point와의 거리 차이가 radius 이하인 member list 조회
@Override
public List<Member> findMemberWithRadius(double latitude, double longitude, double radius) {
QMember member = QMember.member;
Point referencePoint = new GeometryFactory().createPoint(new Coordinate(longitude,latitude));
referencePoint.setSRID(4326);
List<Member> memberList= query.selectFrom(member)
.where(
// querydsl은 데이터베이스에 종속적인데
Expressions.booleanTemplate(
"ST_Distance_Sphere({0},{1}) <= {2}",
member.locationPoint,
referencePoint,
radius
)
)
.fetch();
return memberList;
}
}
3-1. 쿼리 설명
1) existsMemberByEmail
이 함수는 해당 email을 가진 Member의 유무를 파악하는 함수이다. QMember 클래스로 각 필드에 접근할 수 있게 member 객체를 생성한다.
이어지는 코드에서 먼저 selectOne() 함수는 해당 조건을 만족하는 하나의 튜플 전체를 반환하거나 없으면 null을 반환하고, 여러개일 경우 오류를 발생시킨다. email은 모든 사람이 다르다는 조건 하에 생성한 코드이다.
| selectOne() | 하나의 결과 반환/없으면 null 반환. 2개 이상일 경우 오류 발생! |
| from() | 찾고자 하는 테이블, 여기서는 Q타입 클래스. member를 넣는다. |
| where() | where 조건절. member.email 컬럼이 eq(email) 사용자가 입력한 email 변수와의 일치 여부를 파악. |
| fetchFirst() | 쿼리 결과 중 첫 번째 행만을 가져올 때 사용함. 이외에도 fetchOne(결과가 없거나 여러개면 예외를 발생시키는, 정확히 하나의 행만 반환), fetch(결과 리스트 반환), fetchResults(경로가 리스트와 전체 개수를 함께 반환하며, 페이징 정보를 포함한 쿼리), fetchCount(쿼리 결과의 개수만 반환) 등의 여러 함수들이 존재한다. |
이것을 sql로 작성하면 'select * from MEMBER where email=[email 변수]' 와 같고, 이것이 null이 아닌지 여부를 반환하는 boolean 함수로 작성했다.
2) findMemberWithRadius
Member 클래스에는 사용자의 위도와 경도를 저장하는 Point 필드 locationPoint가 있는데, 현 위치 위도와 경도, 그리고 radius를 매개변수로 받아 현 위치와 각 locationPoint들 간의 거리가 radius 안의 member 객체 리스트를 찾는 함수이다. MySQL에는 Point 객체를 사용해서 각 point 간 거리를 구하는 함수 'ST_Distance_Sphere'가 있다.
📜MySQL Document : https://dev.mysql.com/doc/refman/8.4/en/spatial-convenience-functions.html
MySQL :: MySQL 8.4 Reference Manual :: 14.16.13 Spatial Convenience Functions
14.16.13 Spatial Convenience Functions The functions in this section provide convenience operations on geometry values. Unless otherwise specified, functions in this section handle their geometry arguments as follows: If any argument is NULL, the return v
dev.mysql.com
mysql document에서 사용법을 간단히 설명해주고 있다.
함수에서는 받은 latitude와 longtitude로 referencePoint를 생성한 다음 쿼리를 작성했다.
querydsl는 db에 독립적이기 때문에 MySQL 내장함수인 ST_DistanceSphere을 사용할 수 없었다. 그래서 querydsl에서 기본적으로 제공해주지 않는 함수들을 custom해서 사용할 수 있는 Expression을 사용했는데, querydsl에서는 다양한 타입을 반환하는 template이 있다.
자세히 설명하지는 않지만,
stringTemplate의 경우에는 사용자 정의 문자열 표현식을 생성하고, numberTemplate은 숫자 표현식, booleanTemplate은 논리 표현식을 생성하는 expression을 만들 때 사용된다. 각각의 expression들은 쿼리 내에서 조건, 정렬, 선택 시에 다양하게 사용된다. 나는 여기서 radius 이내에 있는지 여부를 살피는 expression을 만들고 싶었기 때문에 인수바인딩을 통해 booleanTemplate을 where 절 안에 조건으로 넣었다.
➡️참고한 블로그 : https://innysfam.tistory.com/123
QueryDsl에서 함수 사용
Expressions 클래스는 SQL 함수의 다양한 표현식을 Java 코드로 직접 사용할 수 있으며, JPA와 같이 Dialect(방언)에 등록된 내용만 호출할 수 있습니다. QueryDsl의 Expressions 클래스에서 자주 사용되는 다양
innysfam.tistory.com