1. QueryDSL이란?
SQL 형식의 쿼리를 Type-Safe 하게 생성할 수 있도록 하는 DSL을 제공하는 라이브러리
2. QueryDSL의 목적
ORM(Object-Relationla-Mapping) 프레임워크의 가장 어려운 설계 선택 중 하나는 정확하고 유형 안전한 쿼리를 구축하기 위한 API입니다.
가장 널리 사용되는 자바 ORM 프레임워크 중 하나인 하이버네이트(JPA 표준)는 SQL과 매우 유사한 문자열 기반 쿼리 언어 HQL(JPQL)을 제안합니다.
이 접근 방식의 단점은 타입 안전성의 부족과 정적 쿼리 검사의 부재입니다.
또한 더 복잡한 경우(예를 들어, 일부 조건에 따라 쿼리를 런타임에 구성해야 하는 경우) HQL 쿼리를 구축하려면 일반적으로 매우 안전하지 않고 오류가 발생하기 쉬운 문자열의 연결이 포함됩니다.
JPQL은 표준 스펙이며 실제로 존재하지 않습니다.
하이버네이트 같은 구현체들이 JPQL 표준 스펙에 맞춰진 기능을 제공하며, 몇 가지 기능들을 더 제공하는 경우가 있습니다.
2.1 HQL, JPQL의 단점
- 타입 안전성의 부족
- 문자열 기반의 쿼리 언어로, 컴파일 시점에 타입을 체크할 수 없기 때문에 런타임 오류가 발생할 가능성이 있습니다.
- 정적 쿼리 검사의 부재
- HQL과 JPQL은 문자열로 쿼리를 작성하기 때문에, 컴파일 시점에 쿼리의 유효성을 검사할 수 없습니다. 따라서 쿼리 작성 시 오타나 잘못된 필드 참조 등의 오류를 런타임에서 발견할 수 없습니다.
- 복잡한 HQL 쿼리 구축에 있어 안전하지 못함
- 문자열 연결을 통해 쿼리를 구성하면 외부 입력을 그대로 쿼리에 포함시키는 경우가 발생할 수 있습니다. 이로 인해 악의적인 사용자가 입력을 조작하여 SQL 인젝션 공격을 시도할 수 있습니다.
QueryDSL을 사용하여 이러한 단점들을 보완합시다.
3. QueryDSL 생성
QueryDSL을 사용하기 위해선 먼저, QClass가 필요합니다.
QClass는 쿼리를 작성할 때 사용하는 메타모델 클래스입니다.
- 쿼리를 작성할 때 컴파일 시점에 필드 이름과 타입을 확인할 수 있어, 타입 안전성이 보장됩니다. (QClass 타입 instance를 만들고 사용합니다.)
- 문자열로 작성된 쿼리와 달리 QClass를 사용하면 오타나 잘못된 타입 사용을 방지할 수 있습니다.
- QClass는 QueryDSL의 annotation processor에 의해 자동 생성됩니다.
- 예를 들어, User 엔티티 클래스가 있다면, QueryDSL은 이를 기반으로 QUser 클래스를 생성합니다. QUser 클래스에는 User 엔티티의 모든 필드에 대한 정적 메타데이터가 포함됩니다.
3.1 QClass 생성
QClass를 생성하기 위해서는 Entity 클래스가 필요합니다.
package com.example.querydsl.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
@Entity
@Getter
public class BlogPost {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String body;
@ManyToOne()
private User user;
}
package com.example.querydsl.entity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
@Entity
@Getter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String login;
private Boolean disabled;
@OneToMany(cascade = CascadeType.PERSIST, mappedBy = "user")
private Set<BlogPost> blogPosts = new HashSet<>();
}
저는 User, BlogPost 2개의 Entity Class를 만들었습니다.
그 후,
Gradle에서
1. Clean
2. Build
를 클릭하면,
Project/build/generated/sources/annotationProcessor/java/main/~
에 QClass가 생성된 것을 확인할 수 있습니다.
3.2 DB구현
<user Table>
<blog_post Table>
저는 MySQL을 사용하였습니다.
각 테이블에 더미 데이터를 넣어주었습니다.
3.3 QueryClass 구현
package com.example.querydsl.service;
import com.example.querydsl.entity.QBlogPost;
import com.example.querydsl.entity.QUser;
import com.example.querydsl.entity.User;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class QueryService {
private final JPAQueryFactory queryFactory;
@Autowired
public QueryService(EntityManager entityManager) {
this.queryFactory = new JPAQueryFactory(entityManager);
}
public List<User> findUsers(String login, Boolean disabled) {
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
if (login != null && !login.isEmpty()) {
builder.and(user.login.eq(login));
}
if (disabled != null) {
builder.and(user.disabled.eq(disabled));
}
return queryFactory.selectFrom(user)
.where(builder)
.fetch();
}
@PostConstruct
public void executeQueries() {
QUser user = QUser.user;
List<User> usersWithLoginUser2 = queryFactory.selectFrom(user)
.where(user.login.eq("user2"))
.fetch();
List<User> usersWithDisabledIsTrue = queryFactory.selectFrom(user)
.orderBy(user.id.desc())
.where(user.disabled.eq(true))
.fetch();
QBlogPost blogPost = QBlogPost.blogPost;
List<User> usersWithBodyOfPost6 = queryFactory.selectFrom(user)
.where(user.id.in(
JPAExpressions.select(blogPost.user.id)
.from(blogPost)
.where(blogPost.title.eq("Title 6"))))
.fetch();
List<User> userWithUser2AndTrue = findUsers("user2", false);
// log
userWithUser2AndTrue.forEach(user2AndTrue -> log.info("User2, true : " + user2AndTrue.getId()));
for (User loginUser : usersWithLoginUser2) {
StringBuilder sb = new StringBuilder();
sb.append("Login as User2 : ")
.append(loginUser.getId())
.append(", ")
.append(loginUser.getLogin());
log.info(String.valueOf(sb));
}
for (User disabledUser : usersWithDisabledIsTrue) {
StringBuilder sb = new StringBuilder();
sb.append("User with Disabled True : ")
.append(disabledUser.getId())
.append(", ")
.append(disabledUser.getLogin())
.append(", ")
.append(disabledUser.getDisabled());
log.info(String.valueOf(sb));
}
for (User UserPost6 : usersWithBodyOfPost6) {
StringBuilder sb = new StringBuilder();
sb.append("User with Title 6 : ")
.append(UserPost6.getId());
log.info(String.valueOf(sb));
}
}
}
<Log history>
4. JPA vs JPQL vs QueryDSL
간단한 CRUD | 간단한 Query | 복잡한 쿼리(동적쿼리 등) | |
JPA | O | X | X |
JPQL | X | O | O |
QueryDSL | X | O | O |
JPQL
장점:
- @Query annotation을 사용하여 간단하게 문자열 기반 Query를 작성할 수 있습니다.
- SQL과 문법이 유사하여 학습 곡선이 낮습니다.
단점
- 문자열 기반 쿼리이기에 타입 체크가 불가능하고, 런타임 시에 오류를 발견할 수 있습니다.
- SQL Injection 공격에 취약합니다.
- 동적 쿼리를 작성할 때 불편하고, 복잡한 쿼리를 작성하는 데 어려움이 있습니다.
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.login = :login")
List<User> findByLogin(@Param("login") String login);
}
QueryDSL
장점
- 타입 안전성을 제공하여 컴파일 단계에서 오류를 발견할 수 있습니다.
- 동적 쿼리를 작성할 때 편리하고, 복잡한 쿼리도 쉽게 작성할 수 있습니다.
- 메타모델을 사용하여 IDE의 자동 완성 기능을 활용할 수 있습니다.
단점
- QueryDSL을 사용하기 위해 추가적인 설정이 필요합니다. (예: Custom Repository 구현)
- 초기 설정 및 학습 곡선이 조금 높을 수 있습니다.
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.core.BooleanBuilder;
public class UserRepositoryImpl implements UserRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Autowired
public UserRepositoryImpl(EntityManager entityManager) {
this.queryFactory = new JPAQueryFactory(entityManager);
}
@Override
public List<User> findUsers(String login, Boolean disabled) {
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
if (login != null && !login.isEmpty()) {
builder.and(user.login.eq(login));
}
if (disabled != null) {
builder.and(user.disabled.eq(disabled));
}
return queryFactory.selectFrom(user)
.where(builder)
.fetch();
}
}
결론
JPA
- 간단한 CRUD 작업에 적합합니다.
- 객체-관계 매핑(ORM)을 통해 데이터베이스와 상호 작용합니다.
JPQL
- 간단한 쿼리와 비교적 단순한 복잡성의 쿼리에 적합합니다.
- 문자열 기반의 쿼리 언어로, 엔티티 객체를 대상으로 합니다. 타입 안전성이 떨어지며, SQL Injection에 취약합니다.
QueryDSL
- 복잡한 쿼리와 동적 쿼리에 적합합니다.
- 타입 안전성을 제공하며, 동적 쿼리 작성이 용이합니다. 초기 설정이 필요합니다.
'Backend' 카테고리의 다른 글
<Backend> Spring / Spring Security (0) | 2024.07.11 |
---|---|
<Backend> Spring / Batch (0) | 2024.07.10 |
<Backend> Java / 상속 (0) | 2024.04.05 |
<Backend> Java / Class (3) 접근제한자와 변수의 타입 (0) | 2024.04.04 |
<Backend> Java / Class (2) this와 메소드 (0) | 2024.04.04 |