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 을 도입하는 것이다. 중요한 것은 특정 기술을 맹목적으로 선택하는 것이 아니라 쿼리의 복잡도, 유지보수 비용, 팀 생산성을 기준으로 적절한 도구를 선택하는 설계 관점이다.
'JPA' 카테고리의 다른 글
| JPQL이 있는데 왜 Querydsl을 쓸까? (0) | 2026.02.25 |
|---|---|
| [JPA] 벌크 연산 - DB는 바뀌었는데 왜 값은 그대로일까? (0) | 2026.02.23 |
| [JPA] 준영속 상태란? 실무에서 왜 중요할까? (0) | 2025.05.08 |
| 공통 인터페이스 기능 (2) | 2024.12.07 |
| 양방향 연관관계와 연관관계 주인 (2) | 2024.12.05 |