JPA 2.0의 형식이 안전한 동적 쿼리

Criteria API에서 동적 쿼리를 빌드하고 런타임 오류를 줄이는 방법

Pinaki Poddar, Senior Software Engineer, IBM

요약:  영속적 Java™ 오브젝트에 대한 쿼리는 컴파일러에서 쿼리의 구문 정확성을 확인할 수만 있으면 형식이 안전합니다. 버전 2.0의 JPA(Java Persistence API)에는 최초로 Java 애플리케이션에서 형식이 안전한 쿼리를 활용하는 기능과 런타임에 동적으로 쿼리를 생성할 수 있는 메커니즘을 제공하는 Criteria API가 새롭게 추가되었습니다. 이 기사에서는 Criteria API 및 관련성이 높은 Metamodel API를 사용하여 형식이 안전한 동적 쿼리를 작성하는 방법에 대해 설명합니다.

2006년에 첫 선을 보일 때부터 JPA는 Java 개발자 커뮤니티의 좋은 호응을 받았다. 이 스펙의 차기 주요 업데이트인 버전 2.0(JSR 317)은 2009년 말에 완료될 것이다(참고자료 참조). JPA 2.0에 새롭게 추가된 주요 기능 중에는 Java 언어에 대한 고유한 기능을 제공하는 Criteria API가 있다. 이 API를 사용하면 Java 컴파일러에서 컴파일 시 정확성을 확인할 수 있는 쿼리를 개발할 수 있으며 Criteria API에는 런타임에 쿼리를 동적으로 빌드할 수 있는 메커니즘도 포함되어 있다.

이 기사에서는 Criteria API 및 관련성이 높은 메타모델 개념에 대해 살펴본다. Criteria API를 사용하여 Java 컴파일러에서 런타임 오류를 줄이기 위해 정확성을 확인할 수 있는 쿼리를 개발하는 방법에 대해 설명하며 이는 문자열 기반 JPQL(Java Persistence Query Language) 쿼리와 대조되는 쿼리이다. 이 기사에서는 데이터베이스 함수를 사용하거나 템플리트 인스턴스와 일치시키는 예제 코드를 통해 프로그래밍 방식으로 쿼리를 생성하는 메커니즘의 우수성을 미리 정의된 문법을 사용하는 JPQL 쿼리와 비교하여 설명한다. 이 기사는 Java 언어 프로그래밍과 EntityManagerFactory 또는 EntityManager 등의 일반적인 JPA 사용법에 대한 기초적인 지식이 있는 사용자를 대상으로 하고 있다.

JPQL 쿼리의 제한 사항

JPA 1.0부터 JPQL이 도입되었는데 이는 JPA가 널리 사용되는 데 있어 중요한 역할을 한 강력한 쿼리 언어이다. 그럼에도 불구하고 JPQL은 한정된 문법을 사용하는 문자열 기반 쿼리 언어로, 다음과 같은 몇 가지 제한 사항을 지닌다. 주요 제한 사항 중 하나를 이해하기 위해 Listing 1의 간단한 코드를 살펴보자. 이 코드에서는 JPQL 쿼리를 실행하여 20살이 넘은 Person 목록을 선택한다.


Listing 1. 간단하지만 잘못된 JPQL 쿼리
EntityManager em = ...;
String jpql = "select p from Person where p.age > 20";
Query query = em.createQuery(jpql);
List result = query.getResultList();

이 기본 예제에서는 JPA 1.0 쿼리 실행 모델의 다음과 같은 주요 특징을 보여 준다.

  • JPQL 쿼리는 String으로 지정된다(2행).
  • EntityManager는 JPQL 문자열을 사용하여 실행 가능한 쿼리 인스턴스를 생성하는 팩토리이다(3행).
  • 쿼리 실행 결과는 형식이 지정되지 않은 java.util.List의 요소로 구성되어 있다.

하지만 이 간단한 예제에는 심각한 오류가 있다. 이 코드는 다행히 컴파일은 되지만 JPQL 쿼리 문자열의 구문이 올바르지 않기 때문에 런타임에 오류가 발생한다. Listing 1에서 2행의 올바른 구문은 다음과 같다.

String jpql = "select p from Person p where p.age > 20";

아쉽게도 Java 컴파일러에서는 이러한 오류를 감지할 수 없기 때문에 3행이나 4행에서 런타임 오류가 발생한다. 오류가 발생하는 행은 JPA 공급자가 JPQL 문법에 따라 JPQL 문자열을 구문 분석하는 작업을 쿼리 생성 중에 하는지 또는 실행 중에 하는지에 따라 달라진다.

형식이 안전한 쿼리의 장점

Criteria API의 주요 장점 중 하나는 구문 오류가 있는 쿼리를 생성할 수 없다는 점이다. Listing 2에서는 CriteriaQuery 인터페이스를 사용하여 Listing 1의 JPQL 쿼리를 다시 작성한다.


Listing 2. CriteriaQuery를 작성하는 기본 단계
EntityManager em = ...
QueryBuilder qb = em.getQueryBuilder();
CriteriaQuery<Person> c = qb.createQuery(Person.class);
Root<Person> p = c.from(Person.class);
Predicate condition = qb.gt(p.get(Person_.age), 20);
c.where(condition);
TypedQuery<Person> q = em.createQuery(c); 
List<Person> result = q.getResultList();

