JPA를 처음 사용할 때는 JpaRepository에서 제공하는 Query Method만으로도 대부분의 조회가 가능했다.
그런데 조회 조건이 하나둘 늘어나기 시작하면서 메서드 이름은 점점 길어졌고, 그 과정에서 여러 테이블을 함께 조회하거나 조건이 동적으로 바뀌는 순간 한계를 느끼게 되었다.
결국 "JPA에서 복잡한 조인이나 동적 조건에 대한 조회를 어떻게 해결할 수 있는가?"를 고민하게 되었고, 해당 질문에 대한 답으로 객체 쿼리 언어(Object Query Language)를 접하게 되었다.
객체 쿼리 언어는 데이터베이스의 테이블이 아니라 엔티티 객체를 대상으로 조회한다는 점이 가장 큰 특징이다.
SQL과 비슷하게 생긴 것도 있고, 자바 코드만으로 쿼리를 작성하는 방식도 있으며, 타입 안정성을 제공하는 방식도 존재한다.
이번 글에서는 다음과 같은 객체 쿼리 언어를 다룬다.
JPA는 객체와 데이터베이스 사이의 매핑을 담당한다.
따라서 JPA를 사용한다면 조회 역시 엔티티를 기준으로 작성하는 것이 자연스럽다.
물론 Native SQL을 사용할 수도 있지만, 그렇게 되면 특정 데이터베이스의 문법에 의존하게 되고 JPA가 제공하는 변경 감지(Dirty Check)나 객체 중심 설계의 장점을 충분히 활용하기 어렵다.
그래서 JPA는 객체를 기준으로 조회할 수 있는 다양한 객체 쿼리 언어를 제공한다.
JPQL은 JPA에서 기본적으로 제공하는 객체 쿼리 언어이다. @Query 어노테이션 등을 사용해서 간단하게(별도의 의존성 추가 없이) 사용할 수 있다.
JAVA Persistence API의 일부로 제공되며, SQL과 문법이 매우 유사하지만 테이블이 아닌 엔티티를 대상으로 조회한다는 점에서 차이가 있다. 또한 데이테베이스에 종속되지 않는 객체 지향적인 조회를 지원한다.
다음은 JPQL의 예제이다.
@Query("""
SELECT m.id, m.username
FROM member m
WHERE m.username = :username
""")
List<MemberDto> findMember(String username);
JPQL은 SQL과 문법이 비슷해서 익히기 쉽고 대부분의 조회를 해결할 수 있다.
하지만 문자열 기반으로 작성되기 때문에 오타를 컴파일 시점에서 확인할 수 없고, 검색 조건이 계속 바뀌는 동적 쿼리를 작성하기 시작하면 코드가 빠르게 복잡해진다.
결국 "타입 안정성도 갖추면서 동적으로 쿼리를 작성할 수 있는 방법이 없을까?"하는 고민에서 등장한 것이 바로 Criteria API이다.
JPQL의 한계를 해결하기 위해 JPA는 Criteria API를 제공한다.
Criteria API는 문자열 대신 자바 코드를 이용해 쿼리를 생성하는 방식으로, 컴파일 시점에 타입 검사를 받을 수 있으며 동적으로 조합하기 쉽다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
cq.select(member)
.where(cb.greateThanOrEqualTo(member.get("age"), age));
List<Member> result = em.createQuery(cq).getResulList();
직접 작성해보면 문자열 기반으로 작성하는 JPQL보다는 안전하다는 장점이 분명하다는 것을 알 수 있다.
하지만 간단한 조회 하나를 작성하는 데도 코드가 여러 줄로 늘어나고, 실제 실행될 쿼리가 한눈에 들어오지 않는다는 점이 아쉽다.
그래서 실무에서는 Criteria API보다는 같은 타입 안정성을 제공하면서도 문법이 훨씬 직관적인 QueryDSL을 사용하는 경우가 많다.
Criteria API는 코드가 지나치게 복잡하고 가독성이 떨어진다는 단점이 있다.
QueryDSL은 이러한 문제를 해결하기 위해 등장한 오픈 소스 라이브러리로 자바 코드만으로 SQL과 유사한 형태의 쿼리를 작성할 수 있도록 지원한다.
QueryDSL은 컴파일 시점에 생성되는 Q 클래스를 이용하여 엔티티의 필드에 접근하기 때문에 문자열 기반 쿼리에서 발생할 수 있는 오타를 방지할 수 있으며, IDE의 자동 완성과 리팩토링 기능도 활용할 수 있다.
QMember memeber = QMember.member;
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(age))
.fetch();
Criteria API와 비교하면 동일한 타입 안정성을 제공하면서도 코드가 훨씬 간결하고 가독성이 높다.
예제를 비교하다보니 왜 QueryDSL이 Criteria보다 인기가 많은지 조금은 알 수 있었다.
Criteria API와 같은 타입 안정성을 제공하면서도 SQL을 읽는 것처럼 코드를 작성할 수 있었고, 조건을 메서드로 분리해 재사용하기도 훨씬 편하다.
물론 별도의 설정과 Q클래스 생성 과정은 필요하다.
하지만 동적 쿼리를 자주 작성하는 프로젝트라면, 이러한 초기 설정 비용보다 얻을 수 있는 생산성과 가독성의 이점이 훨씬 크다고 느꼈다.
Query Method만으로도 간단한 조회는 충분히 해결할 수 있다.
하지만 조회 조건이 복잡해질수록 JPQL, Criteria API, QueryDSL과 같은 객체 쿼리 언어의 필요성을 느끼게 된다.
그중에서도 실무에서는 타입 안정성과 가독성을 모두 갖춘 QueryDSL이 가장 많인 선택된느 이유를 이번 정리를 통해 조금은 이해할 수 있었다.