| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- lv1
- 자바
- Spring
- DB
- 토스
- 디자인 패턴
- Effective Java
- spring boot
- 이펙티브 자바
- 성능 개선
- 배치
- 프록시 패턴
- redis
- 로드밸런서
- 빌더 패턴
- GoF 23
- 추상클래스
- 계산기
- 백엔드
- 김영한
- Spring Batch
- 스프링
- Til
- 코드카타
- 템플릿 메서드 패턴
- 스프링 배치
- java
- 프로그래머스
- 스케줄러
- 트러블슈팅
- Today
- Total
김코딩
QueryDSL 기본 가이드 본문
QueryDSL 초기 세팅은 스프링 부트 3.x 버전을 기준으로 세팅합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'study'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//test 롬복 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
useJUnitPlatform()
}
clean {
delete file('src/main/generated')
}
- build.gradle에 들어가서 QueryDSL 의존성을 추가해줍니다. 주의 : 스프링 부트 3.x 부터는 :jakarta를 붙혀야합니다.
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta
- 컴파일 시 @Entity가 붙은 JPA 엔터티를 기반으로 Q-타입 클래스를 자동으로 생성해줍니다.
- 이게 없으면 Q클래스가 생성되지 않아서 QueryDSL을 사용할 수 없습니다.
annotationProcessor "jakarta.annotation:jakarta.annotation-api
- APT 처리 과정에서 필요한 @Generated 등의 어노테이션을 제공하는 Jakarta 표준 API입니다.
- QueryDSL APT가 작동하기 위해 필요합니다.
APT ( Annotation Processing Tool )?
-> 컴파일 타임에 어노테이션을 분석해서 코드를 자동 생성해주는 도구
-> 결국에는 엔터티를 분석해서 Q클래스로 만들어주는 도구입니다.
annotationProcessor "jakarta.persistence:jakarta.persistence-api
- JPA의 기본 어노테이션들(@Entity, @Id, @OneToMany, 등등)을 제공하는 Jakarta Persistence API입니다.
- APT가 Q타입을 만들 때 이 JPA 어노테이션들을 인식해야 하므로 processor로 지정합니다.
# 엔터티 생성
예제 코드로 사용할 Member 엔티티와 Team 엔티티를 생성하겠습니다.
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id
@GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
# Q클래스 생성
1. settings -> gradle -> 모두 gradle로 설정해줍니다.

2. 인텔리제이 우측 사이드바에 있는 gradle을 클릭합니다. 아래 사진이 보이는 순서대로 클릭해줍니다.

3. 루트 패키지/build/generated/ 경로로 들어가서 Q클래스 생성을 확인해줍니다.