Listing 2에서는 Criteria API의 핵심 구문과 기본적인 사용법을 볼 수 있다.

  • 1행에서는 사용 가능한 몇 가지 방법 중 하나를 사용하여 EntityManager 인스턴스를 가져온다. 

  • 2행에서는 EntityManager QueryBuilder 인스턴스를 작성한다. QueryBuilder CriteriaQuery의 팩토리이다. 

  • 3행에서는 QueryBuilder 팩토리가 CriteriaQuery 인스턴스를 작성하며 CriteriaQuery에는 제네릭 형식이 지정된다. 제네릭 형식 인수는 실행 시 이 CriteriaQuery가 리턴할 결과의 형식을 선언한다. CriteriaQuery를 생성하는 동안 Person.class와 같은 영속적 엔터티와 Object[]와 같은 좀 더 자유로운 형식의 엔터티를 포함한 다양한 종류의 결과 형식 인수를 제공할 수 있다. 

  • 4행에서는 쿼리 표현식 CriteriaQuery 인스턴스에 설정된다. 쿼리 표현식은 CriteriaQuery를 지정하기 위해 트리에서 어셈블링되는 핵심 단위 또는 노드이다. 그림 1에서는 Criteria API에 정의된 쿼리 표현식의 계층 구조를 보여 준다.



    그림 1. 쿼리 표현식의 인터페이스 계층 구조
    쿼리 표현식의 인터페이스 계층 구조 

    먼저 CriteriaQuery Person.class에서 쿼리하도록 설정되며 결과적으로 Root<Person> 인스턴스 p가 리턴된다. Root는 영속적 엔터티의 범위를 표시하는 쿼리 표현식이다. Root<T>는 "T 형식의 모든 인스턴스에 대해 이 쿼리의 값을 구한다"는 의미로 JPQL 또는 SQL 쿼리의 FROM 절과 유사하다. 또한 Root<Person>에는 제네릭 형식이 지정된다. (실제로 모든 표현식은 제네릭 형식이다.) 형식 인수는 표현식의 결과 값의 형식이므로 Root<Person> Person.class로 평가되는 표현식을 의미한다.

  • 5행에서는 Predicate를 생성한다. Predicate는 true 또는 false로 평가되는 쿼리 표현식의 또 하나의 공통 형식이다. 조건부는CriteriaQuery 및 쿼리 표현식의 팩토리인 QueryBuilder에 의해 생성된다. QueryBuilder에는 기존 JPQL 문법에서 지원되는 모든 종류의 쿼리 표현식과 기타 몇 가지 쿼리 표현식을 생성할 수 있는 API 메소드가 있다. Listing 2에서는 QueryBuilder를 사용하여 첫 번째 표현식 인수의 값이 두 번째 인수의 값보다 큰지 여부를 평가하는 표현식을 생성한다. 메소드 서명은 다음과 같다.

    Predicate gt(Expression<? extends Number> x, Number y);               
    

    이 메소드 서명은 Java 언어와 같은 강형 언어를 현명하게 사용하여 올바른 내용을 표현하고 그렇지 못한 내용을 차단하는 API를 정의하는 방법을 보여 주는 좋은 예이다. 메소드 서명은 Number 값을 가지고 있는 표현식을 다른 Number에만 비교할 수 있도록 지정한다. (예를 들어, String에는 비교할 수 없다.)

    Predicate condition = qb.gt(p.get(Person_.age), 20);
    

    계속해서 5행을 살펴보자. qb.gt() 메소드의 첫 번째 입력 인수는 p.get(Person_.age)이고, 여기서 p는 앞에서 가져온Root<Person> 표현식이다. p.get(Person_.age) 경로 표현식이다. 경로 표현식은 하나 이상의 영속적 속성을 통해 루트 표현식부터 탐색한 결과이므로 p.get(Person_.age) Person age 속성을 통해 루트 표현식 p부터 탐색한 경로 표현식을 나타낸다. Person_.age가 무엇인지 궁금하겠지만 일단은 Person age 속성을 나타내는 방법이라고만 생각하자. JPA 2.0의 새로운 Metamodel API에 대해 설명할 때 Person_.age의 의미를 자세히 다룰 것이다.

    앞에서 언급한 대로 모든 쿼리 표현식에는 표현식의 결과 값의 형식을 나타내는 제네릭 형식이 지정된다. 경로 표현식p.get(Person_.age) Person.class age 속성이 Integer(또는 int) 형식으로 선언된 경우 Integer로 평가된다. API의 안전한 형식 특성으로 인해 컴파일러 자체에서는 다음과 같이 의미 없는 비교에 대해 오류가 발생한다.

    Predicate condition = qb.gt(p.get(Person_.age, "xyz"));

  • 6행에서는 CriteriaQuery의 조건부를 WHERE 절로 설정한다.
  • 7행에서는 EntityManager CriteriaQuery 입력을 지정하여 실행 가능한 쿼리를 작성한다. 이는 JPQL 문자열을 입력으로 지정하여 실행 가능한 쿼리를 생성하는 것과 비슷하다. 하지만 CriteriaQuery 입력이 더 풍부한 형식 정보를 전달하기 때문에 익숙한 javax.persistence.Query의 확장인 TypedQuery가 리턴된다. 이름으로 짐작할 수 있듯이 TypedQuery는 실행 결과로 리턴되는 형식을 알고 있으며 다음과 같이 정의된다.

    public interface TypedQuery<T> extends Query {
                 List<T> getResultList();
    }
    

    이에 상응하는 형식이 없는 수퍼 인터페이스는 다음과 같이 반대된다.

    public interface Query {
    List getResultList();
    }

    기본적으로 TypedQuery 결과의 형식은 QueryBuilder에서 CriteriaQuery를 생성하는 동안 지정된 Person.class 형식과 같다(3행).

  • 8행에서 결과적으로 전달된 형식 정보는 최종적으로 쿼리가 실행되어 결과 목록을 가져왔을 때의 장점을 보여 준다. 결과적으로 형식이 지정된 Person 목록이 리턴되므로 결과 요소를 반복하는 동안 부가적인 캐스팅을 고려하지 않아도 되고 런타임에 발생할 수 있는 ClassCastException 오류도 최소화할 수 있다.

Listing 2에서 살펴본 간단한 예제의 기본 개념을 요약하면 다음과 같다.

  • CriteriaQuery는 기존 문자열 기반 쿼리 언어의 FROM, WHERE  ORDER BY와 같은 쿼리 절을 지정하는 데 사용되는 쿼리 표현식 노드로 구성된 트리이다. 그림 2에서는 쿼리와 관련된 절을 보여 준다. 

    그림 2. 기존 쿼리 절을 포함하는 CriteriaQuery
    쿼리 표현식의 인터페이스 계층 구조 

  • 쿼리 표현식에는 제네릭 형식이 지정되며 다음은 몇 가지 일반적인 표현식이다.
    • Root<T> FROM 절과 동등한 기능을 수행한다.
    • Predicate는 부울 값(true 또는 false)으로 평가된다. (실제로 이 표현식은 interface Predicate extends Expression<Boolean>으로 선언된다.)
    • Path<T> Root<?> 표현식에서 탐색한 영속적 속성을 나타낸다. Root<T>는 상위 항목이 없는 특수 Path<T>이다.
  • QueryBuilder CriteriaQuery 및 모든 종류의 쿼리 표현식의 팩토리이다. 

  • CriteriaQuery는 런타임 캐스팅 없이 선택한 목록의 요소에 액세스할 수 있도록 저장된 형식 정보와 함께 실행 가능한 쿼리로 전환된다.

영속적 도메인의 메타모델

Listing 2를 설명하면서 Person의 영속적 age 속성을 나타내는 Person_.age라는 보기 드문 구문에 대해 간단히 언급했었다. Listing 2 p.get(Person_.age) 구문에서는 Person_.age를 사용하여 Root<Person> 표현식 p부터 탐색하는 경로 표현식을 생성한다.Person_.age Person_ 클래스의 공용 정적 필드이며 Person_은 원래 Person 엔터티 클래스에 해당하는 인스턴스화된 정적 정식 메타모델 클래스이다.

메타모델 클래스는 영속적 클래스의 메타 정보를 설명한다. 만일 JPA 2.0 스펙에 따라 정확하게 영속적 엔터티의 메타 정보를 설명할 경우 이러한 메타모델 클래스를 정식 메타모델 클래스라고 한다. 정식 메타모델 클래스는 모든 멤버 변수가 static(및 public)으로 선언되기 때문에 정적이다. Person_.age는 그러한 정적 멤버 변수 중 하나이다. 정적 클래스는 개발 시 소스 코드 레벨에서 구체적Person_.java를 생성하여 인스턴스화하게 된다. 이러한 인스턴스화 과정을 거치고 나면 런타임 시 강형을 사용하는 대신 컴파일 시Person의 영속적 속성을 참조할 수 있다.

 Person_ 메타모델 클래스는 Person의 메타 정보를 참조할 수 있는 대안이다. 이 대안은 매우 많이 사용되고 있는 Java Reflection API와 비슷하지만 개념 상 큰 차이가 있다. 리플렉션을 사용하여 java.lang.Class의 인스턴스에 대한 메타 정보를 가져올 수는 있지만Person.class에 대한 메타 정보는 컴파일러에서 확인할 수 있는 방식으로 참조할 수 없다. 예를 들어, 다음과 같이 리플렉션을 사용하여 Person.class age 필드를 참조할 수 있다.

Field field = Person.class.getField("age");

하지만 이 방법에는 Listing 1에서 살펴본 문자열 기반 JPQL 쿼리의 경우와 비슷한 제한 사항이 따른다. 즉, 이 코드는 컴파일러에서 정상적인 코드로 인식되어 컴파일되지만 정상적으로 작동할지 여부는 확인할 수 없기 때문에 간단한 오타만 포함되어 있어도 런타임에 오류가 발생한다. 결국 리플렉션이 JPA 2.0의 형식이 안전한 쿼리 API가 원하는 대로 작동하지 않는다.

형식이 안전한 쿼리 API에서는 컴파일러가 컴파일 시 확인할 수 있는 방식으로 Person 클래스의 영속적 속성인 age를 참조할 수 있어야 한다. 이에 대한 해결책으로 JPA 2.0에서는 동일한 영속적 속성을 정적으로 노출하여 Person에 해당하는 메타모델 클래스Person_을 인스턴스화할 수 있는 기능을 제공한다.

