JPQL(Java Persistence Query Language)은 객체 지향 쿼리 언어로, JPA를 사용하여 데이터베이스와 상호작용할 때 사용하는 문자열 기반의 쿼리 언어입니다.
하지만 JPQL은 타입 세이프하지 않으며, 쿼리의 오류가 런타임 시점에만 발생합니다.
반면 QueryDSL은 타입 세이프하며 컴파일 시점에서 오류를 발견할 수 있고, 동적 쿼리를 안전하게 처리할 수 있는 Java 기반의 쿼리 빌더입니다.
예를 들어, 회원(Member)과 팀(Team)이 연관된 구조에서, 특정 팀에 속한 회원들의 목록을 조회하는 JPQL을 작성한다고 가정해 보겠습니다. JPQL은 문자열로 작성되므로 쿼리 작성 시 오류를 잡기 어려운 단점이 있습니다.
public List<Member> findMembersByTeam(String teamName) {
String jpql = "SELECT m FROM Member m JOIN m.team t WHERE t.name = :teamName";
return em.createQuery(jpql, Member.class)
.setParameter("teamName", teamName)
.getResultList();
}
위 JPQL은 동적 쿼리 작성에 불편하고 쿼리의 타입 안전성이 떨어집니다. 잘못된 필드명이나 잘못된 타입을 사용할 경우 런타임에 오류가 발생하게 됩니다.
QueryDSL을 사용하면 동일한 쿼리를 타입 세이프하고 동적으로 작성할 수 있습니다.
JPQL에서의 문제를 QueryDSL로 해결할 수 있습니다. QueryDSL은 Q 클래스를 통해 타입 안전성 및 코드 완성 기능을 제공합니다.
public List<Member> findMembersByTeam(String teamName) {
QMember member = QMember.member;
QTeam team = QTeam.team;
return queryFactory.selectFrom(member)
.join(member.team, team) // Member 엔터티의 team 필드와 조인
.where(team.name.eq(teamName)) // 팀 이름 조건
.fetch(); // 결과 반환
}
위 코드에서 QMember
와 QTeam
은 QueryDSL이 자동 생성하는 Q 클래스입니다. Q 클래스는 JPA 엔티티 클래스를 바탕으로 자동으로 생성되며, 쿼리에서 사용할 타입 세이프한 변수들을 제공합니다.
Q 클래스는 QueryDSL에서 엔티티 클래스에 대한 타입 안전 쿼리를 작성하기 위해 자동으로 생성되는 클래스입니다.
QMember
, QTeam
과 같은 클래스는 쿼리 조건에 사용될 수 있는 필드들을 제공합니다.엔티티 클래스 Member
와 Team
이 있을 때, QueryDSL은 이들에 대해 각각 QMember
, QTeam
클래스를 생성합니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@ManyToOne
private Team team;
}
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
위와 같은 엔티티가 있을 때, QueryDSL은 빌드 시점에 아래와 같은 Q 클래스를 생성합니다.
QMember
: Member
엔티티의 필드 id
, name
, age
, team
에 대한 쿼리 조건을 제공합니다.QTeam
: Team
엔티티의 필드 id
, name
에 대한 쿼리 조건을 제공합니다.public class QMember {
public static final QMember member = new QMember("member");
public final StringPath name = createString("name");
public final NumberPath<Long> id = createNumber("id", Long.class);
public final NumberPath<Integer> age = createNumber("age", Integer.class);
public final QTeam team = new QTeam("team");
// 생성자, 필드 등...
}
public class QTeam {
public static final QTeam team = new QTeam("team");
public final StringPath name = createString("name");
public final NumberPath<Long> id = createNumber("id", Long.class);
// 생성자, 필드 등...
}
QueryDSL의 가장 큰 장점 중 하나는 동적 쿼리 작성입니다. 여러 조건이 있을 때 쿼리를 안전하고 효율적으로 생성할 수 있습니다.
예를 들어, 나이, 이름, 팀 이름에 따라 회원을 조회하는 동적 쿼리를 작성해보겠습니다.
public List<Member> findMembers(String name, Integer age, String teamName) {
QMember member = QMember.member;
QTeam team = QTeam.team;
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(member.name.eq(name)); // 이름이 null이 아니면 조건 추가
}
if (age != null) {
builder.and(member.age.eq(age)); // 나이가 null이 아니면 조건 추가
}
if (teamName != null) {
builder.and(team.name.eq(teamName)); // 팀 이름이 null이 아니면 조건 추가
}
return queryFactory.selectFrom(member)
.join(member.team, team)
.where(builder) // 동적 조건 적용
.fetch(); // 결과 반환
}
BooleanBuilder는 QueryDSL에서 동적 쿼리를 작성할 때 조건을 누적하여 추가할 수 있도록 돕는 클래스입니다. 위 코드에서는 name
, age
, teamName
을 조건에 따라 동적으로 추가할 수 있습니다.
QueryDSL에서는 offset()
과 limit()
메서드를 통해 손쉽게 페이징 처리를 할 수 있습니다. 예를 들어, 10명씩 회원을 조회하려면 다음과 같이 작성할 수 있습니다.
public List<Member> findMembersWithPaging(int page, int size) {
QMember member = QMember.member;
return queryFactory.selectFrom(member)
.orderBy(member.name.asc()) // 이름 기준 오름차순 정렬
.offset(page * size) // 페이지 번호에 맞게 시작 위치 설정
.limit(size) // 페이지 크기 설정
.fetch(); // 결과 반환
}
페이징 처리 시에는 offset()
과 limit()
을 사용하여 시작 위치와 크기를 조정할 수 있습니다. 이 방식은 SQL에서의 LIMIT
과 OFFSET
과 동일하게 작동합니다.
QueryDSL을 사용하면 집계 함수(예: count()
, avg()
, max()
, min()
)도 손쉽게 사용할 수 있습니다.
public Double findAverageAge() {
QMember member = QMember.member;
return queryFactory.select(member.age.avg()) // 나이 평균 계산
.from(member)
.fetchOne(); // 결과 반환
}
위 예시에서 평균 나이를 구하는 쿼리를 QueryDSL로 작성한 모습입니다. avg()
는 SQL의 AVG()
와 동일하게 동작하며, 평균 값을 반환합니다.
QueryDSL은 서브쿼리를 사용하여 복잡한 조건을 표현할 수 있습니다. 서브쿼리는 subQuery()
메서드를 사용하여 생성하고, in()
, exists()
, notExists()
등의 연산자와 함께 사용합니다.
// 평균 나이보다 많은 회원 조회
QMember member = QMember.member;
NumberPath<Integer> avgAge = Expressions.numberPath(Integer.class, "avgAge");
JPAQuery<Member> query = queryFactory.selectFrom(member)
.where(member.age.gt(
JPAExpressions.select(avgAge)
.from(member)
.select(member.age.avg())
));
List<Member> results = query.fetch();
QueryDSL은 다양한 조건절을 지원합니다.
eq
, ne
, gt
, lt
, goe
, loe
like
, startsWith
, endsWith
, contains
isNull
, isNotNull
in
, notIn
or
QueryDSL은 타입 세이프하고 동적인 쿼리를 작성할 수 있는 훌륭한 도구입니다.
JPA를 사용할 때 쿼리 작성 시 발생할 수 있는 오류를 방지하고, 보다 유연하고 안전한 쿼리를 만들 수 있습니다.
타입 안전성과 코드 완성 기능을 제공하여 쿼리 작성의 생산성을 크게 향상시킬 수 있습니다.