본문 바로가기
Framework/Spring Boot

[Spring Boot] QueryDSL 입문: 타입 세이프(Type-safe)한 동적 쿼리의 끝판왕

반응형

10단계에서 배운 Spring Data JPA의 Query Method와 @Query는 훌륭했지만, 실무의 거친 요구사항을 만나면 한계가 명확하게 드러납니다. 쇼핑몰의 상품 검색창을 생각해 봅시다. 사용자는 '상품명'만 검색할 수도 있고, '가격 범위'만 필터링할 수도 있으며, '카테고리'와 '브랜드'를 동시에 선택할 수도 있습니다. 즉, 조건이 있을 수도 있고 없을 수도 있는 '동적 쿼리(Dynamic Query)'가 필요합니다. 과거 MyBatis 시절에는 XML 파일에 <if test="name != null"> 같은 태그를 수십 개 도배하여 쿼리 문자열(String)을 조립했습니다. JPA @Query에서 문자열로 동적 쿼리를 짜다 보면 띄어쓰기 하나 잘못해서 서버가 켜진 뒤 고객이 검색 버튼을 누르는 순간 에러가 터집니다(런타임 에러). 이 모든 고통을 완벽하게 끝내버리고 "자바 코드로 쿼리를 짜서, 컴파일 시점에 SQL 에러를 잡아내는 마법"을 선사한 기술이 바로 QueryDSL입니다.

이번 시리즈에서는 "우아한 백엔드 개발의 상징" QueryDSL의 기본 개념과 완벽 세팅 가이드에 대하여 다뤄보겠습니다.

🚨 1. JPQL의 한계와 QueryDSL의 탄생

우선 @Query(JPQL)로 동적 쿼리를 짤 때의 지옥을 감상해 봅시다.

// 과거의 동적 쿼리 (MyBatis나 순수 JPQL 문자열 조립)
String jpql = "SELECT u FROM User u WHERE 1=1";
if (name != null) {
    jpql += " AND u.name = '" + name + "'"; // 띄어쓰기 실수하면 런타임 에러 폭발!
}
if (age != null) {
    jpql += " AND u.age >= " + age;
}

문자열(String)은 컴퓨터 입장에서 그냥 글자일 뿐입니다. u.namee라고 오타를 내도 자바 컴파일러는 알지 못합니다. 개발자가 가장 사랑하는 에러는 '컴파일 에러(빨간 줄)'이고, 가장 저주하는 에러는 '런타임 에러'입니다. QueryDSL은 쿼리를 문자열이 아닌 '자바 코드(메서드)'로 짜게 만들어 주어, 오타가 나면 즉시 빨간 줄을 그어줍니다.


⚙️ 2. Q-클래스(Q-Class)의 정체와 생성 원리

QueryDSL을 쓰려면 가장 먼저 Q-Class라는 것을 만들어야 합니다. 우리가 만든 User.java 엔티티를 QueryDSL 플러그인(APT)이 싹 긁어다가 QUser.java라는 붕어빵을 구워냅니다. 이 Q-클래스 안에는 테이블의 컬럼들이 자바의 필드로 예쁘게 매핑되어 있습니다.

Gradle 빌드 설정에서 annotationProcessor "com.querydsl:querydsl-apt"를 추가하고 빌드를 돌리면 build/generated 폴더에 QUser가 생성됩니다.


🪄 3. 자바 코드로 쿼리를 짜는 경이로운 경험

세팅이 끝났다면 이제 JPAQueryFactory를 사용하여 실제 쿼리를 작성해 봅니다. 아까 보았던 끔찍한 문자열 조립 코드가 얼마나 아름답게 변하는지 확인해 보세요.

import static com.example.domain.QUser.user; // Q클래스를 static import 하면 코드가 극도로 짧아집니다.

@Repository
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {

    private final JPAQueryFactory queryFactory; // QueryDSL의 핵심 엔진

    public List<User> searchDynamicUsers(String nameCond, Integer ageCond) {
        return queryFactory
                .selectFrom(user) // SELECT * FROM user
                .where(
                    nameEq(nameCond), // WHERE 조건들을 쉼표(,)로 넘기면 자동으로 AND 결합이 됩니다.
                    ageGoe(ageCond)
                )
                .orderBy(user.createdAt.desc()) // 정렬도 완벽한 자동완성(IntelliSense) 지원!
                .fetch();
    }

    // 🌟 쉼표(,) AND 결합의 마법: 메서드가 null을 반환하면 QueryDSL이 알아서 조건에서 제외해 줍니다! (동적 쿼리의 핵심)
    private BooleanExpression nameEq(String nameCond) {
        return nameCond != null ? user.name.eq(nameCond) : null;
    }

    private BooleanExpression ageGoe(Integer ageCond) {
        return ageCond != null ? user.age.goe(ageCond) : null; // goe = Greater or Equal (>=)
    }
}

🎯 4. 실무 도입의 절대적 장점 요약

  • 타입 안정성(Type-safe): user.namme라고 오타를 내는 순간 컴파일러가 빨간 줄을 쫙 긋고 빌드를 막아버립니다. 운영 서버에서 쿼리 에러가 터질 확률이 0%에 수렴합니다.
  • 자동 완성(Auto-complete): user.만 쳐도 IDE가 테이블의 컬럼 목록을 쫘르륵 보여줍니다. DB 스키마를 외울 필요가 없습니다.
  • 동적 쿼리 처리의 예술성: BooleanExpression을 메서드로 뽑아내어 조합하면(isServiceable() = isAdult().and(isNotBanned())), 조건문 자체가 하나의 완벽한 비즈니스 문장으로 읽힙니다.

🎯 5. 마무리 및 다음 단계

지금까지 오타와 띄어쓰기의 지옥이었던 JPQL 문자열 조립을 버리고, 완벽한 자바 컴파일러의 보호 아래에서 예술적이고 우아하게 동적 쿼리를 찍어내는 QueryDSL의 웅장한 아키텍처에 대해 다루어 보았습니다. QueryDSL을 자유자재로 다루는 순간, 여러분은 대한민국 백엔드 상위 10%의 쿼리 작성 능력을 갖추게 됩니다.

이로써 애플리케이션의 코딩(Controller, Service, Repository, QueryDSL) 단계는 실무 레벨로 완성되었습니다. 그런데 코드를 아무리 잘 짜도, 데이터베이스(DB)로 향하는 '고속도로'가 막혀있다면 시스템은 마비됩니다. 수천 명의 유저가 동시에 접속했을 때 DB 연결 통로가 모자라 요청이 튕겨 나가는 사태를 막기 위해 우리는 '커넥션 풀(Connection Pool)'이라는 방파제를 세워야 합니다. 이어지는 12단계 포스팅에서는 "전 세계 스프링 부트가 기본으로 채택한 극강의 성능 튜닝 포인트!" 데이터베이스 커넥션 풀(HikariCP)의 원리 파악과 장애를 예방하는 완벽한 실무 튜닝 공식에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형