[QueryDSL] 2.2 Querying JPA

http://source.mysema.com/static/querydsl/2.1.1/reference/html/ch02s02.html

2.2 JPA 쿼리

QueryDSL은 영속화 하려는 도메인 모델 데이터를 기반으로 타입 안전한 일관된 문법을 제공한다. 이번 가이드에서는 QueryDSL을 JPA/Hibernate와 같이 사용하는 방법을 살펴본다.

2.2.1. 메이븐

다음 의존성 설정을 메이븐 프로젝트에 추가한다. Mysema 메이븐 저장소 URL은 http://source.mysema.com/maven2/releases.

<dependency>
  <groupId>com.mysema.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>   
   
<dependency>
  <groupId>com.mysema.querydsl</groupId>
  <artifactId>querydsl-jpa</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency> 

메이븐 APT 플러그인 설정 추가 한다.

<project>
  <build>
  <plugins>
    …
    <plugin>
      <groupId>com.mysema.maven</groupId>
      <artifactId>maven-apt-plugin</artifactId>
      <version>1.0</version>
      <executions>
        <execution>
          <goals>
            <goal>process</goal>
          </goals>
          <configuration>
            <outputDirectory>target/generated-sources/java</outputDirectory>
            <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
          </configuration>
        </execution>
      </executions>
    </plugin>
    …
  </plugins>
  </build>
</project>

JPAAnnotationProcessor가 javax.persistence.Entity 애노테이셔니이 붙어있는 도메인 타입을 찾아서 쿼리 타입을 생성해 준다.

하이버네이트 애노테이션을 사용하면 APT 프로세서로 com.mysema.query.apt.hibernate.HibernateAnnotationProcessor를 사용해야 한다.

이클립스를 사용하고 있다면 mvn eclipse:eclipse를 실행해서 target/generated-source.java를 소스 폴더로 사용하도록 이클립스 프로젝틀 갱신하라.

이제 JPQL 쿼리 인스턴스와 쿼리 도메인 모델 인스턴스를 만들 수 있다.

2.2.2. 앤트 연동

생략

2.2.3. hbm.xml 파일에서 모델 생성하기

XML 기반 설정으로 하이버네이트를 사용하고 있다면, XML 메타데이터를 사용해서 QueryDSL 모델을 만들 수도 있다.

com.mysema.qeury.jpa.hiberante.HibernateDoaminExporter는 다음과 같은 기능을 제공한다.

HibernateDomainExporter exporter = new HibernateDomainExporter(
  "Q",                     // name prefix
  new File("target/gen3"), // target folder
  configuration);          // instance of org.hibernate.cfg.Configuration

exporter.export();

HibernateDomainExporter는 리플렉션을 사용하기 때문에 도메인 타입을 참조할 수 있는 클래스패스에서 실행해야 한다.

2.2.4. 쿼리 타입 사용하기

QueryDSL로 쿼리를 만들려면 변수와 쿼리 구현체의 인스턴스를 만들어야 한다. 변수부터 시작하겠다.

프로젝트에 다음과 같은 도메인 타입이 있다고 가정해보자.

@Entity
public class Customer {
    private String firstName;
    private String lastName;

    public String getFirstName(){
        return firstName;
    }

    public String getLastName(){
        return lastName;
    }

    public void setFirstName(String fn){
        firstName = fn;
    }

    public void setLastName(String ln)[
        lastName = ln;
    }
}

Querydsl은 Customer와 같은 패키지에 QCustomer라는 이름으로 쿼리 타입을 생성해준다. Querydsl 쿼리에서 Customer 타입을 대신할 정적 타입 변수로 QCustomer를 사용할 수 있다.

QCustomer는 정적 필드로 접근할 수 있는 기본 인스턴스 변수를 제공한다.

QCustomer customer = QCustomer.customer;

이 방법 말고, 다음과 같이 Customer 변수를 만들 수도 있다.

QCustomer customer = new QCustomer("myCustomer");

2.2.5. 쿼리

Querydsl JPA 모듈은 JPA와 하이버네이트 API 둘 다 지원한다.

하이버네이트 API를 사용하려면 다음과 같이 HibernateQuery 인스턴스를 사용하면 된다.

// where session is a Hibernate session
JPQLQuery query = new HibernateQuery (session);

JPA API를 사용하려면 다음과 같이 JPAQuery 인스턴스를 만들면 된다.

// where entityManager is a JPA EntityManager  
JPQLQuery query = new JPAQuery (entityManager);

HibernateQuery와 JPAQuery 둘 다 JPQLQuery 인터페이스 구현체다.

JPA 구현체로 하이버네이트를 사용할 때는 JPAQeury 기본 설정을 사용하면 되고, EclipseLink를 JPA 구현체로 사용할 때는 다음과 같이 JPAQuery 인스턴스를 생성한다.

// where entityManager is a JPA EntityManager  
JPQLQuery query = new JPAQuery (entityManager, EclipseLinkTemplates.DEFAULT);

Bob이라는 이름을 가지고 있는 customer를 가져오는 쿼리는 다음과 같다.

QCustomer customer = QCustomer.customer;
JPQLQuery query = new HibernateQuery (session);
Customer bob = query.from(customer)
  .where(customer.firstName.eq("Bob"))
  .uniqueResult(customer);

from 호출은 쿼리 소스를 정의하며, where 절은 필터를 정의하고 uniqueResult는 프로젝션을 정의하고 Querydsl이 단일 엘리먼트를 반환하도록 알려준다. 참. 쉽죠?

여러 소스를 기반으로 쿼리를 만들 때는 다음과 같이 한다.

query.from(customer, company);

여러 필터를 사용할 때는 다음과 같다.

query.from(customer)
    .where(customer.firstName.eq("Bob"), customer.lastName.eq("Wilson"));  

또는 다음과 같이 사용한다.

query.form(customer)
    .where(customer.firstName.eq("Bob").and(customer.lastName.eq("Wilson")));

네이티브 JPQL 쿼리로는 다음과 같이 작성할 수 있겠다.

from Customer as customer
    where customer.firstName = "Bob" and customer.lastName = "Wilson"

