QueryDSL1️⃣ QueryDSL 이란?그래서 이거 왜 쓰나요?QueryDSL의 장점QueryDSL의 원칙2️⃣ QueryDSL 설정build.gradle 파일 설정yml 파일설정Configuration 클래스 작성설정 끝난 후 해야할 작업3️⃣ QueryDSL 사용해보기예제 도메인JpaQueryFactory 다건 유저 조회단건 유저 조회조건에 맞는 유저 조회정렬기능페이징 기능집계 기능조인 기능서브쿼리 case문상수, 문자 더하기 4️⃣ QueryDSL 추가사항DTO 반환하기QeuryDSL Bean 생성동적쿼리수정, 삭제 벌크
QueryDSL
1️⃣ QueryDSL 이란?
- QueryDSL은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크입니다.
그래서 이거 왜 쓰나요?
@Query("select p from Post p join fetch p.user u " + "where u in " + "(select t from Follow f inner join f.target t on f.source = :user) " + "or u = :user " + "order by p .createdAt desc") List<Post> findAllAssociatedPostsByUser(@Param("user") User user, Pageable pageable);
- Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용 하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성해야 합니다.
- 간단한 로직을 작성하는데 큰 문제는 없지만, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어지게 되며 JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 애플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생합니다.
- 이러한 문제를 어느정도 해소하는데 기여하는 프레임워크가 QueryDSL이기 때문에 사용합니다.
QueryDSL의 장점
- 문자가 아닌 코드로 쿼리를 작성 함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
- 자동 완성 등 IDE의 도움을 받을 수 있다.
- 동적인 쿼리 작성이 편리하다.
- 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용 할 수 있다.
QueryDSL의 원칙
- QueryDSL의 핵심 원칙은 타입 안전성(Type Safety)입니다. 도메인 타입의 프로퍼티를 반영해서 생성한 쿼리 타입을 애용해 쿼리를 작성하게 됩니다. 또한 완전한 타입에 안전한 방법으로 함수/메서드 호출이 이루어집니다.
- 다른 원칙은 일관성(consistency)입니다. 기반 기술에 상관없이 쿼리 경로와 오퍼레이션은 모두 동일하며 Query 인터페이스는 공통의 상위 인터페이스를 갖게 됩니다.
2️⃣ QueryDSL 설정
- QueryDSL 적용하면서 가장 까다로운 부분이 설정이라고 할 수 있다. 공식 문서에는 Gradle에 대한 내용이 누락되어 있으며, 실제로 QueryDSL 설정 방법은 Gradle 및 IntelliJ 버전에 따라 상이하기 때문이다.
build.gradle 파일 설정
- buildscript
buildscript { ext { queryDslVersion = "5.0.0" } }
- plugins 추가
plugins { id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" }
- dependencies
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
- queryDSL 설정
- querydsl 플러그인 task를 실행했을 때 Q클래스 파일 위치를 지정해주는 설정
/*************************** ** QueryDSL Config Start ** ***************************/ def querydslDir = "$buildDir/generated/querydsl" querydsl { jpa = true querydslSourcesDir = querydslDir } sourceSets { main.java.srcDir querydslDir } // 여기부터는 Gradle 5.0 이후로 추가 작성 compileQuerydsl{ options.annotationProcessorPath = configurations.querydsl } configurations { compileOnly { extendsFrom annotationProcessor } querydsl.extendsFrom compileClasspath } /*************************** *** QueryDSL Config End *** ***************************/
yml 파일설정
jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect database: mysql generate-ddl: false open-in-view: false show-sql: true hibernate: ddl-auto: properties: hibernate: show_sql: true format_sql: true use_sql_comments: true logging: level: org: hibernate: SQL: DEBUG type: trace
- datasource : database 설정값을 세팅한다.
- jpa.database-platform : platform 설정
- jpa.open-in-view : 영속성을 어느 범위까지 설정할지 결정
- jpa.show-sql : 실행하는 쿼리 show 설정
- jpa.hibernate.ddl-auto : 톰캣 기동할 때 어떤 동작을 할지 결정
- 해당 설정을 잘못하면 테이블이 drop 될 수 있다.
- 한번 설정이 끝났다면 none, validate로 설정하는 것을 추천한다.
- jpa.properties.hibernate.format_sql : 쿼리를 잘 정렬해서 보여준다.
- logging.level.org.hibernate.SQL: 로그레벨 설정
Configuration 클래스 작성
- jpa 기반이기 때문에 설정과 동일하다.
@Configuration public class DataBaseConfiguration { @PersistenceContext private EntityManager entityManager; @Bean public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(entityManager); } }
설정 끝난 후 해야할 작업
- compileQuerydsl을 실행시켜 주면 build.gradle에 설정한 경로로 Q클래스가 생성됩니다.
- Q클래스
- 설정 및 빌드를 마친 이후, 다음과 같이 Java 파일을 컴파일 해야합니다.
- APT를 이용하여 Entity 기반으로 querydsl plugin을 실행시키면 prefix “Q”가 붙는 Q클래스가 생성된다.
- User → QUser
- APT란? Annotation Processing Tool의 약자로 Annotation이 있는 코드 기준으로 새로운 파일을 만들 수 있고 compile 기능도 가능하다.

