JPA

왜 실무에서는 Criteria를 안 쓰고 QueryDSL을 선택할까? (JPA 쿼리 선택 기준 정리)

경딩 2026. 3. 1. 02:37

JPA  가 지원하는 다양한 쿼리 방법 - JPQL부터 QueryDSL까지

JPA를 처음 배우면 em.find() 로 단건 조회하는 것부터 시작한다. 엔티티를 조회하고 연관관계를 따라 객체 그래프 탐색하는 방식은 매우 직관적이다. 

 

하지만 실무로 넘어가면 상황이 완전히 달라진다. 다양한 검색 조건, 동적필터, 조인, 정렬, 페이징, 집계까지 단순 조회만으로 해결할 수 없는 복잡한 쿼리가 필수적으로 등장한다.

 

이 글을 읽고 나면 네 가지를 명확히 판단할 수 있게 된다.

  • JPA 가 지원하는 쿼리 방법의 종류와 각각의 실무 활용도를 이해하게 된다.
  • JPQL이 왜 "객체 지향 SQL"이라고 불리는지 본질적인 의미를 알게 된다.
  • Criteria 가 이론적으로 좋지만 실무에서 외면받는 이유를 납득하게 된다.
  • QueryDSL이 단순한 편의 도구가 아니라 설계 도구로 평가받는 이유를 이해하게 된다.

JPA가 지원하는 쿼리 방법

JPA는 다음과 같은 다양한 쿼리 방법을 지원한다.

 

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL
  • JDBC API 직접 사용 (MyBatis, SpringJdbcTemplate 등)

이 중 Criteria와 QueryDSL은 새로운 쿼리 언어가 아니라 자바 코드로 JPQL 을 생성하는 (Generator) 계열 도구다.

실무에서는 대부분의 쿼리가 JPQL과 QueryDSL로 해결되며, 특정 데이터베이스에 종속적인 기능이 필요할 때만 네이티브 SQL이나 MyBatis  등을 함께 사용한다.


JPQL - 객체를 대상으로 하는 객체 지향 SQL 

em.find()나 객체 그래프 탐색만으로는 복잡한 검색 조건을 처리할 수 없다. 

"18세 이상 회원 전체 조회" 같은 요구사항이 있다면 결국 WHERE  조건이 포함된 쿼리가 필요하다.

 

그렇다고 모든 데이터를 애플리케이션으로 가져온 뒤 메모리에서 필터링하는 것은 현실적으로 불가능하다. 네트워크 비용과 메모리 사용량이 폭발적으로 증가하고 DB 성능도 나오지 않는다. 결국 필요한 데이터만 DB에서 조회하려면 조건이 포함된 쿼리를 DB  레벨에서 실행해야 한다.

 

이 문제를 해결하기 위해 등장한 것이  JPQL이다.

List<Member> resultList = em.createQuery(
        "select m from Member m where m.name like '%kim%'",
        Member.class
).getResultList();

 

SQL과 문법이 매우 유사하지만 가장 본질적인 차이는 하나다. 테이블이 아니라 엔티티 객체를 대상으로 쿼리한다.

실행 로그를 보면  JPQL 이 먼저 주석으로 나오고, 엔티티 매핑 정보를 기반으로 실제 SQL  로 변환되어 실행된다.

/* select m from Member m where m.name like '%kim%' */
select m1_0.id, m1_0.age, m1_0.name
from MBR m1_0
where m1_0.name like '%kim%' escape ''

 


Criteria — 이론적으로 완벽하지만 실무에서 외면받는 이유

JPQL의 가장 큰 한계는 문자열 기반이라는 점이다. 동적 쿼리를 작성할 때 조건을 상황에 따라 붙였다 뗏다 해야 하는데, 오타 위험,  WHERE/AND 순서 관리, 가독성 저하 같은 문제가 따라온다.

 

이를 해결하기 위해 등장한 것이  JPA Criteria다.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = query.from(Member.class);

CriteriaQuery<Member> cq = query.select(m);

String name = "test";
if (name != null) {
    cq = cq.where(cb.equal(m.get("name"), "kim"));
}