2.2.6. 조인 사용하기

Querydsl은 JPQL로 다음과 같은 조인을 제공한다. inner join, join, left join, full join. 조인을 사용할 때도 타입 안전성을 보장할 수 있으며 다음과 같이 사용한다.

query.from(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    .list(cat);

네이티브 JPQL로는 다음과 같이 표현할 수 있다.

from Cat as cat
    inner join cat.mate as mate
    left outer join cat.kittens as kitten

또 다른 예제를 보자.,

query.from(cat)
    .leftJoin(cat.kittens, kitten)
    .on(kitten.bodyWeight.gt(10.0))
    .list(cat);

JPQL로는 다음과 같다.

from Cat as cat
    left join cat.kittens as kitten
    with kitten.bodyWeight > 10.0

2.2.7. 사용법

JPQLQuery 인터페이스는 다음과 같다.

from: 쿼리 소스를 정의한다.

innerJoin, join, leftJoin, fullJoin, on: 조인할 것을 정의한다. 조인 메서드의 첫번째 인자는 조인할 소스이고 두번째는 타겟(alias)이다.

where: 쿼리 필터를 정의한다. 가변 인자 또는 and 연산자로 이어 붙일 수 있다.

groupBy: 가변인자 형식으로 묶음에 사용할 인자를 나열한다

having: “group by” 의 having filter를 정의한다. Predicate expression의 가변 인자를 사용한다.

orderBy: Order expression 가변 인자를 사용해서 정렬 방식을 정의한다. asc()와 desc()를 숫자, 문자 또는 OrderSpecifier 인스턴스에 접근할 수 있는 어떠한 비교 가능한 표현식에 적용한다.

limit, offset, restrict: 페이징 할 때 사용한다. limit로 최대한 가져올 행 수를 설정하고, offset으로 스킵할 행 수를 정하거나, restrict로 둘 다 한방에 호출할 수 있다.

2.2.8. 정렬

정렬하는 방법은 다음과 같다.

query.from(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);

JPQL로는 다음과 같다.

from Customer as customer
    order by customer.lastName asc, customer.firstName desc

2.2.9. Grouping

grouping 방법은 다음과 같다.

query.from(customer)
    .groupBy(customer.lastName)
    .list(customer.lastName);

JPQL로는 다음과 같다.

select customer.lastName
    from Customer as customer
    group by customer.lastName

2.2.10. Delete 절

Querydsl에서 Delete 절은 단순하게 delete-where-execute 형식이다. 예제는 다음과 같다.

QCustomer customer = QCustomer.customer;
// delete all customers
new HibernateDeleteClause(session, customer).execute();
// delete all customers with a level less than 3
new HibernateDeleteClause(session, customer).where(customer.level.lt(3)).execute();

HibernateDeleteClause의 두번째 매개변수가 지워질 엔티티다. where 호출은 부가적이며 execute를 실행하면 삭제를 수행하고 삭제된 엔티티 갯수를 반환한다.

JPA API로 Delete할 때는 JPADeleteClause를 사용한다.

2.2.11. Update 절

Querydsl에서 Update 절은 update-set/where-execute 형식이다. 예제는 다음과 같다.

QCustomer customer = QCustomer.customer;
// rename customers named Bob to Bobby
new HibernateUpdateClause(session, customer).where(customer.name.eq("Bob"))
    .set(customer.name, "Bobby")
    .execute();

HibernateUpdateClause의 두번째 매개변수가 수정될 엔티티다. set 호출은 SQL Update 스타일로 수정할 프로퍼티를 지정하고, call은 갱신을 실행하고 갱신된 엔티티 갯수를 반환한다.

JPA API로 Update할 때는 JPAUpdateClause를 사용한다.

2.2.12. 서브쿼리

HibernateSubQuery 인스턴스를 만들어서 서브쿼리를 만들 수 있다. from으로 쿼리 매개변수를 정의하고, unique나 list를 사용해서 서브쿼리를 만든다.

query.from(department)
    .where(department.employees.size().eq(
        new HibernateSubQuery().from(d).unique(d.employees.size().max())
     )).list(department);

또 다른 예제를 살펴보자.

query.from(employee)
    .where(employee.weeklyhours.gt(
        new HibernateSubQuery().from(employee.department.employees, e)

        .where(e.manager.eq(employee.manager))
        .unique(e.weeklyhours.avg())
    )).list(employee);

JPA 기반 서브 쿼리를 사용할 때는 JPASubQuery를 사용한다.

2.2.13. 오리지널 쿼리

쿼리를 실행하기 전에 오리지널 쿼리를 조작할 필요가 있다면 다음과 같이 오리지널 쿼리를 노출시킬 수 있다.

HibernateQuery query = new HibernateQuery(session);
org.hibernate.Query hibQuery = query.from(employee).createQuery(employee);
hibQuery.setResultTransformer(someTransformer);
List results = hibQuery.list();

2.2.14. 하이버네이트 쿼리에서 네이티브 쿼리 사용하기

Querydsl은 하이버네이트에서 네이티브 쿼리를 사용할 수 있도록 지원한다.

그러려면 먼저 SQL 스키마에 대한 Querydsl 쿼리 타입을 만들어야 된다. 다음과 같은 메이븐 설정을 사용할 수 있다.

<plugin>
  <groupId>com.mysema.querydsl</groupId>
  <artifactId>querydsl-maven-plugin</artifactId>
  <version>${project.version}</version>
  <executions>
    <execution>
      <goals>
        <goal>export</goal>
      </goals>
    </execution>           
  </executions>
  <configuration>
    <jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>
    <jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl>
    <packageName>com.mycompany.mydomain</packageName>
    <targetFolder>${project.basedir}/target/generated-sources/java</targetFolder>
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.apache.derby</groupId>
      <artifactId>derby</artifactId>
      <version>${derby.version}</version>
    </dependency>
  </dependencies>
</plugin>

쿼리 타입을 성공적으로 만들어지면 쿼리에서 다음과 같이 사용할 수 있다.

컬럼 한개 조회

// serialization templates
SQLTemplates templates = new DerbyTemplates();
// query types (S* for SQL, Q* for domain types)
SAnimal cat = new SAnimal("cat");  
SAnimal mate = new SAnimal("mate");
QCat catEntity = QCat.cat;         
 
HibernateSQLQuery query = new HibernateSQLQuery(session, templates);
List<String> names = query.from(cat).list(cat.name);

컬럼 여러개 조회

query = new HibernateSQLQuery(session, templates); List rows = query.from(cat).list(cat.id, cat.name);

모든 컬럼 조회

List rows = query.from(cat).list(cat.all());

쿼리는 SQL로 하고 엔티티로 받기

query = new HibernateSQLQuery(session, templates);
List<Cat> cats = query.from(cat).orderBy(cat.name.asc()).list(catEntity);

조인을 사용한 조회

query = new HibernateSQLQuery(session, templates);
cats = query.from(cat)
    .innerJoin(mate).on(cat.mateId.eq(mate.id))
    .where(cat.dtype.eq("Cat"), mate.dtype.eq("Cat"))
    .list(catEntity);

쿼리하고 DTO로 받기

query = new HibernateSQLQuery(session, templates);
List<CatDTO> catDTOs = query.from(cat)
    .orderBy(cat.name.asc())
    .list(EConstructor.create(CatDTO.class, cat.id, cat.name));

JPA API를 사용할 때는 HibernateSQLQuery 대신 JPASQLQeury를 사용하면 된다.

[QueryDSL] 2.1.1 Chapter 1. Introduction

http://source.mysema.com/static/querydsl/2.1.1/reference/html/ch01.html

1. 소개

1-1. 배경

문자열 연결해서 쿼리 만들면 코드 읽기가 어렵다. 타입 안전성을 보장하지도 못한다.

도메인 모델을 타입 안정한 방법으로 변경할 수 있다는 것은 소프트웨어 개발에 큰 장점이다.

QueryDSL은 처음에 HQL을 타겟으로 작업했지만, 요즘은 콜렉션, JDO, JDBC, Lucene, Hibernate Search, MongoDB, RDFBean 등을 지원한다.

1-2. 원칙

타입 안전성(Type safety)가 QueryDSL의 핵심 원칙이다. 도메인 타입을 통해 생성된 쿼리 타입과 속성을 사용하고, 메서드 호출 역시 모두 타입 안전한 방식으로 처리한다.

일관성(Consistency) 역시 중요한 원칙이다. 공통의 쿼리 인터페이스를 기반으로 여러 구현체를 제공한다.

모든 쿼리 인스턴스를 계속해서 재사용할 수 있다.

주요 API

http://source.mysema.com/static/querydsl/2.1.1/apidocs/com/mysema/query/Query.html

Query defines the main query interface of the fluent query language.

Note that the from method has been left out, since there are implementation specific variants of it.

http://source.mysema.com/static/querydsl/2.1.1/apidocs/com/mysema/query/Projectable.html

Projectable defines default projection methods for Query implementations. All Querydsl query implementations should implement either this interface or SimpleProjectable.

http://source.mysema.com/static/querydsl/2.1.1/apidocs/com/mysema/query/types/Expression.html

Expression defines a general typed expression in a Query instance. The generic type parameter is a reference to the type the expression is bound to.

[Spring Data JPA] 1.0.0.RC1 Chapter 2

2장. JPA 리파지토리

이 챕터에서는 JPA 리파지토리 구현체를 자세히 설명한다.

2.1. 쿼리 메서드

2.1.1. 쿼리 룩업 전략

JPA 모듈은 쿼리를 직접 정의하는 방법과 메서드 이름으로 정의하는 방법을 제공한다.

선언된 쿼리

메서드 이름을 사용해서 쿼리를 만들수도 있지만 사용하고 싶은 키워드를 지원하지 않는다거나, 메서드 이름이 너무 장황해지길 원치 않을 수도 있다. 그래서 JPA 네임드 쿼리를 사용하거나 쿼리 메서드에 @Query 애노테이션으르 사용할 수 있다.

전략

JPA @NamedQeury 또는 Hades @Query 애노테이션을 찾아내는 전략이다. 선언된 쿼리를 찾지 못하면 쿼리 실행은 실패한다.

CREATE_IF_NOT_FOUND(기본값)

1장에서 설명을 했을텐데…

2.1.2. 쿼리 생성

기본적으로 쿼리 생성은 1.3 “쿼리 메서드”에서 설명한 방식으로 만들어 진다.

예제 2.1. 메서드 이름으로 쿼리 만들기

public interface UserRepository extends Repository<User, Long> {

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

메서드 이름을 읽어서 JPA Criteria API를 사용해서 쿼리를 만들지만 결국 다음과 같은 쿼리가 된다.

select u from User u where u.emailAddress = ?1 and u.lastname = ?2

Spring Data JPA는 1.3.2.2.1 “property expressions”에서 설명한대로 속성을 확인한다. 다음 표에서 JPA가 지원하는 키워드를 살펴보고 키워드가 어떻게 해석되는지 살펴보자.

표 2.1. 메서드 이름으로 지원하는 키워드


Keyword Sample JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Between findByStartDateBetween … where x.startDate between 1? and ?2
LessThan findByAgeLessThan … where x.age < ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> age) … where x.age not in ?1

In이나 NotIn을 사용할 때 매개변수로 Collection 말고 배열이나 가변인자도 사용할 수 있다.

2.1.3. JPA NamedQueries 사용하기

예제에서는 간단하게 <named-query/> 엘리먼트와 @NamedQeury 애노테이션을 사용한다. 여기서 사용할 쿼리는 JPA 쿼리 언어(JQL)로 작성되어야 한다. 물론, <named-native-query /> 또는 @NamedNativeQuery를 사용할 수도 있다. 이 엘리먼트들은 DB에 특화된 네이티브 SQL을 사용할 수 있다.

XML named 쿼리 정의

XML 설정을 사용하려면, 클래스패스에 있는 META-INF 폴더의 orgm.xml에 <named-query/> 엘리먼트를 추가하라.

예제 2.2. XML named 쿼리 설정

[xml]
&lt;named-query name="User.findByLastname"&gt;
  &lt;query&gt;select u from User u where u.lastname = ?1&lt;/query&gt;
&lt;/named-query&gt;
[/xml]

보시다시피 쿼리마다 유일한 이름을 가지게 된다.

애노테이션 설정

애노테이션 설정은 다른 설정 파일을 작성할 필요가 없어서 유지 보수 비용을 낮출 수 있다는 장점이 있다. 대신 쿼리를 수정하면 도메인 클래스를 다시 컴파일 해야 하는 비용이 있다.

예제 2.3. 애노테이션 기반 named 쿼리 설정

[java]
@Entity
@NamedQuery(name = "User.findByEmailAddress",
  query = "select u from User u where u.emailAddress = ?1")
public class User {

}
[/java]

인터페이스 정의하기

네임드 쿼리를 실행하려면 UserRepository에 다음과 같이 정의하면 된다.

예제 2.4. UserRepository에 쿼리 메서드 선언

[java]
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  List&lt;User&gt; findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}
[/java]