메타 또는 메타-메타 정보에 대한 설명은 지루할 수 있으므로 이 기사에서는 익숙한 POJO(Plain Old Java Object) 엔터티 클래스에 대한 메타모델 클래스인 domain.Person을 예로 들어 설명한다(Listing 3 참조).


Listing 3. 간단한 영속적 엔터티
package domain;
@Entity
public class Person {
  @Id
  private long ssn;
  private string name;
  private int age;

  // public gettter/setter methods
  public String getName() {...}
}

이 코드에서는 JPA 공급자가 클래스의 인스턴스를 영속적 엔터티로 관리할 수 있도록 @Entity 또는 @Id 와 같은 어노테이션을 사용하여 일반적인 방법으로 POJO를 정의한다.

Listing 4에서는 해당하는 domain.Person의 정적 정식 메타모델 클래스를 보여 준다.


Listing 4. 간단한 엔터티에 대한 정식 메타모델
package domain;
import javax.persistence.metamodel.SingularAttribute;

@javax.persistence.metamodel.StaticMetamodel(domain.Person.class)

public class Person_ {
  public static volatile SingularAttribute<Person,Long> ssn;
  public static volatile SingularAttribute<Person,String> name;
  public static volatile SingularAttribute<Person,Integer> age;
}

이 메타모델 클래스는 원래 domain.Person 엔터티의 각 영속적 속성을 SingularAttribute<Person,?> 형식의 공용 정적 필드로 선언한다. 이 Person_ 메타모델 클래스를 사용하면 Reflection API를 사용하지 않고 컴파일 시 정적 Person_.age 필드에 대한 직접 참조를 통해 domain.Person의 영속적 속성인 age를 참조할 수 있다. 그런 다음 컴파일러에서는 age 속성의 선언된 형식을 기반으로 형식 검사를 수행할 수 있다. 앞 부분에서 이미 아래와 같은 제한 사항의 예를 살펴보았다. 즉, QueryBuilder.gt(p.get(Person_.age), "xyz")에서 컴파일러 오류가 발생할 수 있는데 이는 컴파일러가 QueryBuilder.gt(..)의 서명과 Person_.age 형식을 통해 Person에서 age가 숫자 필드이며 이를 String과 비교할 수 없다는 것을 판별할 수 있기 때문이다.

기타 주의할 점은 다음과 같다.

  • 메타모델 Person_.age 필드는 javax.persistence.metamodel.SingularAttribute 형식으로 선언되며 SingularAttribute는 JPA Metamodel API에 정의된 인터페이스 중 하나이다. 이에 대해서는 다음 섹션에서 설명한다. SingularAttribute<Person, Integer>의 제네릭 형식 인수는 원래 영속적 속성과 영속적 속성 자체의 형식을 선언하는 클래스를 나타낸다. 

  • 원래 영속적 domain.Person 엔터티에 해당하는 메타모델 클래스로 지정하기 위해 메타모델 클래스에@StaticMetamodel(domain.Person.class) 어노테이션이 지정된다.

Metamodel API

앞에서 필자는 메타모델 클래스를 영속적 엔터티 클래스에 대한 설명이라고 정의했다. Reflection API에서 java.lang.Class의 구성 요소를 설명하기 위해 java.lang.reflect.Field, java.lang.reflect.Method 등의 다른 인터페이스가 필요한 것과 마찬가지로 JPA Metamodel API에서도 메타모델 클래스의 형식과 속성을 설명하기 위해 SingularAttribute, PluralAttribute, 등의 다른 인터페이스가 필요하다.

그림 3에서는 형식을 설명하기 위해 Metamodel API에 정의된 인터페이스를 보여 준다.


그림 3. Metamodel API의 영속적 형식에 대한 인터페이스 계층 구조
 

그림 4에서는 속성을 설명하기 위해 Metamodel API에 정의된 인터페이스를 보여 준다.


그림 4. Metamodel API의 영속적 속성에 대한 인터페이스 계층 구조
 

JPA Metamodel API의 인터페이스는 Java Reflection API의 인터페이스보다 세분화되어 있다. 이는 영속성에 대한 다양한 메타 정보를 표현하기 위해 세분화된 인터페이스가 필요하기 때문이다. 예를 들어, Java Reflection API는 모든 Java 형식을 java.lang.Class로 나타낸다. 즉, 클래스, 추상 클래스, 인터페이스 등의 개념을 별도의 정의를 통해 따로 구별하지는 않는다. 물론 Class에 조회하여 인터페이스인지, 추상 클래스인지 여부를 확인할 수는 있지만 서로 다른 두 정의를 통해 인터페이스의 개념과 추상 클래스를 구별하여 표현하는 것과는 다르다.

Java Reflection API는 Java 언어가 처음 발표될 때부터 도입되어 당시에는 일반적인 범용 프로그래밍 언어에 매우 혁신적인 개념으로 평가 받기도 했지만 강형 시스템의 사용과 성능에 대한 인식이 여러 해 동안 발전해왔다. JPA Metamodel API는 이러한 기능을 활용하여 영속적 엔터티에 강형을 지정하는 기능을 제공한다. 예를 들어, 영속적 엔터티는 의미적으로 MappedSuperClass, Entity,Embeddable 등과 구별된다. JPA 2.0 전까지 이 의미적 구분은 영속적 클래스 정의의 해당 클래스 레벨 어노테이션을 통해 표현되었다. JPA Metamodel에서는 의미 차이를 좀 더 세부적으로 구별하기 위해 javax.persistence.metamodel 패키지의 세 가지 인터페이스 즉,MappedSuperclassType, EntityType  EmbeddableType을 설명한다. 이와 유사하게 영속적 속성은 SingularAttribute,CollectionAttribute  MapAttribute와 같은 인터페이스를 통해 형식 정의 레벨에서 구별된다.

이론 상의 장점 외에도 이 특별한 메타모델 인터페이스에는 형식이 안전한 쿼리를 빌드하고 런타임 오류 가능성을 줄이는 데 도움이 되는 실질적인 장점이 있다. 이전 예제에서 몇 가지 장점을 살펴보았으며 앞으로 CriteriaQuery를 사용하는 조인 예제에서도 더 많은 장점을 볼 수 있다.

런타임 범위

대체로 말해, Java Reflection API의 기존 인터페이스와 영속성 메타데이터를 설명하기 위해 특화된 javax.persistence.metamodel의 인터페이스를 비교할 수 있다. 이때 인터페이스 간 유사성을 상세히 알아보려면 메타모델 인터페이스에 대한 런타임 범위에 상응하는 개념이 필요하다. java.lang.Class 인스턴스의 범위는 java.lang.ClassLoader에 의해 런타임에 결정된다. 다른 클래스 인스턴스를 참조하는 Java 클래스 인스턴스 세트는 모두 ClassLoader의 범위 이내로 정의되어야 한다. 세트 경계는 엄격하거나 닫혀 있다. 즉,ClassLoader L의 범위에 속한 클래스 A ClassLoader L의 범위에 속하지 않은 클래스 B를 참조하려고 하면 잘못된ClassNotFoundException 또는 NoClassDef FoundError가 발생한다. (여러 ClassLoader를 사용하는 환경의 개발자나 전개자의 경우 이 문제를 해결하지 못해서 어려움을 겪기도 한다.)

이처럼 상호 참조 가능한 클래스의 엄격한 세트로서의 런타임 범위 개념은 JPA 1.0에서 영속성 단위로 적용되었다. 영속적 엔터티의 관점에서 영속성 단위의 범위는 META-INF/persistence.xml 파일의 <class> 절에 열거된다. JPA 2.0에서는javax.persistence.metamodel.Metamodel 인터페이스를 통해 이 범위를 런타임에 사용할 수 있다. 그림 5와 같이 Metamodel 인터페이스는 특정 영속성 단위에 알려져 있는 모든 영속적 엔터티를 포함한다.


그림 5. 영속성 단위의 형식을 포함하고 있는 메타모델 인터페이스
 