List<Member> resultList = em.createQuery(cq).getResultList();

 

자바 코드 기반이라 컴파일 타임 검증이 가능하고 동적 쿼리 작성도 용이하다. 장점은 분명하다.

 

하지만 실무에서는 거의 사용도지 않는다. 이유는 단순히 복잡해서가 아니다. 

컴파일 안전성보다 가독성과 유지보수 비용이 더 중요하기 때문이다. Criteria 로 복잡한 쿼리를 작성하면 코드만 보고 어떤  SQL이 실행되는지 직관적으로 파악하기 어렵다. 이론적으로 우수하지만 실무 생산성 관점에서는 불리한 기술이라는 평가를 받는 이유다.

 


QueryDSL — 가독성과 타입안전성을 동시에 해결한 실무 표준

 

Criteria의 단점을 실질적으로 해결한 것이 QueryDSL이다.

JPAQueryFactory query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list = query
        .selectFrom(m)
        .where(m.age.gt(18))
        .orderBy(m.name.desc())
        .fetch();

 

문자열이 아닌 자바 코드로 JPQL 을 작성하면서도 SQL 과 유사한 형태를 유지하기 때문에 가독성이 훨씬 좋다. 컴파일 타임 문법 검증, 타입 안전성, 동적 쿼리 작성의 용이성, 리팩토링 안정성까지 Criteria 의 장점을 그대로 가져오면서 단점을 극복했다.

 

한 가지 중요한 점은 QueryDSL이 새로운 쿼리 언어가 아니라는 것이다. QueryDSL  은 JPQL을 타입 안정하게 생성해주는  DSL 이다. 따라서 QueryDSL을 제대로 사용하려면 JPQL 에 대한 이해가 선행되어야 한다.


네이티브 SQL — DB 종속 기능이 필요할 때

JPQL로 해결할 수 없는 특정 데이터베이스 종속 기능이 필요한 경우 네이티브 SQL 을 사용한다.

Oracle의 CONNECT BY나 특정 DB 성능 튜닝 쿼리가 대표적인 예다.

em.createNativeQuery("select id, name from Member", Member.class)
        .getResultList();

 

데이터베이스 종속성이 생기므로 유지보수 관점에서 신중하게 사용해야 한다.

 


JDBC 직접 사용 시 주의사항 — flush를 놓치지 말자

JPA를 사용하면서 JDBC, MyBatis, SpringJdbcTemplate을 함께 사용하는 것은 가능하다. 단 반드시 알아야 할 중요한 동작원리가 있다.  JPA를 우회하는 SQL은 영속성 컨텍스트가 인식하지 못한다.

 

Member member = new Member();
member.setName("test1");
em.persist(member);

// JPA를 우회하는 SQL 실행 전 수동 flush
// createNativeQuery 의 경우 jpa 가 자동 플러시함
em.flush();

List<Member> resultList = em.createNativeQuery(
        "select id, name from Member", Member.class)
        .getResultList();

 

JPQL은 실행 전에 자동으로 flush가 발생하지만, JDBC를 직접 사용할 경우 JPA가 쿼리 실행을 인지하지 못하기 때문에 명시적인 flush 처리가 필요하다. 이 부분을 놓피면 정합성 문제로 이어질 수 있다.


 

정리 — 실무에서의 JPA 쿼리 선택 기준

방법 특징 실무 활용
JPQL 객체 지향 SQL, 문자열 기반 대부분의 쿼리
Criteria 자바 코드 기반, 가독성 낮음 거의 미사용
QueryDSL 자바 코드 기반, 가독성 높음 동적 쿼리
네이티브 SQL DB 종속 기능 필요 시 필요할 때
JDBC / MyBatis JPA 우회, flush 주의 필요할 때

 

가장 현실적인 접근 순서는  JPQL을 먼저 정확히 이해하고, 동적 쿼리와 유지보수 복잡도가 증가하는 지점에 QueryDSL 을 도입하는 것이다. 중요한 것은 특정 기술을 맹목적으로 선택하는 것이 아니라 쿼리의 복잡도, 유지보수 비용, 팀 생산성을 기준으로 적절한 도구를 선택하는 설계 관점이다.