# QueryDSL 기본문법
(기본 문법은 테스트 환경에서 진행하였습니다.)
(테스트 환경을 돌리기 전에 데이터베이스를 먼저 연결해야합니다.)
테스트 환경 세팅
@SpringBootTest
@Transactional
public class QueryDslTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
void beforeEach() {
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamA);
Member member4 = new Member("member4", 40, teamA);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
}
- JPAQueryFactory는 QueryDSL 쿼리를 만들기 위한 핵심 객체이며, 내부적으로 EntityManager를 사용한다.
- 예제로 사용하기 위한 Team과 Member를 생성하였다.
# QueryDSL vs JPQL
/**
* member1을 찾아라
*/
@Test
void startJPQL() {
String sql = "select m from Member m " +
"where m.username = :username";
Member findMember = em.createQuery(sql, Member.class)
.setParameter("username", "member1")
.getSingleResult();
Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
}
- JPQL만으로도 충분히 쿼리를 생성하여 조건에 맞는 결과를 찾을 수 있습니다.
- 하지만 JPQL은 쿼리를 직접 만들어야하기 때문에 오타가 날 수도 있고, 컴파일 에러 시점이 아닌 런타임 에러 시점에서 오류가 발생하여 오류를 찾기가 어렵습니다.
- 이러한 단점을 보완하기 위해서 나온게 QueryDSL입니다.
import static study.querydsl.entity.QMember.*;
@Test
void startQueryDsl() {
// QMember m = new QMember("m");
// QMember m = QMember.member;
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
}
- Q클래스는 위에 주석처리된 방식으로 객체를 생성해서 사용해도 되고, static import를 사용하여 바로 사용하셔도 됩니다.
- QueryDSL은 JPQL을 자바 코드로 타입 안정성 있게 작성할 수 있도록 도와주는 JPQL 빌더입니다.
- QueryDSL은 JPQL과 다르게 컴파일 시점에서 오류를 잡아줍니다.
# 검색 조건 쿼리
기본 검색 쿼리
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
Assertions.assertThat(findMember.getAge()).isEqualTo(10);
}
- 검색 조건은 .and(), or() 로 메서드 체인으로 연결할 수 있습니다.
- where(member.username.eq("member1"), member.age.eq(10)) 와 같이 and조건을 ,로 바로 사용할 수 있습니다.
- select, from을 selectFrom으로 바로 합칠 수 있습니다.
검색 조건
member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() //이름이 is not null
member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30
member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30
member.username.like("member%") //like 검색
member.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") //like ‘member%’ 검색
# 결과 조회
@Test
void resultFetch() {
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
Member member1 = queryFactory
.selectFrom(member)
.fetchOne();
Member member2 = queryFactory
.selectFrom(member)
.fetchFirst();
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
long count = queryFactory
.selectFrom(member)
.fetchCount();
}
- fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne() : 단 건 조회 결과가 없으면 : null 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException\
- fetchFirst() : limit(1).fetchOne()
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount() : count 쿼리로 변경해서 count 수 조회
# 정렬
/**
* 회원 정렬 순서
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 올림차순(asc)
* 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
*/
@Test
public void sort() {
em.persist(new Member(null, 100));
em.persist(new Member("member5", 100));
em.persist(new Member("member6", 100));
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
}
- orderBy() : 정렬
- desc() : 내림차순, asc() : 오름차순
- nullsLast() : null을 마지막에 출력
# 페이징
조회 건수 제한
@Test
void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetch();
Assertions.assertThat(result.size()).isEqualTo(2);
}
- offset(n) : n부터 시작 (index)
- limit(n) : 최대 n건 조회
- 페이징을 할 때에는 orderBy 사용해 정렬 후 사용합니다.( orderBy 없이 페이징하면 페이지마다 결과 순서가 달라질 수 있음)
#집합
/**
* JPQL
* select
* COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이
* from Member m
*/
@Test
public void aggregation() {
List<Tuple> result = queryFactory
.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetch();
Tuple tuple = result.get(0);
Long count = tuple.get(member.count());
Integer sum = tuple.get(member.age.sum());
Double avg = tuple.get(member.age.avg());
Integer max = tuple.get(member.age.max());
Integer min = tuple.get(member.age.min());
}
- 여러 개의 값을 동시에 조회하고 있기 때문에 Tuple이라는 타입으로 반환된다.
- tuple은 result.get(0)으로 꺼낼 수 있다. (List의 size가 1이기 때문에 get(0)를 사용해 튜플을 꺼낸다.)
- tuple.get을 사용하여 조회 결과를 꺼낼 수 있다.
#GroupBy
/**
* 팀의 이름과 각 팀의 평균 연령을 구해라.
*/
@Test
public void group() {
List<Tuple> result = queryFactory
.select(team.name,
member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
Tuple teamA = result.get(0);
Tuple teamB = result.get(1);
assertThat(teamA.get(team.name)).isEqualTo("teamA");
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
assertThat(teamB.get(team.name)).isEqualTo("teamB");
assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
- groupBy : 그룹화된 결과에 조건을 추가하고싶으면 having()을 사용하면 된다.
#조인
기본 조인
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() {
List<Member> result = factory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
- join(조인 대상, 별칭으로 사용할 Q타입)
- join() , innerJoin() : 내부 조인(inner join)
- leftJoin() : left 외부 조인(left outer join)
- rightJoin() : rigth 외부 조인(rigth outer join)
세타 조인(막 조인)
/**
* 세타 조인
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@Test
public void theta_join() {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
List<Member> result = factory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("teamA", "teamB");
}
- 연관관계가 없는 필드로 조인
- from 절에 여러 엔터티 선택해서 세타 조인
- 외부 조인 불가능(left, right)
조인 - on절
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and
t.name='teamA'
*/
@Test
public void join_on_filtering() {
List<Tuple> result = factory
.select(member, team)
.from(member)
.leftJoin(member.team, team)
.on(team.name.eq("teamA"))
.where(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
- on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where절에서 필터링 하는 것과 기능이 동일하다.
페치 조인(fetch join)
fetch join을 사용하지 않았을 경우
@Test
public void fetchJoinNo() {
em.flush();
em.clear();
Member findMember = factory
.selectFrom(member)
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();
}

- 지연로딩으로 인하여 team은 아직 불러오지 않음.
fetch join을 사용했을 경우
@Test
public void fetchJoinUse() throws Exception {
em.flush();
em.clear();
Member findMember = factory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();
}

- member와 team을 한 쿼리로 함께 조회
#서브쿼리
서브쿼리 eq 사용
/**
* 나이가 가장 많은 회원 조회
*/
@Test
public void subQuery() {
QMember memberSub = new QMember("memberSub");
List<Member> result = factory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age").containsExactly(40);
}
서브쿼리 goe 사용
/**
* 나이가 평균 나이 이상인 회원
*/
@Test
public void subQueryGoe() {
QMember memberSub = new QMember("memberSub");
List<Member> result = factory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions.select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(30, 40);
}
서브쿼리 여러 건 처리 in 사용
/**
* 서브쿼리 여러 건 처리, in 사용
*/
@Test
public void subQueryIn() {
QMember memberSub = new QMember("memberSub");
List<Member> result = factory
.selectFrom(member)
.where(member.age.in(
JPAExpressions.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(20,30, 40);
}
select 절에 subquery
@Test
public void selectSubQuery() {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = factory
.select(member.username,
JPAExpressions.select(memberSub.age.avg())
.from(memberSub))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
- JPAExpressions를 활용하여 서브쿼리 작성 가능
- JPAExperssions는 static import 가능
- from절에는 서브쿼리 불가능
CASE문
단순 조건
@Test
public void basicCase() {
List<String> result = factory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
복잡 조건
@Test
public void complexCase() {
List<String> result = factory
.select(new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20살")
.when(member.age.between(21, 30)).then("21~30살")
.otherwise("기타"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
정리
- QueryDSL은 JPQL을 사용할 때, 휴먼 에러(오타) 를 방지하기 위해서 자주 사용한다.
- QueryDSL을 사용하면 컴파일 에러가 발생하여 오류를 발견하기 쉽다.
- Join에 대해서 조금 더 공부를 해야할 것 같다.
'스프링' 카테고리의 다른 글
| JPA - 연관관계 편의 메서드 (1) | 2025.06.27 |
|---|---|
| 스프링 MVC - 타입 변환은 어떻게 이루어질까? (Converter & Formatter) (0) | 2025.06.05 |
| 반복되는 로그인 체크, 필터 하나로 끝내기(Filter 사용법) (0) | 2025.05.22 |
| 여긴 못지나간다.(@Valid) (0) | 2025.05.19 |
| Spring Container와 Bean이 헷갈리는 이들에게 (0) | 2025.05.15 |