이 인터페이스를 사용하면 해당 영속성 엔터티 클래스에서 메타모델 요소에 액세스할 수 있다. 예를 들어, 다음과 같은 구문을 사용하여 Person 영속성 엔터티의 영속적 메타데이터에 대한 참조를 가져올 수 있다.

EntityManagerFactory emf = ...;
Metamodel metamodel = emf.getMetamodel();
EntityType<Person> pClass = metamodel.entity(Person.class);

위 코드와 조금 다른 스타일과 관용어를 사용하기는 하지만 아래 코드에서도 ClassLoader를 통해 이름을 기준으로 Class를 가져온다.

ClassLoader classloader =  Thread.currentThread().getContextClassLoader();
Class<?> clazz = classloader.loadClass("domain.Person");

EntityType<Person> Person 엔터티에 선언된 영속적 속성을 가져오기 위해 런타임에 탐색할 수 있다. 애플리케이션에서pClass.getSingularAttribute("age", Integer.class)와 같은 pClass의 메소드를 호출하면 SingularAttribute<Person, Integer>인스턴스가 리턴된다. 이 인스턴스는 인스턴스화된 정식 메타모델 클래스의 정적 Person_.age 멤버와 같다. 기본적으로 애플리케이션에서 Metamodel API를 통해 런타임에 참조할 수 있는 영속적 속성은 Java 컴파일러에서 정적 정식 메타모델 Person_ 클래스를 인스턴스화하여 사용할 수 있다.

Metamodel API에서는 영속적 엔터티를 해당 메타모델 요소로 해석할 수 있을 뿐만 아니라 알려져 있는 모든 메타모델 클래스(Metamodel.getManagedTypes())에 액세스하거나 영속성 관련 정보를 사용하여 메타모델 클래스에 액세스할 수 있다. 예를 들어,embeddable(Address.class) ManagedType<>의 서브인터페이스인 EmbeddableType<Address> 인스턴스를 리턴한다.

JPA에서 POJO에 대한 메타 정보에는 클래스가 포함되는지 여부나 기본 키로 사용되는 필드 등과 같은 영속적인 관련 메타 정보에 대한 속성이 소스 코드 레벨 어노테이션(또는 XML 디스크립터)를 사용하여 추가로 지정된다. 영속적인 메타 정보는 크게 두 가지 범주 즉, 영속성을 위한 범주(예: @Entity)와 맵핑을 위한 범주(예: @Table)로 분류된다. JPA 2.0의 경우 메타모델은 영속성 어노테이션에 대한 메타데이터만 캡처하고 맵핑 어노테이션에 대한 메타데이터는 캡처하지 않는다. 따라서 현재 버전의 Metamodel API에서는 영속적인 필드는 알 수 있지만 필드가 맵핑된 데이터베이스 열은 찾을 수 없다.

정식과 비정식

JPA 2.0 스펙에 정식 정적 메타모델 클래스의 세부 사항(메타모델 클래스의 완전한 이름 및 정적 필드의 이름 포함)이 정확히 규정되어 있기는 하지만 애플리케이션에서 이러한 메타모델 클래스를 작성할 수도 있다. 애플리케이션 개발자가 작성한 메타모델 클래스를 비정식 메타모델이라고 한다. 현재까지 비정식 메타모델의 스펙은 자세하게 규정되어 있는 상태가 아니며 비정식 메타모델에 대한 지원을 JPA 공급자 간에 이식할 수도 없다. 공용 정적 필드는 정식 메타모델에서 선언만 되고 초기화되지는 않는다. 이렇게 선언한 후CriteriaQuery를 개발하는 동안 이러한 필드를 참조할 수 있다. 하지만 이러한 필드에는 런타임에 유용한 값이 할당되어야 한다. 정식 메타모델의 경우 이러한 필드에 값을 할당하는 책임이 JPA 공급자에게 있지만 비정식 메타모델의 경우에는 비슷한 보장이 제공되지 않는다. 비정식 메타모델을 사용하는 애플리케이션에서는 특정 벤더 메커니즘에 의존하거나 메타모델 속성에 대한 필드 값을 런타임에 초기화할 수 있는 고유한 방법을 마련해야 한다.

코드 생성 및 유용성

자동 소스 코드 생성과 관련하여 몇 가지 고려할 사항이 있으며 정식 메타모델의 생성된 소스 코드의 경우에는 추가 고려 사항이 있다. 생성된 클래스는 개발 중에 사용되며CriteriaQuery를 빌드하는 데 사용되는 코드의 다른 부분에서는 컴파일 시 생성된 클래스를 직접 참조하게 된다. 이 경우 유용성과 관련하여 다음과 같은 의문을 가질 수 있다.

  • 소스 코드 파일이 원래 소스와 같은 디렉토리, 다른 디렉토리 또는 출력 디렉토리에 대한 상대 디렉토리에 생성되어야 하는가?
  • 소스 코드 파일이 버전 제어 구성 관리 시스템에 체크인되어야 하는가?
  • 원래 Person 엔터티 정의와 정식 Person_ 메타모델 간의 상관성을 유지해야 하는가? 예를 들어, Person.java를 편집하여 추가 영속적 속성을 추가하거나 리팩토링하여 영속적 속성의 이름을 바꾸면 어떻게 되는가?

이 기사를 집필할 때까지는 이러한 질문에 대한 답이 명확하지 않았다.

어노테이션 처리 및 메타모델 생성

영속적 엔터티를 많이 가지고 있다면 당연히 메타모델 클래스를 직접 작성하지 않으려고 할 것이다. 사람들은 영속성 공급자가 이러한 메타모델 클래스를 대신 작성해 줄 것으로 기대하고 있다. 스펙에는 영속성 공급자의 기능 또는 생성 메커니즘이 의무 사항으로 규정되어 있지는 않지만 JPA 공급자들은 영속성 공급자가 Java 6 컴파일러에 통합되어 있는 Annotation Processor 기능을 사용하여 정식 메타모델을 생성한다는 점에 암묵적으로 동의하고 있다. Apache OpenJPA에는 영속적 엔터티에 대한 소스 코드를 컴파일할 때 암묵적으로 또는 스크립트를 호출할 때 명시적으로 이러한 메타모델 클래스를 생성하는 유틸리티가 있다. Java 6 전에는 apt라는 어노테이션 프로세서 도구가 많이 사용되었지만 Java 6에서는 컴파일러와 Annotation Processorr를 결합한 도구가 표준으로 정의되었다.

OpenJPA에서 이러한 메타모델 클래스를 영속성 공급자로 생성하는 과정은 컴파일러의 클래스 경로에 있는 OpenJPA 클래스 라이브러리를 사용하여 POJO 엔터티를 컴파일하는 것처럼 쉽게 수행할 수 있다.

$ javac domain/Person.java

정식 메타모델 Person_ 클래스가 생성되어 Person.java와 동일한 디렉토리에 작성된 후 이 컴파일 작업의 부수적인 결과로 컴파일된다.

형식이 안전한 방식으로 쿼리 작성하기

지금까지 CriteriaQuery의 구성 요소 및 연관된 메타모델 클래스를 작성했다. 이제 Criteria API를 사용하여 쿼리를 개발하는 방법을 살펴보자.

함수 표현식

함수 표현식은 하나의 함수를 하나 이상의 입력 인수에 적용하여 새 표현식을 작성한다. 함수 표현식의 형식은 함수의 특성과 인수의 형식에 따라 달라진다. 입력 인수 자체는 표현식 또는 리터럴 값일 수 있다. 컴파일러의 형식 검사 규칙은 API의 서명과 결합되어 유효한 입력 항목을 제어한다.

입력 표현식에 대해 평균을 적용하는 단일 요소 표현식을 살펴보자. Listing 5에서는 모든 Account의 평균 잔고를 선택하는CriteriaQuery를 보여 준다.