도메인 클래스 이름 뒤에 점을 붙인 다음 메서드 이름을 붙여서(User.findByLastname) 네임드 쿼리를 찾는다.

2.1.4. @Query 사용하기

실행하려는 쿼리가 특정 메서드에 묶여있기 때문에 Spring Data JPA의 @Query 애노테이션을 사용해서 도메인 클래스 보다는 메서드에 선언할 수 있다. 이렇게 하면 도메인 클래스가 저장소 관련 정보를 담고 있지 않게 할 수 있으며, 쿼리를 리파지토리 인터페이스에 둘 수 있다.

예제 2.5. @Query 사용해서 쿼리 메서드에 쿼리 선언하기

[java]
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}
[/java]

2.1.5. 네임드 매개변수 사용하기

기본으로 Spring Data JPA는 위치 기반 매개변수 바인딩을 사용한다. 이런 식의 쿼리는 약간 에러 발생 가능성이 높고 위치를 조정하기 어렵ㄴ다. 이 이슈를 해결하려면 @Param 애노테이션을 사용해서 정확한 이름을 사용해서 쿼리에 있는 이름에 바인딩할 수 있다.

에제 2.6. 네임드 매개변수 사용하기

[java]
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname,
                                 @Param("firstname") String firstname);
}
[/java]

