❌ 동적 쿼리와 문자열 쿼리의 한계
JdbcTemplate, MyBatis, JPA(JPQL)와 같은 기존 데이터 접근 기술들은 다음과 같은 공통적인 약점을 가지고 있다.
- 동적 쿼리 작성의 어려움:
- 검색 조건이 다양하게 조합되는 동적 쿼리를 작성하기가 매우 복잡하다.
- 자바 코드에서 if 문으로 SQL 문자열을 조립하거나, XML에서 복잡한 태그를 사용해야 한다.
- 문자열 기반 쿼리의 한계:
- SQL이나 JPQL은 결국 문자열이다.
- 따라서 쿼리에 오타가 있어도 컴파일 시점에는 오류를 잡을 수 없고, 애플리케이션이 실행된 후에야 런타임 오류가 발생한다.
Querydsl은 바로 이 두 가지 문제를 해결하기 위해 등장한 쿼리 빌더 라이브러리다.
🧩 Querydsl: 쿼리를 Java 코드로, Type-Safe하게
Querydsl의 핵심 아이디어는 SQL(또는 JPQL)을 문자열이 아닌 자바 코드로 작성하는 것이다.
🔎 동작 원리
- Q-Type 생성:
- 프로젝트를 컴파일하면, Querydsl은 Annotation Processor를 이용해 JPA 엔티티(@Entity)를 분석하여,
- 해당 엔티티의 메타 정보를 담은 Q-Type 클래스(예: Item → QItem)를 자동으로 생성한다.
- 자바 코드로 쿼리 작성:
- 개발자는 이 Q-Type 클래스를 사용하여 자바 코드로 쿼리를 작성한다.
- 쿼리의 모든 요소(테이블, 컬럼, 조건 등)는 자바 객체로 표현된다.
✅ 장점
- 컴파일 시점 오류 감지:
- 쿼리를 자바 코드로 작성하므로, 오타가 발생하면 컴파일 시점에 바로 발견할 수 있어, 타입 안정성이 보장된다.
- IDE 자동 완성 지원:
- Q-Type을 통해 엔티티의 필드들이 자동으로 완성되므로, 생산성이 향상되고 실수가 줄어든다.
- 동적 쿼리 해결:
- 자바의 if문과 메서드 분리 등 모든 기능을 활용하여 동적 쿼리를 매우 깔끔하고 유연하게 작성할 수 있다.
🔧 Querydsl 설정 방법
우선 build.gradle에 다음 설정을 추가하자.
dependencies {
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"
}
clean {
delete file('src/main/generated')
}
그리고 다음을 차례대로 실행하자.
- Gradle → Tasks → build → clean
- Gradle → Tasks → other → compileJava
그러면 프로젝트 파일의 build/generated/sources/annotationProcessor/java/main 하위에 Q 타입 클래스가 생성되어 있어야 한다.
📜 Querydsl 적용 예시
import static hello.itemservice.domain.QItem.item;
@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV3 implements ItemRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public JpaItemRepositoryV3(EntityManager em) {
this.em = em;
this.queryFactory = new JPAQueryFactory(em);
}
...
// 1. BooleanBuilder를 이용한 동적 쿼리
public List<Item> findAllV1(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
// QItem item = new QItem("i");
// QItem 내부에 public static final QItem item = new QItem("item"); 존재
BooleanBuilder booleanBuilder = new BooleanBuilder();
if (StringUtils.hasText(itemName)) {
booleanBuilder.and(item.itemName.like("%" + itemName + "%"));
}
if (maxPrice != null) {
booleanBuilder.and(item.price.loe(maxPrice));
}
return queryFactory
.select(item)
.from(item)
.where(booleanBuilder)
.fetch();
}
// 2. BooleanExpression를 이용한 동적 쿼리
public List<Item> findAllV2(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
return queryFactory
.select(item)
.from(item)
.where(
ItemNameLike(itemName),
maxPriceLoe(maxPrice)
)
.fetch();
}
private BooleanExpression ItemNameLike(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
private BooleanExpression maxPriceLoe(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
}
JPAQueryFactory:
- Querydsl은 JPA 쿼리인 JPQL을 자동으로 생성하고 실행한다.
- 이를 위해 JPAQueryFactory가 필요하며, 이는 JPA의 EntityManager를 통해 생성한다.
동적 쿼리를 작성하는 2가지 방법:
- BooleanBuilder 이용:
- booleanBuilder.and(), booleanBuilder.or() 등을 통해 원하는 조건을 추가하고,
- where() 절 안에 booleanBuilder를 넣으면 된다.
- BooleanExpression 이용:
- if 문 등을 통해 BooleanExpression을 반환하는 조건 메서드를 정의하고,
- where() 절 안에 해당 메서드를 넣으면 된다.
- where() 절은 파라미터로 받은 BooleanExpression들을 AND 조건으로 자동으로 조합한다.
- 또한 where() 절에 null이 전달되면 해당 조건은 자동으로 무시된다.
- 조건 메서드들은 다른 쿼리에서도 재사용될 수 있어 코드의 모듈화가 가능해진다.
📚 정리
다음 세 가지 기술은 각자의 역할을 보완하며 강력한 시너지를 낸다.
- JPA: ORM 기술을 통해 객체-테이블 매핑을 처리하여 생산성을 높인다.
- Spring Data JPA: JPA를 더욱 편리하게 사용하여, 인터페이스 선언만으로 리포지토리 구현을 자동화한다.
- Querydsl: 두 기술의 약점인 복잡한 조회와 동적 쿼리를 타입-세이프하고 깔끔하게 해결한다.
따라서 현대 스프링 기반 애플리케이션의 데이터 접근 기술에서 위 세 기술은 실무의 다양한 문제를 해결하기 위한 표준 조합으로 여겨진다.
'Spring DB' 카테고리의 다른 글
| [Spring DB] @Transactional – 주요 옵션 (0) | 2025.10.14 |
|---|---|
| [Spring DB] 스프링 트랜잭션 @Transactional 사용 시 주의점 (0) | 2025.10.05 |
| [Spring DB] 스프링 데이터 JPA (Spring Data JPA) (0) | 2025.10.04 |
| [Spring DB] JPA (Java Persistence API) (0) | 2025.10.03 |
| [Spring DB] MyBatis (0) | 2025.10.03 |