Listing 5. CriteriaQuery의 함수 표현식
CriteriaQuery<Double> c = cb.createQuery(Double.class);
Root<Account> a = c.from(Account.class);

c.select(cb.avg(a.get(Account_.balance)));

이와 동등한 JPQL 쿼리는 다음과 같다.

String jpql = "select avg(a.balance) from Account a";

Listing 5에서 QueryBuilder 팩토리(cb 변수로 표현됨)는 avg() 표현식을 작성한 후 쿼리의 select() 절에서 사용한다.

쿼리 표현식은 쿼리의 최종 선택 조건부를 정의하기 위해 어셈블링할 수 있는 빌딩 블록이다. Listing 6 예제에서는 Account의 잔고로 이동하여 작성되는 Path 표현식을 볼 수 있다. 그런 다음 이 Path 표현식은 한 쌍의 바이너리 함수 표현식(greaterThan()lessThan())의 입력 표현식으로 사용되며 두 함수 표현식의 결과는 모두 부울 표현식이거나 조건부이다. 그런 다음 이들 조건부는and() 연산을 통해 결합되어 쿼리의 where() 절에서 평가될 최종 선택 조건부가 된다.

Fluent API

이 예제에서 보듯이 Criteria API는 관련된 메소드에서 직접 사용할 수 있는 형식을 리턴하기도 한다. 이러한 방식의 프로그래밍 스타일을 Fluent API라고 한다.


Listing 6. CriteriaQuery where() 조건부
CriteriaQuery<Account> c = cb.createQuery(Account.class);
Root<Account> account = c.from(Account.class);
Path<Integer> balance = account.get(Account_.balance);
c.where(cb.and
       (cb.greaterThan(balance, 100), 
        cb.lessThan(balance), 200)));

이와 동등한 JPQL 쿼리는 다음과 같다.

"select a from Account a where a.balance>100 and a.balance<200";

복합 조건부

in()과 같은 특정 표현식은 가변 개수의 표현식에 적용된다. Listing 7에서 그 예를 볼 수 있다.


Listing 7. CriteriaQuery의 다중 값 표현식
CriteriaQuery<Account> c = cb.createQuery(Account.class);
Root<Account> account = c.from(Account.class);
Path<Person> owner = account.get(Account_.owner);
Path<String> name = owner.get(Person_.name);
c.where(cb.in(name).value("X").value("Y").value("Z"));

이 예제에서는 Account부터 시작한 후 두 단계를 거쳐서 계정 소유자의 이름을 나타내는 경로 표현식을 작성한다. 그런 다음 경로 표현식을 입력으로 사용하는 in() 표현식을 작성한다. in() 표현식은 입력 표현식이 가변 개수의 인수와 일치하는지 평가한다. 이러한 인수는 In<T> 표현식의 value() 메소드를 통해 지정되며 이 표현식의 메소드 서명은 다음과 같다.

In<T> value(T value); 

Java 제네릭을 사용하여 In<T> 표현식이 T 형식의 값만 가지고 있는 멤버에 대해 평가될 수 있도록 지정하는 방법을 살펴보자. Account소유자의 이름을 나타내는 경로 표현식은 String 형식이므로 String 값 인수에 대한 비교만 유효하다. String 값 인수는 리터럴이거나String으로 평가되는 다른 표현식일 수 있다.

Listing 7의 코드와 동등한 JPQL을 대조해 보자.

"select a from Account a where a.owner.name in ('X','Y','Z')";

JPQL의 사소한 오류는 컴파일러에서 감지되지 않을 뿐만 아니라 원하지 않는 결과도 생성된다. 예를 들면, 다음과 같다.

"select a from Account a where a.owner.name in (X, Y, Z)";

관계 조인하기

Listing 6  Listing 7의 예제에서 표현식을 빌딩 블록으로 사용하지만 쿼리는 단일 엔터티 및 해당 속성을 기반으로 한다. 하지만 쿼리에 둘 이상의 엔터티가 사용될 수 있으며, 이 경우 둘 이상의 엔터티를 조인해야 한다. CriteriaQuery 형식이 지정된 조인 표현식을 사용하여 두 엔터티의 결합을 표현한다. 형식이 지정된 조인 표현식에는 두 가지 형식 매개변수가 있다. 하나는 조인할 원본의 형식이고 다른 하나는 조인될 대상 속성의 바인딩 가능한 형식이다. 예를 들어, 하나 이상의 PurchaseOrder가 아직 전달되지 않은 Customer를 쿼리하려면 Customer PurchaseOrder에 조인하는 표현식을 사용해야 한다. 여기서, Customer에는 java.util.Set<PurchaseOrder>형식의 orders라는 영속적 속성이 있다(Listing 8 참조).


Listing 8. 다중 값 속성 조인하기
CriteriaQuery<Customer> q = cb.createQuery(Customer.class);
Root<Customer> c = q.from(Customer.class);
SetJoin<Customer, PurchaseOrder> o = c.join(Customer_.orders);

루트 표현식 c와 영속적 Customer.orders 속성을 사용하여 작성된 조인 표현식은 선언된 형식인 java.util.Set<PurchaseOrder> 아니라 조인의 소스인 Customer Customer.orders 속성의 바인딩 가능한 형식인 PurchaseOrder에 의해 매개변수화된다. 또한 원래 속성이 java.util.Set 형식이었으므로 결과 조인 표현식은 SetJoin이 되며 이는 선언된 형식인 java.util.Set의 속성을 위해 특화된Join이다. 이와 마찬가지로 지원되는 다른 다중 값의 영속적 속성 유형에 대해 이 API는 CollectionJoin, ListJoin  MapJoin을 정의한다. (그림 1에서 다양한 조인 표현식을 볼 수 있다.) Listing 8의 3행에서는 명시적 변환이 필요하지 않다. 왜냐하면 CriteriaQuery와 Metamodel API가 join()의 오버로드된 메소드를 통해 java.util.Collection, List, Set 또는 Map으로 선언된 속성 형식을 인식하고 구별하기 때문이다.

조인은 쿼리에서 조인된 엔터티에 대한 조건부를 생성하는 데 사용된다. 따라서 하나 이상의 전달되지 않은 PurchaseOrder가 있는Customer를 선택하려는 경우에는 다음과 같이 상태 속성을 통해 조인된 표현식 o로 이동하여 DELIVERED 상태와 비교한 후 조건부를 부정하여 조건부를 정의할 수 있다.

Predicate p = cb.equal(o.get(PurchaseOrder_.status), Status.DELIVERED)
        .negate();

조인 표현식 작성과 관련하여 한 가지 주의할 점은 표현식에서 조인할 때마다 새 조인 표현식이 리턴된다는 점이다(Listing 9 참조).


Listing 9. 고유 인스턴스를 작성하는 각각의 조인 
SetJoin<Customer, PurchaseOrder> o1 = c.join(Customer_.orders);
SetJoin<Customer, PurchaseOrder> o2 = c.join(Customer_.orders);
assert o1 == o2;

Listing 9에서 동일한 속성을 가지고 있는 동일한 표현식 c에서 생성된 두 조인 표현식의 동질성에 대한 가정은 실패한다. 따라서 값이 200달러 이상이고 전달되지 않은 PurchaseOrder에 대한 조건부가 쿼리에 있는 경우 올바른 구문은 PurchaseOrder를 루트 Customer표현식과 한 번만 조인한 후 결과 조인 표현식을 로컬 변수(JPQL 용어로 범위 변수에 해당)에 할당한 다음 이 로컬 변수를 사용하여 조건부를 작성하는 것이다.

매개변수 사용하기

이 기사의 원래 JPQL 쿼리(올바른 쿼리)를 살펴보자.

String jpql = "select p from Person p where p.age > 20";

상수 리터럴을 사용하여 쿼리를 작성하기도 하지만 이는 좋은 방법이 아니다. 되도록이면 쿼리를 매개변수화하는 것이 좋다. 이렇게 하면 쿼리를 한 번만 구문 분석 또는 준비한 후 캐싱해 두고 다시 사용할 수 있기 때문이다. 그리고 다음과 같이 명명된 매개변수를 사용하여 쿼리를 작성하면 더 효과적이다.