메서드 매개변수는 순서가 실제 SQL에서 사용된 순서와 같지 않아도 된다는 점을 눈여겨 보시라.

2.1.6. 수정 쿼리

가져오는 쿼리 말고 수정하는 쿼리도 커스터마이징 할 수 있다. 쿼리 메서드에 @Modifying을 추가하면 된다.

예제 2.7. 수정 쿼리 선언

[java]
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
[/java]

select가 아니라 update 쿼리를 실행한다. EntityManager가 수정 쿼리를 실행한 다음 변경되기 전 데이터를 가지고 있을 수 있기 때문에, 기본적으로 EntityManager.clear()를 실행한다. 이렇게 하면, EntityManager에 남아있던 동기화(flush)되지 않은 데이터를 모두 버리는데, 그렇게 하고 싶지 않다면 @Modifying 애노테이션의 clearAutomatically 속성을 false로 설정하자.

2.2 Specifications

JPA 2는 쿼리를 프로그래밍으로 만들 수 있도록 Criteria API를 추가했다.

Spring Data JPA는 에릭 에반스의 DDD에서 specification 개념을 차용했다. JPA Criteria API를 사용해서 Specification을 만들 수 있다. 그렇게 하려면 JpaSpecificationExecutor 인터페이스를 리파지토리에 추가 확장 시켜야 한다.

[java]
public interface CustomerRepository extends CrudRepository&lt;Customer, Long&gt;, JpaSpecificationExecutor {

}
[/java]

여기서 추가된 인터페이스의 메서드로는 다음과 같은 것들이 있다.

[java]
List&lt;T&gt; readAll(Specification&lt;T&gt; spec);
[/java]

Specification 인터페이스는 다음과 같이 생겼다.

[java]
public interface Specification&lt;T&gt; {
  Predicate toPredicate(Root&lt;T&gt; root, CriteriaQuery&lt;?&gt; query,
            CriteriaBuilder builder);
}
[/java]

좋아. 그래서 이걸 어떻게 쓰냐고? Specification은 확장 가능한 Predicate를 손쉽게 만드는데 사용한다. 그리고 그렇게 만든 것들을 조합해서 활용할 수 있다.

예제 2.8. Customer 스팩

[java]
public class CustomerSpecs {

  public static Specification&lt;Customer&gt; isLongTermCustomer() {
    return new Specification&lt;Customer&gt;() {
      Predicate toPredicate(Root&lt;T&gt; root, CriteriaQuery&lt;?&gt; query,
            CriteriaBuilder builder) {

         LocalDate date = new LocalDate().minusYears(2);
         return builder.lessThan(root.get(Customer_.createdAt), date);
      }
    };
  }

  public static Specification&lt;Customer&gt; hasSalesOfMoreThan(MontaryAmount value) {
    return new Specification&lt;Customer&gt;() {
      Predicate toPredicate(Root&lt;T&gt; root, CriteriaQuery&lt;?&gt; query,
            CriteriaBuilder builder) {

         // build query here
      }
    };
  }
}
[/java]

구체적인 구현은 생략했다. 클라이언트는 이렇게 만들어둔 스팩을 다음과 같이 사용할 수 있다.

예제 2.9. 스팩 사용하기

[java]
List&lt;Customer&gt; customers = customerRepository.findAll(isLongTermCustomer());
[/java]

그냥 쿼리 하나 만들지 뭐하러 이렇게 하냐는 생각이 들겠지만, 스팩을 하나만 사용했을 때는 그 진가를 잘 발휘하지 못한다. 스팩의 진정한 힘은 스팩 여러개를 조합해서 새로운 스팩을 만들 때 그 빛을 발한다. 스프링에서 제공하는 스팩 헬퍼 클래스(이게 뭔진 안보이는데.. 흠..)의 도움을 받아서 다음가 같이 만들 수 있다.

예제 2.10. 스팩 조합하기