- 도메인이 바뀐다면 위의 작업을 다시 해주어야 합니다.
3️⃣ QueryDSL 사용해보기
예제 도메인
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int age; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id") private Team team; protected User() {} public User(String name, int age) { this.name = name; this.age = age; } // GETTER } @Entity public class Team { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; protected Team() {} @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); public Team(String name) { this.name = name; } // GETTER }
JpaQueryFactory


- static import를 사용하면 더욱 가독성이 좋아집니다.
다건 유저 조회
유저 정보가 저장소에 없으면 빈 리스트를 반환하게 됩니다.
- 방법 1 - fetch()
List<User> user = jpaQueryFactory.selectFrom(user).fetch();
- 방법 2- fetchResults()
// fetchResults() 페이징 정보 포함, total count 쿼리 추가 QueryResults<User> userQueryResults = jpaQueryFactory.selectFrom(user) .fetchResults(); List<User> users = userQueryResults.getResults();
단건 유저 조회
단건 조회로 결과가 하나라면 해당
User
객체 반환 결과가 없으면 null
결과가 둘 이상이라면 NonUniqueResultException
에외를 반환하게 됩니다.- 방법 1 - fetchOne()
jpaQueryFactory.selectFrom(user) .where(user.name.eq(name)) .fetchOne();
- 방법 2 - fetchFirst();
// fetchFirst()는 limit(1).fetchOne() 한 것과 동일합니다. jpaQueryFactory.selectFrom(user) .where(user.name.eq(name)) .fetchFirst();
조건에 맞는 유저 조회
- eq() : equlas, == 연산자로 생각하시면 됩니다.
// 이름이 ~ 이고 나이가 ~ 인 유저를 조회해줘 ! jpaQueryFactory.selectFrom(user) .where( user.name.eq(name) .and(user.age.eq(age)) ) .fetchOne();
and의 경우에 and 키워드 말고 ,(콤마)로 구분이 가능합니다.
// 이름이 ~ 이고 나이가 ~ 인 유저를 조회해줘 ! jpaQueryFactory.selectFrom(user) .where( user.name.eq(name), user.age.eq(age) ) .fetchOne();
- ne() : != 연산자로 생각하시면 됩니다.
- eq().not() 방식으로 사용할 수도 있어요 !
- isNotNull() : 필드값이 null이 아닌 것
- in() : 안의 요소가 포함되는 값 조회
// 유저의 나이가 25, 26, 27인 사람들을 조회해줘! List<User> users = jpaQueryFactory.selectFrom(user) .where(user.age.in(25, 26, 27)) .fetch();
- notIn() : 요소에 포함되지 않는 값 조회
- between() : 요소 사이에 포함되는 값
- goe() : 값이 요소보다
≥
인것 조회
// 유저의 나이가 25 이상인 사람들을 조회해줘! List<User> users = jpaQueryFactory.selectFrom(user) .where(user.age.goe(25)) .fetch();
- gt() : 값이 요소보다
>
인것 조회
- loe() : 값이 요소보다
≤
인것 조회
- lt() : 값이 요소보다
<
인것 조회
- like() : like 문법과 동일합니다
// %name%, %name, name% 식으로 작성을 해야합니다. List<User> users = jpaQueryFactory.selectFrom(user) .where(user.name.like("name%")) .fetch();
정렬기능
- asc() : 오름차순 정렬
- desc() : 내림차순 정렬
// 나이가 27인 유저들을 이름기준으로 내림차순으로 조회해줘! List<User> users = jpaQueryFactory.selectFrom(user) .where(user.age.eq(27)) .orderBy(user.name.desc()) .fetch();
- nullLast(), nullFirst() : null 데이터 순서 부여
// null일 경우에 첫번째로 정렬할 것인지 마지막으로 정렬할 것인지 ! // nullFirst() 라면 정렬 기준이 asc, desc 상관없이 첫번째로 온다! List<User> users = jpaQueryFactory.selectFrom(user) .where(user.age.eq(27)) .orderBy(user.name.asc().nullsFirst()) .fetch();
페이징 기능
- 방법 1 - fetch()
List<User> users = jpaQueryFactory.selectFrom(user) .where(user.age.eq(27)) .orderBy(user.name.desc().nullsFirst()) .offset(1) .limit(10) .fetch();
- 방법 2 - fetchResults()
- 추천하는 방법도 아니며 현재 deprecated 된 상태이다.
QueryResults<User> users = jpaQueryFactory.selectFrom(user) .where(user.age.eq(27)) .orderBy(user.name.desc()) .offset(1) .limit(10) .fetchResults();
집계 기능
- count() : 수
- sum() : 합계
- avg() : 평균
- max() : 최대
- min() : 최소
// 유저 수, 유저 나이의 합, 평균 나이, 최대 나이, 최소 나이를 구해줘 ! List<Tuple> tuples = jpaQueryFactory .select( user.count(), user.age.sum(), user.age.avg(), user.age.max(), user.age.min() ) .from(user) .fetch(); // 그룹화된 결과를 제한하려면 having 절과 함께 사용하면 된다. List<Tuple> result = jpaQueryFactory .select(team.name, user.age.avg()) .from(user) .join(user.team, team) .groupBy(team.name) .fetch();
조인 기능
- join(), innerJoin() : 내부 조인(inner join)
- leftJoin() : left 외부 조인
- rightJoin() : right 외부 조인
List<Member> result = jpaQueryFactory .selectFrom(user) .join(user.team, team) .where(team.name.eq("규현팀")) .fetch(); // fetchJoin()을 해주면 연관관계 매핑된 정보도 함께 가져온다. List<Member> result = jpaQueryFactory .selectFrom(user) .join(user.team, team).fetchJoin() .where(team.name.eq("규현팀")) .fetch();
서브쿼리
- 서브 쿼리는 JPAExpressions 를 통해서 표현해야 한다. 서브쿼리에 사용되는 QClass는 겉의 쿼리와 다른 객체여야 하므로 QClass를 직접 선언해서 다른 인스턴스를 사용하도록 해야한다.
- JPA JPQL 서브쿼리의 한계점으로는 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
- 당연히 QueryDSL도 지원하지 않으며 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. QueryDSL도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
- from 절의 서브쿼리 해결법
- 서브쿼리를 join으로 변경한다. (가능한 상황도 있으며 불가능한 상황도 존재한다.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다.
// 유저의 나이가 현재 데이터베이스에 있는 유저 평균 나이 이상인 사람만 조회해줘 ! QUser userSub = new QUser("userSub"); List<User> result = jpaQueryFactory .selectFrom(user) .where(user.age.goe( JPAExpressions .select(userSub.age.avg()) .from(userSub))) .fetch();
case문
- case문은 select, 조건절(where), order by에서 사용 가능하다.
NumberExpression<Integer> rangPath = new CaseBuilder() .when(user.age.between(20, 30)).then(2) .when(user.age.between(40, 50)).then(1) .otherwise(3); List<Tuple> fetch = jpaQueryFactory .select(user.name, user.age, rangPath) .from(user) .orderBy(rangPath.desc()) .fetch();
상수, 문자 더하기
- 문자가 아닌 타입들은 stringValue()를 통해서 문자로 변환할 수 있으며 이 방법은 ENUM을 처리할 때도 자주 쓰인다.
jpaQueryFactory .select(user.name.concat("_").concat(user.age.stringValue())) .from(user) .where(user.name.eq("형욱")) .fetchOne();
4️⃣ QueryDSL 추가사항
DTO 반환하기
- DTO 생성을 위한 new 키워드가 필요하며 패지키 풀 경로까지 작성해줘야 합니다.
- 생성자 방식만 지원합니다.
entityManager.createQuery( "select new 패지키 풀 경로.클래스이름(파라미터) FROM 테이블", 반환클래스) ).getResultList(); // 에시 entityManager.createQuery( "SELECT new com.prgrms.querydsl.example.UserDto(u.name, u.age) FROM User u", UserDto.class ).getResultList();
QeuryDSL Bean 생성
- db를 다루다 보면 종종 projection을 다뤄야 할 때가 있다.
- Projection 연산
- 한 Relation의 Attribute들의 부분 집합을 구하는 연산자로 결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가진다.
- 결과로 나온 Relation은 기본 키가 아닌 Attribute 에 대해서만 중복된 tuple들이 존재할수 있다.
- 프로퍼티 접근 - Setter
jpaQueryFactory .select( Projections.bean(UserDto.class, user.name user.age) ) .from(user) .fetch();
- 필드 직접 접근
jpaQueryFactory .select( Projections.fields(UserDto.class, user.name, user.age)) .from(member) .fetch();
필드가 다르다면 alias를 이용하자 !
jpaQueryFactory .select(UserDto.class, user.name.as("name"), ExpressionUtils.as( JPAExpressions .select(userSub.age.max()) .from(userSub), "age") )) .from(user) .fetch();
- 생성자 사용
jpaQueryFactory .select(Projections.constructor(UserDto.class, user.name, user.age)) .from(user) .fetch();
- @QueryProjection + 생성자
- 컴파일러로 타입 체크가 가능합니다 ! 가장 안전한 방법!
- QueryDSL 애노테이션이 DTO에 침식해 있다는 점과 QClass를 생성해 주어야 함
public class UserDto { ~~~ @QueryProjection // 따봉 ! public UserDto(String name, Integer age) { this.name = name; this.age = age; } } jpaQueryFactory .select(new QUserDto(user.name, user.age)) .from(member) .fetch();
동적쿼리
- BooleanBuilder
List<User> searchBooleanBuilder(String name, Integer age) { BooleanBuilder builder = new BooleanBuilder(); if(name != null) { builder.add(user.name.eq(name)); } if(age != null) { builder.add(user.age.eq(age)); } return jpaQueryFactory .select(user) .from(builder) .fetch(); }
- where 다중 파라미터 사용하기
- where 조건에 null 값은 무시된다 !
- 메서드를 다른 쿼리에서도 재활용이 가능하다 !
- 쿼리 자체의 가독성도 좋아진다 !
List<User> searchWhere(String name, Integer age) { return jpaQueryFactory .selectFrom(user) .where(nameEq(name), ageEq(age)) // 가독성 증가 ! .fetch(); } private BooleanExpression nameEq(String name) { return name != null ? user.name.eq(name) : null; } private BooleanExpression ageEq(Integer age) { return age != null ? user.age.eq(age) : null; } // 조합도 가능하다 private BooleanExpression allEq(String name, Integer age) { return nameEq(name).and(ageEq(age)); }
조합하는 경우에는 null을 조심해야 한다 !
수정, 삭제 벌크
- 계산의 경우, add() 등의 메서드를 활용하면 된다.
// 수정 jpaQueryFactory .update(user) .set(user.name, "짱구") .set(user.age, user.age.multiply(5)) // age = age * 5 .where(user.age, isNull()) .execute(); // 삭제 jpaQueryFactory .delete(user) .where(user.age.lt(27).or(user.age.isNull())) .execute(); )
JPQL 배치와 같이 영속성 컨텍스트를 무시하고 실행한다.
실행 후 영속성 컨텍스트를 초기화하는 것이 안전하다.