String jpql = "select p from Person p where p.age > :age";

매개변수화된 쿼리는 쿼리 실행 전에 매개변수의 값을 바인딩한다.

Query query = em.createQuery(jpql).setParameter("age", 20);
List result = query.getResultList();

JPQL 쿼리에서 매개변수는 명명된 쿼리 문자열(:age와 같이 콜론이 앞에 표시됨)이나 위치 쿼리 문자열(?3과 같이 물음표가 앞에 표시됨)로 인코딩된다. CriteriaQuery에서 매개변수는 그 자체로 쿼리 표현식이다. 다른 표현식과 마찬가지로 매개변수도 강형 표현식이며 표현식 팩토리인 QueryBuilder에 의해 생성된다. 따라서 Listing 10과 같이 Listing 2의 쿼리를 매개변수화할 수 있다.


Listing 10. CriteriaQuery에서 매개변수 사용하기
ParameterExpression<Integer> age = qb.parameter(Integer.class);
Predicate condition = qb.gt(p.get(Person_.age), age);
c.where(condition);
TypedQuery<Person> q = em.createQuery(c); 
List<Person> result = q.setParameter(age, 20).getResultList();

매개변수의 사용법과 JPQL의 사용법을 비교해 보면 매개변수 표현식은 Integer가 되는 명시적 형식 정보를 사용하여 작성되며 값20을 실행 가능한 쿼리에 바인딩하는 데 직접 사용된다. 추가 형식 정보를 사용하면 런타임 오류를 줄이는 데 유용한데 이렇게 하면 매개변수가 호환되지 않는 형식의 표현식과 비교되거나 잘못된 형식의 값에 바인딩되지 않도록 할 수 있기 때문이다. JPQL 쿼리의 매개변수에 대해서는 컴파일 시 안전한 형식이 보장되지 않는다.

Listing 10 예제에서는 바인딩에 직접 사용되는 명명되지 않은 매개변수 표현식을 보여 준다. 매개변수를 생성하는 동안 매개변수에 대한 이름을 두 번째 인수로 할당할 수도 있다. 이렇게 하면 해당 이름을 사용하여 매개변수 값을 쿼리에 바인딩할 수 있다. 하지만 위치 매개변수는 사용할 수 없다. 선형 JPQL 쿼리 문자열의 정수 위치는 어느 정도 직관적으로 파악이 가능하나 정수 위치의 개념은 개념 모델이 쿼리 표현식의 트리에 해당하는 CriteriaQuery의 컨텍스트에서 파악할 수 없다.

JPA 쿼리 매개변수의 또 다른 특징은 내재값이 없다는 것이다. 값은 실행 가능한 쿼리의 컨텍스트에서 매개변수에 바인딩된다. 따라서 동일한 CriteriaQuery 쿼리에서 서로 다른 두 개의 실행 가능한 쿼리를 작성한 후 이들 실행 가능한 쿼리의 동일한 매개변수에 서로 다른 두 정수 값을 바인딩할 수 있다.

결과 예상하기

실행 시 CriteriaQuery가 리턴하는 결과의 형식은 QueryBuilder에서 CriteriaQuery가 생성될 때 지정된다. 쿼리의 결과는 하나 이상의 프로젝션 조건으로 지정된다. CriteriaQuery 인터페이스의 경우 다음과 같은 두 가지 방법으로 프로젝션 조건을 지정할 수 있다.

CriteriaQuery<T> select(Selection<? extends T> selection);
CriteriaQuery<T> multiselect(Selection<?>... selections);

가장 단순하고 일반적으로 사용되는 프로젝션 조건은 쿼리 자체의 후보 클래스이다. 이는 Listing 11과 같이 암묵적일 수 있다.


Listing 11. 기본적으로 후보 범위를 선택하는 CriteriaQuery
CriteriaQuery<Account> q = cb.createQuery(Account.class);
Root<Account> account = q.from(Account.class);
List<Account> accounts = em.createQuery(q).getResultList();

Listing 11에서 Account의 쿼리는 선택 조건을 명시적으로 지정하지 않지만 이는 후보 클래스를 명시적으로 선택하는 것과 같다. Listing 12에서는 명시적 선택 조건을 사용하는 쿼리를 보여 준다.


Listing 12. 명시적인 단일 선택 조건을 사용하는 CriteriaQuery
CriteriaQuery<Account> q = cb.createQuery(Account.class);
Root<Account> account = q.from(Account.class);
q.select(account);
List<Account> accounts = em.createQuery(q).getResultList();

예상한 쿼리 결과가 영속적 엔터티 후보와 다를 경우 여러 다른 구문을 사용하여 쿼리의 결과를 조정할 수 있다. 이러한 구문은QueryBuilder 인터페이스에서 사용할 수 있다(Listing 13 참조).


Listing 13. 쿼리 결과를 조정하는 메소드
<Y> CompoundSelection<Y> construct(Class<Y> result, Selection<?>... terms);
    CompoundSelection<Object[]> array(Selection<?>... terms);
    CompoundSelection<Tuple> tuple(Selection<?>... terms);

Listing 13의 메소드는 선택 가능한 다른 표현식으로 구성된 복합 프로젝션 조건을 작성한다. construct() 메소드는 지정된 클래스 인수의 인스턴스를 작성한 후 입력 선택 조건의 값을 사용하여 생성자를 호출한다. 예를 들어, 비영속적 엔터티인 CustomerDetailsString  int 인수를 지닌 생성자가 있으면 CriteriaQuery가 영속적 엔터티인 선택된 Customer 인스턴스의 이름과 나이를 기반으로 인스턴스를 작성하여 CustomerDetails를 결과로 리턴할 수 있다(Listing 14 참조).