[java]
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List&lt;Customer&gt; customers = customerRepository.readAll(
  where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));
[/java]

보시다시피, 스팩을 조합하고 체인 형태로 만들 수 있다. 따라서 데이터 접근 계층을 확장하는 것은 새로운 스팩 구현체를 만들거나, 기존의 스팩을 어떻게 조합하느냐의 문제가 된다.

2.3. Transactionality

리파지토리의 CRUD 메서드는 기본적으로 트랜잭셔널하다. 읽어오는 오퍼레이션에는 기본으로 readOnly 플래스가 true로 설정되어 있다. 다른 것들은 모두 @Transactional이 붙어있다. 자세한 내용은 Repository 자바독을 살펴보자. 트랜잭션 설정을 바꾸려면 자신의 리파지토리 인터페이스에서 다음과 같이 @Transactional을 재정의하면 된다.

예제 2.11. 커스텀 트랜잭션 설정

[java]
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Override
  @Transactional(timeout = 10)
  public List&lt;User&gt; findAll();

  // Further query method declarations
}
[/java]

findAll() 메서드 실행 시간을 10초로 하고, readOnly 플래스를 제거했다.

또다르느 트랜잭션 설정은 퍼사드 또는 여러 리파지토리를 사용하는 서비스 구현체에서 트랜잭션을 담당하는 것이다.

예제 2.12. 여러 저장소 호출할 때 퍼사드 사용하기

[java]
@Service
class UserManagementImpl implements UserManagement {

  private final UserRepository userRepository;
  private final RoleRepository roleRepository;

  @Autowired
  public UserManagementImpl(UserRepository userRepository,
    RoleRepository roleRepository) {
    this.userRepository = userRepository;
    this.roleRepository = roleRepository;
  }

  @Transactional
  public void addRoleToAllUsers(String roleName) {

    Role role = roleRepository.findByName(roleName);

    for (User user : userRepository.readAll()) {
      user.addRole(role);
      userRepository.save(user);
    }
}
[/java]

이렇게 하면 저장소에 설정해둔 트랜잭션은 무시되고 서비스에 적용한 트랜잭션으로 동작한다. <tx:annotation=driven/>과 컴포넌스 스캔을 사용하고 있다는 가정하게 작성한 예제다.

2.3.1. 트랜잭셔널 쿼리 메서드

쿼리 메서드를 트랜잭션으로 묶으려면 @Transactional을 리파지토리 인터페이스에 사용하면 된다.

예제 2.13. 쿼리 메서드에 @Transactional 사용하기

[java]
@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  List&lt;User&gt; findByLastname(String lastname);

  @Modifying
  @Transactional
  @Query("delete from User u where u.active = false")
  void deleteInactiveUsers();
}
[/java]

해당 클래스의 모든 메서드의 readOnly 플래그를 true로 설정하고, deleteInactiveUsers 같은 경우는 수정하는 메서드니까 @Modifying을 붙이고, @Transactional을 재정의해서 readOnly를 제거했다.

*주의*

읽기 전용 오퍼레이션에는 반드시 readOnly 플래그를 true로 설정하는게 좋다. 이렇게 한다고 해서  수정 쿼리가 날아가는 것을 확인하는게 아니라, 기반으로 사용하고 있는 JDBC 드라이버에 힌트를 알려줘서 성능 최적화를 할 수 있게 해준다. 거기에 스프링이 기반하고 있는 JPA provider에 몇몇 최적화를 수행한다. 가령, 하이버네이트의 flush 모드를 NEVER로 설정해서 하이버네이트가 dirty checking을 무시하게 한다.

2.4. Auditing

대부분의 애플리케이션은 특정 엔티티의 작성일, 장성자, 갱신일, 갱신자를 기록하는 작업이 필요하다. Spring Data JPA는 이런 오딧 정보를 AOP를 사용해서 투명하게 처리한다. 이 기능을 도메인에 추가하려면 고급 인터페이스를 구현할 필요가 있다.

에제 2.14. Auditable 인터페이스

[java]
public interface Auditable&lt;U, ID extends Serializable&gt;
        extends Persistable&lt;ID&gt; {

    U getCreatedBy();

    void setCreatedBy(U createdBy);

    DateTime getCreatedDate();

    void setCreated(Date creationDate);

    U getLastModifiedBy();

    void setLastModifiedBy(U lastModifiedBy);

    DateTime getLastModifiedDate();

    void setLastModified(Date lastModifiedDate);
}
[/java]

보시다시피 수정하려는 엔티티는 반드시 엔티티어야 한다. 보통 User 엔티티가 필요하기 때문에 U 매개변수 타입을 선택하자.

*주의*

코딩을 줄일 수 있도록 Spring Data JPA에서 AbstractPersisteable과 AbstractAuditable 기반 클래스를 제공한다. 그러니까, 단순히 인터페이스를 구현할지 아니면 기반 클래스를 확장해서 좀 더 복잡한 기능을 즐길지 맘대로해라.

일반적인 오딧 설정

Spring Data JPA는 오딧 정보를 캡춰할 수 있는 트리거로 사용할 엔티티 리스너를 제공한다. 우선, orm.xml에 모든 엔티티에 적용할 AuditingEntityListener를 등록해야 한다.

예제 2.15. orm.xml 오딧 설정

[xml]
&lt;persistence-unit-metadata&gt;
  &lt;persistence-unit-defaults&gt;
    &lt;entity-listeners&gt;
      &lt;entity-listener class="….data.jpa.domain.support.AuditingEntityListener" /&gt;
    &lt;/entity-listeners&gt;
  &lt;/persistence-unit-defaults&gt;
&lt;/persistence-unit-metadata&gt;
[/xml]

이제 auditing 엘리먼트만 추가하면 된다.

예제 2.16. 스프링 설정에 오딧 기능 활성화하기

<jpa:auditing auditor-aware-ref=”yourAuditorAwareBean” />

다음과 같이 생긴 AuditorAware 인터페이스의 구현체를 설정해야 한다.

예제 2.17. AuditorAware 인터페이스

[java]
public interface AuditorAware&lt;T, ID extends Serializable&gt; {

