01 기본
1. 환경 설정과 Q클래스
2. 기본 쿼리 작성
3. 조인과 서브쿼리
02 실전
4. 동적 쿼리 ← 현재 편
5. 프로젝션과 DTO 매핑
03 심화
7. 성능 최적화
8. 실전 패턴과 면접 대비
검색 기능을 만들면 "이름은 입력했고, 나이는 안 넣었고, 지역은 선택했고..." 같은 상황이 생긴다. 조건이 있으면 적용하고 없으면 무시하는 쿼리, 이것이 동적 쿼리다. QueryDSL이 가장 빛나는 영역이기도 하다.
BooleanBuilder
BooleanBuilder는 조건을 하나씩 쌓아가는 가변 객체다.
builder 전달] AddRegion --> Execute
public List<User> search(String name, Integer minAge, String region) {
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(user.name.contains(name));
}
if (minAge != null) {
builder.and(user.age.goe(minAge));
}
if (region != null) {
builder.and(user.region.eq(region));
}
return queryFactory
.selectFrom(user)
.where(builder)
.fetch();
}
BooleanBuilder는 빈 상태에서 시작해서, 조건이 존재할 때만 and()로 추가한다. 최종적으로 where에 전달하면, 추가된 조건들이 AND로 연결된 하나의 조건이 된다. 조건이 하나도 없으면 where가 생략된다.
OR 조건 조합
AND만 쓰는 게 아니다. OR 조건을 섞어야 할 때도 있다.
BooleanBuilder builder = new BooleanBuilder();
// 이름이 "kim"이거나 "lee"인 사용자
builder.or(user.name.eq("kim"));
builder.or(user.name.eq("lee"));
// AND와 OR를 함께
BooleanBuilder andCondition = new BooleanBuilder();
andCondition.and(user.age.goe(20));
andCondition.and(builder); // OR 조건을 AND로 묶기
OR 조건이 복잡해지면 BooleanBuilder를 중첩해서 쓸 수 있다. 다만 이 지점부터 가독성이 급격히 떨어진다.
where 다중 파라미터 패턴
QueryDSL의 where는 가변인자를 받는다. null이 전달되면 해당 조건을 무시한다. 이 특성을 활용하면 BooleanBuilder 없이도 동적 쿼리를 작성할 수 있다.
public List<User> search(String name, Integer minAge, String region) {
return queryFactory
.selectFrom(user)
.where(
nameContains(name),
ageGoe(minAge),
regionEq(region)
)
.fetch();
}
private BooleanExpression nameContains(String name) {
return name != null ? user.name.contains(name) : null;
}
private BooleanExpression ageGoe(Integer minAge) {
return minAge != null ? user.age.goe(minAge) : null;
}
private BooleanExpression regionEq(String region) {
return region != null ? user.region.eq(region) : null;
}
각 조건을 별도 메서드로 분리한다. 값이 null이면 null을 반환하고, where가 알아서 무시한다. 이 방식의 장점은 뚜렷하다.
- 가독성 : 쿼리 본문에 if문이 없다. 어떤 조건들이 적용되는지 한눈에 보인다.
- 재사용 :
nameContains()는 다른 쿼리에서도 그대로 쓸 수 있다. - 조합 :
BooleanExpression끼리and(),or()로 조합할 수 있다.
// 조건 조합 예시
private BooleanExpression nameOrRegion(String name, String region) {
BooleanExpression nameExpr = nameContains(name);
BooleanExpression regionExpr = regionEq(region);
if (nameExpr == null) return regionExpr;
if (regionExpr == null) return nameExpr;
return nameExpr.or(regionExpr);
}
BooleanBuilder vs where 다중 파라미터
두 방식의 차이를 비교하면 이렇다.
BooleanBuilder
- 가변 객체. 조건을 순서대로 쌓는다.
- if문이 메서드 본문에 들어간다.
- 복잡한 OR 조합에 유리하다.
- 조건 재사용이 어렵다.
where 다중 파라미터
- 조건별 메서드 분리. 불변
BooleanExpression을 반환한다. - 쿼리 본문이 깔끔하다.
- 조건을 다른 쿼리에서 재사용할 수 있다.
- 실무에서 권장되는 방식이다.
일반적으로 where 다중 파라미터 방식을 기본으로 쓰고, OR 조건이 복잡하게 중첩되는 경우에만 BooleanBuilder를 보조적으로 활용하는 것이 좋다.
조건 메서드의 네이밍은 필드명 + 연산자 패턴이 읽기 좋다. nameContains, ageGoe, regionEq, createdAtAfter 같은 식이다. 이렇게 하면 메서드 이름만으로 어떤 조건인지 바로 알 수 있다.
검색 조건 객체 활용
파라미터가 많아지면 검색 조건을 별도 객체로 묶는 것이 깔끔하다.
public record UserSearchCondition(
String name,
Integer minAge,
Integer maxAge,
String region
) {}
이 조건 객체를 받아서 쿼리를 작성한다.
public List<User> search(UserSearchCondition condition) {
return queryFactory
.selectFrom(user)
.where(
nameContains(condition.name()),
ageGoe(condition.minAge()),
ageLoe(condition.maxAge()),
regionEq(condition.region())
)
.fetch();
}
메서드 시그니처가 깔끔해지고, 검색 조건이 늘어나도 메서드 파라미터를 바꿀 필요가 없다. 검색 조건 클래스 설계에 대한 자세한 내용은 실전 패턴과 면접 대비 편에서 다룬다.
자주 하는 실수
BooleanExpression끼리 and()나 or()로 직접 조합할 때, 한쪽이 null이면 NPE가 발생한다. where 파라미터로 전달할 때는 null이 무시되지만, 직접 조합할 때는 null 체크가 필수다.
```mermaid
flowchart TD
Start([조건 A와 B 결합]) --> CheckA{A가 null?}
CheckA -- Yes --> ReturnB[B 반환]
CheckA -- No --> CheckB{B가 null?}
CheckB -- Yes --> ReturnA[A 반환]
CheckB -- No --> Combine[A.and B 결합]
Combine --> End([결과 반환])
ReturnA --> End
ReturnB --> End
```
```java
// 위험: nameExpr이 null이면 NPE
nameExpr.and(regionExpr);
// 안전: null 체크 후 조합
if (nameExpr != null && regionExpr != null) {
return nameExpr.and(regionExpr);
}
```
[!DANGER] BooleanBuilder를 초기값 없이 or()로 시작
빈 BooleanBuilder에 바로 or()를 호출하면 의도치 않게 전체 조회가 될 수 있다. 조건이 하나도 없는 BooleanBuilder는 where에서 무시되기 때문이다. 의도한 것이 아니라면 최소한 하나의 기본 조건을 설정하거나, 조건이 비어있는지 확인하는 로직을 추가해야 한다.
[!DANGER] 모든 조건이 null일 때 전체 조회
where 다중 파라미터 방식에서 모든 조건 메서드가 null을 반환하면 where가 통째로 빠진다. 즉 테이블 전체를 조회한다. 의도한 것이라면 괜찮지만, 대용량 테이블에서 실수로 조건 없이 조회하면 장애가 발생할 수 있다. limit을 항상 걸어두거나, 최소 하나의 필수 조건을 검증하는 방어 로직이 필요하다.