Listing 14. construct()를 사용하여 쿼리 결과를 클래스의 인스턴스로 표현하기
CriteriaQuery<CustomerDetails> q = cb.createQuery(CustomerDetails.class);
Root<Customer> c = q.from(Customer.class);
q.select(cb.construct(CustomerDetails.class,
              c.get(Customer_.name), c.get(Customer_.age));

여러 프로젝션 조건을 Object[] 또는 Tuple을 나타내는 복합 조건으로 결합할 수도 있다. Listing 15에서는 결과를 Object[]로 결합하는 방법을 보여 준다.


Listing 15. 쿼리 결과를 Object[]로 표현하기
CriteriaQuery<Object[]> q = cb.createQuery(Object[].class);
Root<Customer> c = q.from(Customer.class);
q.select(cb.array(c.get(Customer_.name), c.get(Customer_.age));
List<Object[]> result = em.createQuery(q).getResultList();

이 쿼리는 길이가 2인 Object[]에 각 요소가 포함되어 있는 결과 목록을 리턴한다. 이 배열에서 0번째 배열 요소는 Customer의 이름이고 1번째 요소는 Customer의 나이이다.

Tuple은 데이터 행을 나타내는 JPA에 정의된 인터페이스이다. Tuple은 개념적으로 TupleElement의 목록이며 여기서, TupleElement는 원자성 단위이고 모든 쿼리 표현식의 루트이다. Tuple에 포함된 값은 0부터 시작하는 정수 인덱스(익숙한 JDBC 결과와 비슷함)나TupleElement의 별명을 사용하여 액세스하거나 TupleElement를 사용하여 직접 액세스할 수 있다. Listing 16에서는 결과를 Tuple로 결합하는 방법을 보여 준다.


Listing 16. 쿼리 결과를 Tuple로 표현하기
CriteriaQuery<Tuple> q = cb.createTupleQuery();
Root<Customer> c = q.from(Customer.class);
TupleElement<String> tname = c.get(Customer_.name).alias("name");
q.select(cb.tuple(tname, c.get(Customer_.age).alias("age");
List<Tuple> result = em.createQuery(q).getResultList();
String name = result.get(0).get(name);
String age  = result.get(0).get(1);

중첩 제한 사항

이론적으로는 요소 자체가 Object[] 또는 TupleTuple과 같은 조건을 중첩시켜서 복합 결과 형태를 작성할 수 있다. 하지만 JPA 2.0 스펙에서는 이러한 중첩의 사용을 제한하고 있다. multiselect()의 입력 조건은 배열 또는 튜플 값으로 구성된 복합 조건일 수 없다.multiselect() 인수로 허용되는 유일한 복합 조건은construct() 메소드에 의해 작성된 조건(기본적으로 단일 요소를 나타냄)이다.

하지만 OpenJPA에서는 복합 선택 조건을 다른 복합 선택 조건 내에서 중첩시키는 것에 대한 제한이 없다.

이 쿼리는 각 요소가 Tuple인 결과 목록을 리턴한다. 그런 다음 각 튜플은 인덱스나 개별 TupleElement의 별명(있는 경우)을 통해 액세스할 수 있거나 TupleElement를 통해 직접 액세스할 수 있는 두 요소를 전달한다. 이 외에도 Listing 16에서는 두 가지 사항을 더 살펴보아야 한다. 먼저 alias()를 사용하여 이름을 쿼리 표현식에 연결하고 있다. 이 경우 부수적으로 새 복사본이 작성된다. 그리고 createQuery(Tuple.class)QueryBuilder의 대안으로 사용되는 createTupleQuery() 메소드가 있다.

이러한 개별 결과 형태의 메소드 동작과 생성 중에 CriteriaQuery의 결과 형식 인수로 지정되는 항목은 multiselect() 메소드의 시맨틱에 결합된다. 이 메소드는 결과의 형태에 도달한 CriteriaQuery의 결과 형식을 기반으로 입력 조건을 해석한다. Listing 14와 같이 multiselect()를 사용하여 CustomerDetails 인스턴스를 생성하려면 CriteriaQueryCustomerDetails 형식이 되도록 지정한 다음 CustomerDetails 생성자를 구성할 조건을 사용하여 multiselect()를 호출하면 된다(Listing 17 참조).


Listing 17. 결과 형식을 기반으로 조건을 해석하는 multiselect()
CriteriaQuery<CustomerDetails> q = cb.createQuery(CustomerDetails.class);
Root<Customer> c = q.from(Customer.class);
q.multiselect(c.get(Customer_.name), c.get(Customer_.age));

쿼리 결과 형식이 CustomerDetails이므로 multiselect()는 인수 프로젝션 조건을 CustomerDetails에 대한 생성자 인수로 해석한다.Tuple을 리턴하도록 쿼리가 지정된 경우 완전히 동일한 인수를 사용하여 multiselect() 메소드를 호출하면 Tuple 인스턴스가 작성된다(Listing 18 참조).


Listing 18. multiselect()를 사용하여 Tuple 인스턴스 작성하기 
CriteriaQuery<Tuple> q = cb.createTupleQuery();
Root<Customer> c = q.from(Customer.class);
q.multiselect(c.get(Customer_.name), c.get(Customer_.age));

multiselect()의 동작은 Object가 결과 형식이거나 형식 인수를 지정하지 않은 경우 흥미로운 결과를 보여 준다. 그러한 경우multiselect()를 단일 입력 조건과 함께 사용하면 선택된 조건 자체가 리턴된다. 하지만 multiselect()에 둘 이상의 입력 조건이 있을 경우에는 Object[]가 리턴된다.

고급 기능

지금까지 Criteria API의 강형 특성과 이 API가 문자열 기반 JPQL 쿼리에서 발생할 수 있는 구문 오류를 최소화하는 데 도움이 된다는 사실을 살펴보았다. Criteria API는 쿼리를 프로그래밍 방식으로 작성하는 메커니즘이며 동적 쿼리 API라고도 한다. 프로그래밍 가능한 쿼리 생성 API는 사용자의 창의적인 아이디어만 있다면 얼마든지 활용할 수 있다. 이 기사에서는 다음 네 가지 예제를 소개한다.

  • 약형 API 버전을 사용하여 동적 쿼리 작성하기
  • 데이터베이스 지원 함수를 쿼리 표현식으로 사용하여 문법 확장하기
  • 결과 내 검색 기능을 위한 쿼리 편집하기
  • QBE(Query-by-example) — 오브젝트 데이터베이스 커뮤니티에서 많이 사용하는 패턴

약형 지정 및 동적 쿼리 작성

Criteria API의 강형 검사는 개발 시 인스턴스화된 메타모델 클래스의 사용 가능성을 기반으로 한다. 하지만 선택할 엔터티를 런타임에만 결정할 수 있는 경우도 있다. 이러한 방법을 지원하기 위해 Criteria API 메소드는 영속적 속성이 인스턴스화된 정적 메타모델 속성에 대한 참조가 아닌 이름을 통해 참조되는 병렬 버전을 제공한다(Java Reflection API와 유사함). 이 병렬 버전의 API는 컴파일 시 형식 검사를 수행하지 않고 동적 쿼리 생성을 지원할 수 있다. Listing 19에서는 약형 버전을 사용하여 Listing 6의 예제를 다시 작성한 코드를 보여 준다.


Listing 19. 약형 쿼리
Class<Account> cls = Class.forName("domain.Account");
Metamodel model = em.getMetamodel();
EntityType<Account> entity = model.entity(cls); 
CriteriaQuery<Account> c = cb.createQuery(cls);
Root<Account> account = c.from(entity);
Path<Integer> balance = account.<Integer>get("balance");
c.where(cb.and
       (cb.greaterThan(balance, 100), 
        cb.lessThan(balance), 200)));

하지만 약형 API는 제네릭 형식의 올바른 표현식을 리턴할 수 없기 때문에 검사되지 않은 캐스팅에 대한 컴파일러 경고가 생성된다. 이러한 경고 메시지는 Java 제네릭의 기능 중에서 비교적 많이 사용되지 않는 매개변수화된 메소드 호출을 사용하여 제거할 수 있다. 예를 들어, Listing 19에서는 get() 메소드를 호출하여 경로 표현식을 가져온다.

확장 가능한 데이터 저장소 표현식

동적 쿼리 생성 메커니즘의 뚜렷한 장점은 문법이 확장 가능하다는 것이다. 예를 들어, QueryBuilder 인터페이스의 function() 메소드를 사용하여 데이터베이스에서 지원되는 표현식을 작성할 수 있다.

<T> Expression<T> function(String name, Class<T> type, Expression<?>...args);

function() 메소드는 지정된 이름의 표현식과 0개 이상의 입력 표현식을 작성하며 function() 표현식은 지정된 형식으로 평가된다. 따라서 이 기능을 이용하면 애플리케이션에서 데이터베이스 함수를 평가하는 쿼리를 작성할 수 있다. 예를 들어, MySQL 데이터베이스는 CURRENT_USER() 함수를 지원한다. 이 함수는 서버에서 현재 클라이언트를 인증하기 위해 사용했던 사용자 이름 및 호스트 이름 조합을 UTF-8로 인코딩된 MySQL 계정 문자열로 리턴한다. 애플리케이션에서는 Listing 20과 같이 CriteriaQuery에서 인수가 없는CURRENT_USER() 함수를 사용할 수 있다.


Listing 20. CriteriaQuery에서 데이터베이스 관련 함수 사용하기
CriteriaQuery<Tuple> q = cb.createTupleQuery();
Root<Customer> c = q.from(Customer.class);
Expression<String> currentUser = 
    cb.function("CURRENT_USER", String.class, (Expression<?>[])null);
q.multiselect(currentUser, c.get(Customer_.balanceOwed));

JPQL의 정의된 문법에는 지원되는 표현식의 수가 고정되어 있기 때문에 JPQL에서는 동등한 쿼리를 표현할 수 없다. 하지만 동적 API의 경우에는 고정된 표현식 세트로 인한 제한이 엄격하지 않다.

편집 가능한 쿼리

CriteriaQuery는 프로그래밍 방식으로 편집할 수 있다. 선택 조건, WHERE 절의 선택 조건부 및 ORDER BY 절의 정렬 조건과 같은 쿼리의 절은 모두 변경될 수 있다. 이 편집 기능은 제한 사항을 추가하여 후속 단계에서 쿼리 조건부를 보다 세분화하려는 경우와 같이 전형적인 "결과 내 검색" 유형의 기능에서 사용할 수 있다.

Listing 21의 예제에서는 결과를 이름순으로 정렬하는 쿼리를 작성한 후 우편 번호순으로도 정렬하도록 쿼리를 편집한다.


Listing 21. CriteriaQuery 편집하기
CriteriaQuery<Person> c = cb.createQuery(Person.class);
Root<Person> p = c.from(Person.class);
c.orderBy(cb.asc(p.get(Person_.name)));
List<Person> result = em.createQuery(c).getResultList();
// start editing
List<Order> orders = c.getOrderList();
List<Order> newOrders = new ArrayList<Order>(orders);
newOrders.add(cb.desc(p.get(Person_.zipcode)));
c.orderBy(newOrders);
List<Person> result2 = em.createQuery(c).getResultList();

OpenJPA의 메모리 내 평가

OpenJPA의 확장 기능을 사용하면 편집된 쿼리를 메모리 내에서 평가하여 Listing 21의 결과 내 검색 예제의 효율을 향상시킬 수 있다. 이 예제에서는 편집된 쿼리의 결과가 원래 결과의 엄격한 서브세트가 된다. OpenJPA는 후보 컬렉션이 지정된 경우 쿼리를 메모리 내에서 평가할 수 있으므로 Listing 21의 마지막 행만 수정하면 원래 쿼리의 결과를 제공할 수 있다.

List<Person> result2 = 
  em.createQuery(c).setCandidateCollection(result).getResultList();

CriteriaQuery의 setter 메소드( select(), where() 또는 orderBy())는 이전 값을 지우고 새 인수로 바꾼다. getOrderList()와 같은 해당 getter 메소드에서 리턴하는 목록은 활성 목록이 아니다. 즉, 리턴된 목록의 요소를 추가하거나 제거하더라도 CriteriaQuery;가 수정되지 않는다. 게다가 일부 벤더에서는 실수로 수정되는 경우를 막기 위해 변경할 수 없는 목록을 리턴하기도 한다. 따라서 새 표현식을 추가 또는 제거하기 전에 리턴된 목록을 새 목록에 복사하는 것이 좋다.

QBE(Query-by-example)

동적 쿼리 API의 또 다른 장점으로는 비교적 쉽게 QBE(query-by-example)를 지원할 수 있다는 것이다. QBE(Query-by-example, IBM® Research에서 1970년에 개발)는 소프트웨어에 대한 일반 사용자 유용성을 보여 주는 초기 예제라고도 한다. QBE(query-by-example)의 기본 아이디어는 쿼리에 정확한 조건부를 지정하는 대신 템플리트 인스턴스를 제공한다는 것이다. 템플리트 인스턴스가 지정되면 결합된 조건부가 작성된다. 이 경우 각 조건부는 Null도 아니고 기본값도 아닌 템플리트 인스턴스의 속성 값에 대한 비교이다. 이 쿼리를 실행하면 조건부가 평가되면서 템플리트 인스턴스와 일치하는 모든 인스턴스가 검색된다. QBE(Query-by-example)는 JPA 2.0 스펙에 포함될 예정이었지만 포함되지 않았다. OpenJPA는 확장된 OpenJPAQueryBuilder 인터페이스를 통해 이러한 유형의 쿼리를 지원한다(Listing 22 참조).


Listing 22. OpenJPA의 CriteriaQuery 확장을 사용하는 QBE(Query-by-example)
CriteriaQuery<Employee> q = cb.createQuery(Employee.class);

Employee example = new Employee();
example.setSalary(10000);
example.setRating(1);

q.where(cb.qbe(q.from(Employee.class), example);

이 예제에서 보듯이 OpenJPA의 QueryBuilder 인터페이스 확장은 다음과 같은 표현식을 지원한다.

public <T> Predicate qbe(From<?, T> from, T template);

이 표현식은 지정된 템플리트 인스턴스의 속성 값을 기반으로 결합된 조건부를 생성한다. 예를 들어, 이 쿼리는 급여가 10000이고 등급이 1인 모든 Employee를 찾는다. 비교에서 제외할 선택적 속성 목록과 String 값 속성에 대한 비교 스타일을 지정하여 비교를 좀 더 구체적으로 제어할 수 있다. (참고자료에서 OpenJPA의 CriteriaQuery 확장에 대한 Javadoc 링크를 볼 수 있다.)

결론

이 기사에서는 JPA 2.0의 새로운 Criteria API를 Java 언어로 형식이 안전한 동적 쿼리를 개발하기 위한 메커니즘으로 소개했다.CriteriaQuery는 런타임에 강형 쿼리 표현식 트리로 생성되며 이 기사에서는 일련의 코드 예제를 통해 사용법을 살펴보았다.

이 기사에서는 새 Metamodel API의 중요 역할을 설명한 후 인스턴스화된 메타모델 클래스를 통해 컴파일러에서 쿼리의 정확성을 확인하여 구문 오류가 있는 잘못된 JPQL 쿼리에 의해 발생할 수 있는 런타임 오류를 방지하는 방법에 대해 설명했다. 구문 정확성을 적용하는 기능 외에도 쿼리를 프로그래밍 방식으로 생성하는 JPA 2.0의 기능을 활용하면 QBE(query-by-example), 데이터베이스 함수 사용 등과 같은 고급 기능을 수행할 수 있을 뿐만 아니라 사용자의 창의적인 아이디어만 있다면 이 강력한 새 API를 혁신적으로 활용할 수 있다.

감사의 인사

이 기사를 검토하고 유익한 의견을 제안해 준 Rainer Kwesi Schweigkoffer와 이 강력한 API에 대해 자세히 설명해 준 JPA 2.0 Expert Group의 회원에게 감사의 뜻을 전한다. 도움을 준 Fay Wang과 OpenJPA용 Criteria API를 개발하는 동안 아낌 없이 지원해 준 Larry Kestila와 Jeremy Bauer에게도 감사의 뜻을 전한다.


참고자료

교육

  • JSR 317 - Java Persistence 2.0: Java Community Process 사이트에서 JPA 2.0 스펙 문서를 찾을 수 있다. 

  • Apache OpenJPA: Apache의 Java 영속성 프로젝트에 대한 자세한 정보를 볼 수 있다. 

  • OpenJPA Javadoc for CriteriaQuery: org.apache.openjpa.persistence.criteria 패키지에 대한 Javadoc을 볼 수 있다. 

  • LIQUidFORM: Java 형식 검사에 사용할 수 있는 메타 정보를 작성할 수 있는 대안에 대해 알아볼 수 있다. 

  • 기술 서점에서 다양한 기술 주제와 관련된 서적을 살펴보자. 

  • developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자. 

토론

필자소개

Pinaki Poddar

Pinaki Poddar는 오브젝트 영속성을 강조하는 미들웨어 기술 전문가이며 Java Persistence API(JSR 317) 스펙을 담당하는 Expert Group의 일원이자 Apache OpenJPA 프로젝트의 커미터이다. 글로벌 투자 은행의 구성 요소 지향적 통합 미들웨어와 의료 업계를 위한 의료 이미지 처리 플랫폼을 개발하는 프로젝트에 참여했으며 박사 학위 논문을 위해 고유한 신경망 기반 자동 음성 인식 시스템을 개발했다.



출처 - http://www.ibm.com/developerworks/kr/library/j-typesafejpa/



Posted by linuxism
,