    T getCurrentAuditor();
}
[/java]

보통 현재 시스템 사용자를 추적하는데 사용하는 인증 요소가 있을 것이다. 해당 컴포넌트는 보통 AuditAware여야 하며 오디터를 지속적으로 확인해야 한다.

2.5. 기타

2.5.1. 퍼시스턴스 유닛 합치기

모듈화 때문에 애플리케이션을 여럿으로 쪼개놨는데, 런타임 때 그렇게 여럿으로 쪼갠 것을 하나로 묶어서 잘 동작하는 확인하고 싶을 수 있다. 그래서 Spring Data JPA는 자동으로 퍼시스턴스 이름을 기준으로 합쳐주는 PersistenceUnitManager 구현체를 제공한다.

예제 2.18. MergingPersistenceUnitManager 사용하기

[xml]
&lt;bean class="….LocalContainerEntityManagerFactoryBean"&gt;
  &lt;property name="persistenceUnitManager"&gt;
    &lt;bean class="….MergingPersistenceUnitManager" /&gt;
  &lt;/property
&lt;/bean&gt;
[/xml]

[Spring Data JPA] 1.0.0.RC1 Chapter 1

1장. 리파지토리

1.1 소개

데이터 접근 계층 구현이 너무 뻔하고 지겨운 코드 작성이 많다. 그리고 도메인 클래스는 빈약하고 전혀 객체 지향적이거나 도메인 주고적인 방식으로 만들어지지 않는다.

그래서 스프링 데이터에서는 데이터 접근 계층 구현 개발 비용을 줄여주고자 한다.

1.2 핵심 개념

Spring Data 리파지토리 추상화의 핵심 인터페이스는 Repository다. 일종의 마커 인터페이스로 클래스 타입과 ID 타입을 사용해서 Typeable하다. 이 인터페이스의 하위 인터페이스로 CRUD 기능을 제공하는 CrudRepository가 있다.

예제 1.1 Repository 인터페이스

[java]
public interface CrudRepository<T, ID extends Serializable> {

    T save(T entity);                                                                   

    T findOne(ID primaryKey);                                                           

    Iterable<T> findAll();                                                              

    Long count();

    void delete(T entity);                                                              

    boolean exists(ID primaryKey);                                                      

    // … more functionality omitted.                                                    
}
[/java]

인터페이스 설명은 생략. 메서드 이름만 봐도 알겠구만 뭘 굳이;

이걸 기반으로 여러 Spring Data 하위 프로젝트에서 이 구현체를 제공한다.

CrudRepository이외에도 페이징 처리를 쉽게 할 수 있도록 PagingAndSortingRespository를 제공한다.

예제 1.2 PagingAndSortingRepository

[java]
public interface PagingAndSortingRepository<T, ID extends Serializable> extends Repository<T, ID> {

    Iterable<T> findAll(Sort sort);

    Page<T> findAll(Pageable pageable);
}
[/java]

이 인터페이스를 사용해서 두 번째 페이지에서 20개 만큼 가져오려면 다음과 같이 할 수 있다.

[java]
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(new PageRequest(1, 20);
[/java]

1.3 쿼리 메서드

CRUD 기능 이외에 리파지토리를 사용해서 주로 데이터베이스에서 데이터를 쿼리한다. Spring Data에서 그런 쿼리를 선언하는 방법은 4단계 프로세스를 거친다.

1. Respository 또는 Repository 하위 인터페이스를 상속받는 인터페이스 정의하기

[java]
public interface PersonRepository extends Repository<User, Long> { … }
[/java]

2. 쿼리 메서드 정의하기

[java]
List<Person> findByLastname(String lastname);
[/java]

3. Spring에서 해당 인터페이스의 프록시를 만들도록 빈 설정하기

[xml]
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="<a href="http://www.springframework.org/schema/beans&quot;">http://www.springframework.org/schema/beans"</a>
  xmlns:xsi="<a href="http://www.w3.org/2001/XMLSchema-instance&quot;">http://www.w3.org/2001/XMLSchema-instance"</a>
  xmlns="<a href="http://www.springframework.org/schema/data/jpa">http://www.springframework.org/schema/data/jpa</a>
  xsi:schemaLocation="<a href="http://www.springframework.org/schema/beans">http://www.springframework.org/schema/beans</a>
    <a href="http://www.springframework.org/schema/beans/spring-beans.xsd">http://www.springframework.org/schema/beans/spring-beans.xsd</a>
    <a href="http://www.springframework.org/schema/data/jpa">http://www.springframework.org/schema/data/jpa</a>
    <a href="http://www.springframework.org/schema/data/jpa/spring-jpa.xsd&quot;">http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"</a>&gt;

  <repositories base-package="com.acme.repositories" />

</beans>
[/xml]

4. 리파지토리 인스턴스를 주입 받아서 사용하기

[java]
public class SomeClient {

  @Autowired
  private PersonRepoyitory repository;

  public void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
  }
[/java]

각 과정을 상세히 살펴보자.

1.3.1. 리파지토리 인터페이스 정의하기

extends Repository를 하면 되지만, CRUD 기능이 필요하거나 페이징, 소팅이 필요하면 CrudRepository나 PagingAndSortingRepository를 상속받는다.

1.3.1.1. 라피지토리 정의 상세 튜닝

– Sring Data 인터페이스를 상속받고 싶지 않다면, 해당 인터페이스에 @RepositoryDefinition 애노테이션을 붙일 수도 있다.

– CrudRepository의 일부 메서드만 사용하고 싶다면, CrudRepository를 상속받지 말고, CrudRepository에 있는 메서드 중에서 일부만 복사해서 인터페이스에 넣어두면 된다.

예제 1.3 선택적인 CRUD 메서드 노출

[java]
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {
  T findOne(ID id);
  T save(T entity);
}
[/java]

[java]
interface UserRepository extends MyBaseRepository<User, Long> {

  User findByEmailAddress(EmailAddress emailAddress);
}
[/java]

먼저, 모든 도메인 리파지토리에서 사용할 공통 인터페이스를 정의했다. 그래서 UserRepository에서도 findOne과 save 사용할 수 있다.

1.3.2. 쿼리 메서드 정의하기

1.3.2.1. 쿼리 룩업 전략

쿼리를 메서드 이름에서 직접 만드는 방법과 부가적인 쿼리를 사용하는 방법이 있는데, 이건 전적으로 사용하는 스토어에 따라 달라진다. 몇가지 알고리즘이 있다.

세개의 기본 전력이 있다. query-loockup-strategy라는 속성으로 설정할 수 있다.

CREATE

쿼리 메서드 이름으로 특정 쿼리를 만든다. 쿼리 구조에 대해서는 여기를 봐라. 라고 하는데… 링크가 짤려있네;; 헐….

USE_DECLARED_QUERY

이 전략은 먼저 선언된 쿼리를 찾는다. 쿼리는 애노테이션 또는 다른 여러 방법으로 정의할 수 있다. 자세한건 문서를 찾아봐라. (어느 문서..??). 만약에 리파지토리에서 선언된 쿼리를 못 찾으면 애플리케이션 실행 도중 실패한다.

CREATE_IF_NOT_FOUND(기본값)

이 전략은 위에 두 전략을 합쳐둔 것이다. 선언된 쿼리를 못찾으면 메서드 이름으로 쿼리를 만든다.

1.3.2.2. 쿼리 생성

쿼리 생성 전략은 Spring Data 리파지토리 인프라 기반으로 만들었다. 메서드 접두어에 findBy, find, readBy, read, getBy, get을 때어내고 나머지 부분을 엔티티 프로퍼티와 조건(AND, OR)으로 인식한다.

예제 1.4. 메서드 이름으로 쿼리 생성하는 예제

[java]
public interface PersonRepository extends Repository&lt;User, Long&gt; {

  List&lt;Person&gt; findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
}
[/java]

Between, LessThan, GreaterThan, Like 등을 지원하지만, 지원하는 오퍼레이터는 데이터베이스와 데이터 스토어에 따라 달라질 수 있으니 레퍼런스를 참고하라.

1.3.2.2.1. Property expressions

nested 프로퍼티에도 조건을 정의할 수 있다. Persion-Address(ZipCode) 구조에서 다음과 같이 메서드 이름을 정의할 수 있다.

[java]
List&lt;Person&gt; findByAddressZipCode(ZipCode zipCode);
[/java]

이렇게 하면 x.address.zipCode로 순회 한다. 알고리즘은 일단 전체)(AddressZipCode)를 프로퍼티 이름으로 간주하고 도메인 클래스에서 해당 이름의 속성을 찾아본다. 찾지 못하면, 캐멀 케이스를 기준으로 오른쪽부터 잘라서 찾아본다(AddressZip, Code). 앞부분(AddressZip)에 해당하는 속성을 찾으면 그 뒤에는 뒷부분 가지고 nested를 찾아보게 된다. 앞부분에 해당하는 걸 못 찾으면 왼쪽으로 한 칸 이동해서 짜른다.(Address, ZipCode) 그런 다음 다시 찾는다.

