객체지향 쿼리 소개(Criteria, QueryDSL, Native Query)
객체지향 쿼리
EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있습니다. 이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있습니다.
- 식별자로 조회(EntityManager.find())
- 객체 그래프 탐색(getB(), getC())
하지만 이기능만으로 애플리케이션을 개발하기는 어렵습니다. 예를 들어 나이가 30살 이상인 회원을 모두 검색하고 싶다면 좀 더 현실적이고 복잡한 검색 방법이 필요합니다. 그러핟고 모든 회원 엔티티를 메모리에 올려두고 어플리케이션에서 30살 이상인 회원을 검색하는 것은 현실성이 없습니다. JPQL은 이런 문제를 해결하기 위해 만들어졌는데 아래와 같은 특징을 가집니다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리 입니다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않습니다.
JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회합니다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환합니다.
JPQL은 한마디로 정의하면 객체지향 SQL이라고 생각하면 됩니다. JPA가 공식 지원하는 기능은 JPQL, Criteria 쿼리, 네이티브 SQL, QueryDSL
등이 있습니다.
Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 사용할 수 있습니다.
QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크 입니다.
JDBC 직접 사용, MyBatis와 같은 SQL 매퍼 프레임워크 사용: 필요하면 JDBC를 직접 사용할 수 있습니다.
여기서 가장 중요한건 JPQL입니다. Criteria나 QueryDSL은 JPQL을 편하게 작성하도록 도아주는 빌더 클래스일 뿐입니다. 따라서 JPQL을 이해해야 나머지도 이해할 수 있습니다.
JPQL 소개
JPQL은 엔티티 객체를 조회하는 객체지향 쿼리입니다. 문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원합니다.
JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않습니다. 그리고 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있습니다. 예를들어 같은 SQL 함수라도 데이터베이스마다 사용 문법이 다른 것이 있는데, JPQL이 제공하는 표준화된 함수를 사용하면 선택한 방언에 따라 해당 데이터베이스에 맞춘 적절한 SQL 함수가 실행됩니다.
JPQL은 SQL보다 간결합니다.
엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결합니다.
회원 엔티티를 대상으로 JPQL을 사용하는 간단한 예제를 살펴보겠습니다.
@Entity(name = "Member")
public class Member{
@Column(name = "name")
private String username;
}
//JPQL 사용
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
위의 코드에서 보면 회원이름이 kim인 엔티티를 조회합니다. JPQL에서 Member는 엔티티 이름입니다. 그리고 m.username은 테이블 컬럼명이 아니고 엔티티 객체의 필드명입니다.
em.createQuery() 메소드에 실행할 JPQL과 반환할 엔티티의 클래스 타입인 Member.class를 넘겨주고 getResultList() 메소드를 실행하면 JPA는 JPQL을 SQL로 변환해서 데이터베이스를 조회합니다. 그리고 조회한 결과로 Member 엔티티를 생성해서 반환합니다.
실제로 실행한 JPQL은 아래와 쿼리문으로 전송됩니다.
select
member.id as id,
member.age as age,
member.tema_id as team,
member.name as name
from
Member member
where
member.name = 'kim'
참고로 하이버네이트 구현체가 생성한 SQL은 별칭이 너무 복잡해서 알아보기 힘듭니다.
Criteria 쿼리 소개
Criteria는 JPQL을 생성하는 빌더 클래스입니다. Criteria의 장점은 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점입니다. 예를 들어 JPQL에서 select m from membeeee m 처럼 오타가 있다고 가정해보겠습니다. 그래도 컴파일은 성공하고 애플리케이션을 서버에 배포할 수 있습니다. 문제는 해당 쿼리가 실행되는 런타임 시점에 오류가 발생한다는 점입니다. 이것이 문자기반 쿼리의 단점입니다. 반면에 Criteria는 문자가 아닌 코드로 JPQL을 작성합니다. 따라서 컴파일 시점에 오류를 발견할 수 있습니다.
문자로 작성한 JPQL보다 코드로 작성한 Criteria의 장점은 아래와 같습니다.
- 컴파일 시점에 오류를 발견할 수 있습니다.
- IDE를 사용하면 코드 자동완성을 지원합니다.
- 동적 쿼리를 작성하기 편합니다.
JPA는 2.0부터 Criteria를 지원합니다.
// 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).where(cb.equal("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
위의 쿼리를 보면 문자가 아닌 코드로 작성한 것을 확인할 수 있습니다. 안타깝게도 m.get("username")을 보면 필드 명을 문자로 작성했습니다. 만약 이 부분도 문자가 아닌 코드로 작성하고 싶다면 메타 모델을 사용하면 됩니다.
메타 모델 API에 대해 알아보겠습니다. 자바가 제공하는 어노테이션 프로세서 기능을 사용하면 어노테이션을 분석해서 클래스를 생성할 수 있습니다. JPA는 이 기능을 사용해서 Member 엔티티 클래스로부터 Member_라는 Criteria 전용 클래스를 생성하는데 이것을 메타 모델이라고 합니다. 메타 모델을 사용하면 온전한 코드만 사용해서 쿼리를 작성할 수 있습니다.
m.get("username") -> m.get(Member_.username)
이것만 보면 Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황합니다... 너무 TMI 같은 느낌이죠. 따라서 사용하기 불편한 건 물론이고 Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있습니다.
QueryDSL 소개
QueryDSL도 위에서 살펴본 Criteria처럼 JPQL 빌더 역할을 합니다. QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽습니다. 그리고 작성한 JPQL과 비슷해서 한눈에 들어옵니다. QueryDSL과 Criteria를 비교하면 Criteria는 너무 복잡합니다.
참고로 QueryDSL은 JPA 표준은 아니고 오픈소스 프로젝트입니다. 이것은 JPA뿐만 아니라 JDO, 몽고 DB, Java Collection, Lucene, Hibernate Search도 거의 같은 문법으로 지원합니다. 현재 스프링 데이터 프로젝트가 지원할 정도로 많이 기대되는 프로젝트 입니다.
QueryDSL로 작성한 코드
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;
// 쿼리, 결과조회
List<Member> members = query.from(member)
.where(member.username.eq("kim"))
.list(member);
보셨나요? 위의 Criteria와 비교했을때... 그냥 보기만해도 어떤 코드를 리턴할지 예상이 됩니다. QueryDSL도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 합니다. QMember는 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스입니다.
네이티브 SQL 소개
JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라고 합니다.
JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용해야 할 때가 있습니다. 예를 들어 오라클 데이터베이스만 사용하는 CONNECT BY 기능이나 특정 데이터베이스에서만 동작하는 SQL 힌트는 같은 것입니다. 물론 하이버네이트는 SQL 힌트 기능을 지원합니다. 이런 기능들은 전혀 표준화되어 있지 않으므로 JPQL에서 사용할 수 없습니다. 그리고 SQL은 지원하지만 JPQL이 지원하지 않는 기능도 있습니다. 이때는 네이티브 SQL을 사용하면 됩니다.
네이티브 SQL의 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야 한다는 것입니다. 따라서 데이터베이스를 변경하면 네이티브 SQL도 수정해야 합니다.
String sql = "select ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'"l
List<Member> resultList = em.createNativeQuery(sql, Member.class)
.getResultList();
네이티브 SQL은 em.createNativeQuery()를 사용하면 된다. 나머지는 API는 JPQL과 같습니다. 실행하면 직접 작성한 SQL을 데이터베이스에 전달합니다.