대부분의 경우에는 이런 방식으로 잘 동작하지만, 잘못된 프로퍼티를 찾게되는 경우도 있다. Persion이 만약에 addressZip이라는 속성을 가지고 있다면 그렇게 될 것이다. 이런 애매한 상황을 해결하고자 언더바(_)를 사용할 수 있다. 즉 다음과 같이 작성할 수 있다.

[java]
List&lt;Person&gt; findByAddress_ZipCode(ZipCode zipCode);
[/java]

1.3.2.3. 특별 매개변수 핸들링

위 예제에서 사용한 것처럼 메서드 매개변수로 쿼리의 매개변수를 넘겨주면 되는데, 몇몇 특별한 타입의 매개변수를 넘겨서, 동적으로 페이징과 정렬을 처리할 수 있다.

예제 1.5. 쿼리 메서드로 페이징과 정렬 처리

[java]
Page&lt;User&gt; findByLastname(String lastname, Pageable pageable);

List&lt;User&gt; findByLastname(String lastname, Sort sort);

List&lt;User&gt; findByLastname(String lastname, Pageable pageable);
[/java]

Pageable 인스턴스를 넘겨주면 동적으로 페이징을 처리할 수 있다. Pageable 인스턴스에서 정렬도 같이 처리할 수 있는데, 정렬만 하고 싶다면 Sort를 사용하면 된다.

1.3.3. 리파지토리 인스턴스 만들기

가장 쉬운 방법은 Spring Data 모듈에서 제공하는 네임스페이스를 사용해서 스캔하는 것이다.

[xml]
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;beans:beans xmlns:beans="<a href="http://www.springframework.org/schema/beans&quot;">http://www.springframework.org/schema/beans"</a>
  xmlns:xsi="<a href="http://www.w3.org/2001/XMLSchema-instance&quot;">http://www.w3.org/2001/XMLSchema-instance"</a>
  xmlns="<a href="http://www.springframework.org/schema/data/jpa">http://www.springframework.org/schema/data/jpa</a>
  xsi:schemaLocation="<a href="http://www.springframework.org/schema/beans">http://www.springframework.org/schema/beans</a>
    <a href="http://www.springframework.org/schema/beans/spring-beans.xsd">http://www.springframework.org/schema/beans/spring-beans.xsd</a>
    <a href="http://www.springframework.org/schema/data/jpa">http://www.springframework.org/schema/data/jpa</a>
    <a href="http://www.springframework.org/schema/data/jpa/spring-jpa.xsd&quot;">http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"</a>&gt;

  &lt;repositories base-package="com.acme.repositories" /&gt;

&lt;/beans:beans&gt;
[/xml]

이렇게 해두면, com.acme.repositories 패키지 밑 그 하위 패키지에 있는 모든 클래스 중에서 Repository 인터페이스 혹은 그 하위 인터페이스를 상속받은 인터페이스를 찾아서 프록시를 만들어 빈으로 등록해준다. 각 빈의 이름은 인터페이스 이름을 따라서 정해지는데, 예를 들어 UserRepository 인터페이스의 프록시 빈은 userRepository라는 이름으로 등록된다.  base-package 속성에는 와일드카드를 사용해서 패키지 패턴을 설정할 수도 있다.

필터 사용하기

기본으로 Repository나 그 하위 인터페이스를 확장한 모든 인터페이스를 선택하지만 <include-filter />와 <exclude-filter />필터를 사용해서 세부적으로 조절할 수 있다. 동작 방식은 context 네임스페이스와 동일하다.

예를 들어, 특정 인터페이스를 빼고 싶다면 다음과 같이 설정할 수 있다.

예제 1.6. exclude-filter 엘리먼트 사용하기

[xml]
&lt;repositories base-package="com.acme.repositories"&gt;
  &lt;context:exclude-filter type="regex" expression=".*SomeRepository" /&gt;
&lt;/repositories&gt;
[/xml]

SomeRepository로 끝나는 인터페이스는 빈으로 등록되지 않는다.

직접 설정하기

만약 리파지토리 인스턴스를 직접 정의하고 싶다면, <repository /> 엘리먼트를 사용할 수 있다.

[xml]
&lt;repositories base-package="com.acme.repositories"&gt;
  &lt;repository id="userRepository" /&gt;
&lt;/repositories&gt;
[/xml]

1.3.3.2. 스프링 컨테이너 밖에서 사용하기

스프링 컨테이너를 사용하지 않고 리파지토리 인프라를 사용하고 싶다면, 여전히 스프링 라이브러리는 클래스패스에 둔 상태로, RepositoryFactory라는 것을 사용하면 된다.

예제 1.7. 컨테이너 밖에서 리파지토리 팩토리 사용하기

[java]
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
[/java]

1.4. 커스텀 구현체

1.4.1. 단일 리파지토리에 기능 추가하기

부가 기능을 추가하려면, 별도의 인터페이스를 정의하고 그 구현체를 만든다. 그런 다음 리파지토리 인터페이스에 별도로 만든 인터페이스를 추가로 확장하도록 선언한다.

예제 1.8. 커스텀 리파지토리 기능 인터페이스

[java]
interface UserRepositoryCustom {

  public void someCustomMethod(User user);
}
[/java]

예제 1.9. 커스텀 리파지토리 기능 구현체

[java]
class UserRepositoryImpl implements UserRepositoryCustom {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}
[/java]

부가기능 구현체 자체는 Spring Data에 의존하지 않으며 일반적인 스프링 빈이 될 수 있다. 따라서 얼마든지 의존성 주입이나 AOP를 활용할 수 있다.

예제 1.10. 리파지토리 인터페이스에 기능 추가

[java]
public interface UserRepository extends CrudRepository&lt;User, Long&gt;, UserRepositoryCustom {

  // Declare query methods here
}
[/java]

이렇게 기존 리파지토리에 커스텀 리파지토리 인터페이스를 추가 확장하도록 해준다. 이렇게 해두면 CRUD 기능 뿐 아니라 부가 기능도 사용할 수 있게 된다.

설정

커스텀 라파지토리 인터페이스의 구현체는 repository 엘리먼트의 repository-impl-postfix 속성의 값으로 판단하는데, 기본값은 Impl이다.

예제 1.11 설정 예제

[xml]
&lt;repositories base-package="com.acme.repository"&gt;
  &lt;repository id="userRepository" /&gt;
&lt;/repositories&gt;

&lt;repositories base-package="com.acme.repository" repository-impl-postfix="FooBar"&gt;
  &lt;repository id="userRepository" /&gt;
&lt;/repositories&gt;
[/xml]

첫번째 설정은 UserRepositoryImpl을 리파지토리 구현 클래스로 찾게되고, 두번째 설정은 UserRepositoryFooBar를 찾게된다.

직접 주입하기

생략. 애노테이션 쓰면 잘 등록되고, 잘 주입된다.

1.4.2. 모든 리파지토리에 커스텀 기능 추가

먼저, 공통 기능을 가지고 있는 인터페이스를 선언한다.

예제 1.14 커스텀 공유 기능 가지고 잇는 인터페이스

[java]
public interface MyRepository&lt;T, ID extends Serializable&gt;
  extends JpaRepository&lt;T, ID&gt; {

  void sharedCustomMethod(ID id);
}
[/java]

이제 나머지 모든 리파지토리에서는 이 인터페이스를 확장하도록 하고, 이 인터페이스의 구현체를 만든다.

*주의*

이때 당연히 스프링이 MyRepository의 인스턴스를 만들게 하고 싶지 않을텐데 그럴 땐 @NoRepositoryBean을 붙여주면 된다.

예제 1.15. 커스텀 리파지토리 공통 클래스

[java]
public class MyRepositoryImpl&lt;T, ID extends Serializable&gt;
  extends SimpleJpaRepository&lt;T, ID&gt; implements MyRepository&lt;T, ID&gt; {

  public void sharedCustomMethod(ID id) {
    // implementation goes here
  }
}
[/java]

이 구현체를 스프링 데이터 리파지토리의 공통 클래스로 쓰게 하려면 이 구현체를 반환하는 RepositoryFactoryBean을 만들어야 한다.

예제 1.16. 커스텀 리파지토리 팩토리 빈

[java]
public class MyRepositoryFactoryBean&lt;T extends JpaRepository&lt;?, ?&gt;
  extends JpaRepositoryFactoryBean&lt;T&gt; {

  protected RepositoryFactorySupport getRepositoryFactory(…) {
    return new MyRepositoryFactory(…);
  }

  private static class MyRepositoryFactory extends JpaRepositoryFactory{

    public MyRepositoryImpl getTargetRepository(…) {
      return new MyRepositoryImpl(…);
    }

    public Class&lt;? extends RepositorySupport&gt; getRepositoryClass() {
      return MyRepositoryImpl.class;
    }
  }
}
[/java]

마지막으로, 이 커스텀 팩토리를 factory-class 속성에 추가하여, 스프링이 어떤 커스텀 팩토리 구현체를 사용할지 알려준다.

예제 1.17. 커스텀 팩토리 사용하기

[xml]
&lt;repositories base-package="com.acme.repository"
  factory-class="com.acme.MyRepositoryFactoryBean" /&gt;
[/xml]