Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 14장 객체 관계 매핑 (ORM) 데이터 접근 #2

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.




14.4 JDO
스프링은 데이터 접근 전략으로 하이버네이트 지원과 같은 방식으로 표준 JDO 2.0과 2.0 API를 지원한다. 이 통합 클래스들은 org.springframework.orm.jdo 패키지에 있다.


14.4.1 PersistenceManagerFactory 설정
스프링은 스프링 어플리케이션 컨텍스트내에서 로컬 JDO PersistenceManagerFactory를 정의할 수 있도록 LocalPersistenceManagerFactoryBean 클래스를 제공한다.

<beans>
  <bean id="myPmf" class="org.springframework.orm.jdo.LocalPersistenceManagerFactoryBean">
    <property name="configLocation" value="classpath:kodo.properties"/>
  </bean>
</beans>

아 니면 PersistenceManagerFactory 구현클래스를 직접 인스턴스화해서 PersistenceManagerFactory를 설정할 수 있다. JDO PersistenceManagerFactory 구현 클래스는 JDBC DataSource 구현클래스처럼 스프링을 사용하는 설정에 자연스럽게 어울리는 JavaBeans 패턴을 따른다. 이 설정방식은 일반적으로 스프링이 정의하고 connectionFactory 프로퍼티로 전달되는 JDBC DataSource를 지원한다. 예를 들어 다음은 오픈소스 JDO 구현체인 DataNucleus(이전의 JPOX)(http://www.datanucleus.org/)에 대한 PersistenceManagerFactory 구현체의 XML 설정이다.

<beans>

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
  </bean>

  <bean id="myPmf" class="org.datanucleus.jdo.JDOPersistenceManagerFactory" destroy-method="close">
    <property name="connectionFactory" ref="dataSource"/>
    <property name="nontransactionalRead" value="true"/>
  </bean>
</beans>

Java EE 어플리케이션 서버의 JNDI 환경에서 JDO PersistenceManagerFactory를 설정할 수도 있다. 이는 일반적으로 특정 JDO 구현체가 제공하는 JCA 커넥터로 이루어진다. PersistenceManagerFactory 등을 획득하고 노출하는데 스프링의 표준 JndiObjectFactoryBean / <jee:jndi-lookup>를 사용할 수 있다. 하지만 EJB 컨텍스트 외부에서는 JNDI에 PersistenceManagerFactory를 가지고 있어서 생기는 실제적인 장점은 전혀 없다. 합리적인 이유가 있을 때만 이러한 설정을 사용해라. 이 논쟁에 대한 내용은 Section 14.3.6, “컨테이너가 관리하는 리소스와 로컬에 정의한 리소스 비교”를 참고해라. 여기서 나오는 논의는 JDO에도 적용된다.


14.4.2 평범한 JDO API에 기반한 DAO 구현하기
DAO도 스프링에 대한 어떤 의존성을 갖지 않고 주입한 PersistenceManagerFactory로 평범한 JDO API를 직접 사용해서 작성할 수 있다. 다음은 이에 대한 DAO 구현체의 예제이다.

public class ProductDaoImpl implements ProductDao {

  private PersistenceManagerFactory persistenceManagerFactory;

  public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) {
    this.persistenceManagerFactory = pmf;
  }

  public Collection loadProductsByCategory(String category) {
    PersistenceManager pm = this.persistenceManagerFactory.getPersistenceManager();
    try {
      Query query = pm.newQuery(Product.class, "category = pCategory");
      query.declareParameters("String pCategory"); 
      return query.execute(category);
    }
    finally {
      pm.close();
    }
  }
}

위의 DAO가 의존성 주입패턴을 따르기 때문에 스프링의 JdoTemplate로 코드를 작성한 것처럼 스프링 컨테이너와 궁합이 잘 맞는다.

<beans>
  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="persistenceManagerFactory" ref="myPmf"/>
  </bean>
</beans>

팩 토리에서 항상 새로운 PersistenceManager를 얻는다는 것이 이러한 DAO와 관련된 주요 문제점이다. 스프링이 관리하는 트랜잭션이 적용된 PersistenceManager에 접근하려면 대상 PersistenceManagerFactory 앞에 TransactionAwarePersistenceManagerFactoryProxy를 정의하고 이 프록시에 대한 참조를 다음 예제처럼 DAO에 전달해라.

<beans>
  <bean id="myPmfProxy"
      class="org.springframework.orm.jdo.TransactionAwarePersistenceManagerFactoryProxy">
    <property name="targetPersistenceManagerFactory" ref="myPmf"/>
  </bean>

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="persistenceManagerFactory" ref="myPmfProxy"/>
  </bean>
</beans>

데 이터 접근 코드는 호출한 PersistenceManagerFactory.getPersistenceManager() 메서드에서 (존재한다면) 트랜잭션이 적용된 PersistenceManager를 얻을 것이다. 이 메서드 호출이 프록시를 통해서 이뤄져서 팩토리에서 새로운 PersistenceManager 얻기 전에 현재 사용중인 트랜잭션이 적용된 PersistenceManager를 먼저 확인한다. 트랜잭션이 적용된 PersistenceManager에서는 PersistenceManager의 모든 close() 호출을 무시한다.

데이터 접근코드가 항상 활성화된 트랜잭션내에서(또는 최소한 활성화된 트랜잭션 동기화내에서) PersistenceManager.close() 호출을 생략해도 안전하므로 DAO 구현체에서 가지고 있어야 하는 전체 finally 블럭이 간결해진다.

public class ProductDaoImpl implements ProductDao {

  private PersistenceManagerFactory persistenceManagerFactory;

  public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) {
    this.persistenceManagerFactory = pmf;
  }

  public Collection loadProductsByCategory(String category) {
    PersistenceManager pm = this.persistenceManagerFactory.getPersistenceManager();
    Query query = pm.newQuery(Product.class, "category = pCategory");
    query.declareParameters("String pCategory"); 
    return query.execute(category);
  }
}

활성화된 트랜잭션에 기반한 DAO에서는 활성화된 트랜잭션이 TransactionAwarePersistenceManagerFactoryProxy의 allowCreate 플래그를 끄도록 하는 것을 권장한다.

<beans>
  <bean id="myPmfProxy" class="org.springframework.orm.jdo.TransactionAwarePersistenceManagerFactoryProxy">
    <property name="targetPersistenceManagerFactory" ref="myPmf"/>
    <property name="allowCreate" value="false"/>
  </bean>

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="persistenceManagerFactory" ref="myPmfProxy"/>
  </bean>
</beans>

이 DAO 방식의 가장 큰 장점은 JDO API에만 의존한다는 것이다. 어떤 스프링 클래스도 임포트할 필요가 없다. 물론 이는 비침투적인 방법이고 JDO 개발자들도 자연스럽게 느낄 것이다.

하 지만 DAO는 평범한 JDOException(언체크드 예외이므로 선언하거나 잡지 않아야 한다.)를 던지므로 JDO 자체의 예외 구조에 의존하기를 원하지 않는한 호출자들은 예외를 치명적인 오류(fatal)로만 다뤄야한다. 호출자가 구현전략에 묶이지 않고는 낙관적인 작금(locking) 실패같은 특정 원인을 잡는 것은 불가능하다. 이 트래이드오프는 어플리케이션이 상당히 JDO 기반이거나 특별한 예외처리가 필요없다면 수긍할만할 것이다.

요약하자면 평범한 JDO API에 기반해서 DAO를 작성할 수 있고 이 DAO는 여전히 스프링이 관리하는 트랜잭션에 참여할 수 있다. 이미 JDO에 익숙하다면 이 전략이 맘에 들 것이다. 하지만 평범한 JDOException를 던지는 DAO에서는 스프링의 DataAccessException로 명시적인 변환을 해야한다.(원한다면)


14.4.3 트랜잭션 관리
Note
아직 읽어보지 않았다면 Section 11.5, “선언적인 트랜잭션 관리”를 읽어보기를 강력히 권장한다. 스프링의 선언적인 트랜잭션 지원의 자세한 내용을 알 수 있을 것이다.

트랜잭션내에서 서비스 작업을 실행하려고 스프링의 일반적인 선언적 트랜잭션 기능을 사용할 수 있다. 예를 들면 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
<beans
        xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xsi:schemaLocation="
   http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/tx 
   http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
   http://www.springframework.org/schema/aop 
   http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

  <bean id="myTxManager" class="org.springframework.orm.jdo.JdoTransactionManager">
    <property name="persistenceManagerFactory" ref="myPmf"/>
  </bean>

  <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="productDao" ref="myProductDao"/>
  </bean>

  <tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="increasePrice*" propagation="REQUIRED"/>
      <tx:method name="someOtherBusinessMethod" propagation="REQUIRES_NEW"/>
      <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
    </tx:attributes>
  </tx:advice>

  <aop:config>
    <aop:pointcut id="productServiceMethods" expression="execution(* product.ProductService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="productServiceMethods"/>
  </aop:config>
</beans>

JDO 가 퍼시스턴트 객체를 수정하려면 활성화된 트랜잭션이 필요하다. 하이버네이트와는 다르게 JDO에서는 트랜잭션이 아닌 플러시(flush) 개념은 존재하지 않는다. 이 때문에 선택한 JDO 구현체를 특정 환경에 대해 설정해야 한다. 특히 활성화된 JTA 트랜잭션 자체를 탐지하려고 JTA 동기화를 명시적으로 설정해야 한다. 이는 스프링의 JdoTransactionManager가 수행하듯이 로컬 트랜잭션에서는 필수사항이 아니지만 스프링의 JtaTransactionManager가 주도하든지 EJB CMT와 평범한 JTA가 주도하든지 간에 JTA 트랜잭션에 참여하려면 필요하다.

JdoTransactionManager는 같은 JDBC DataSource에 접근하는 JDBC 접근 코드에 JDO 트랜잭션을 노출할 수 있다. 그래서 등록된 JdoDialect가 의존하는 JDBC Connection의 획득을 지원한다. 이는 기본적으로 JDBC에 기반한 JDO 2.0 구현체에 해당하는 경우이다.


14.4.4 JdoDialect
고 급 기능으로 JdoTemplate와 JdoTransactionManager 둘 다 jdoDialect 빈 프로퍼티에 전달할 수 있는 커스텀 JdoDialect를 지원한다. 이 시나리오에서는 DAO가 PersistenceManagerFactory 참조를 받지 않고 대신 전체 JdoTemplate 인스턴스(예를 들어 JdoDaoSupport의 jdoTemplate 프로퍼티에 전달된다.)를 얻는다. JdoDialect 구현체를 사용하는 경우 일반적으로 벤더에 특화된 방법으로 스프링이 지원하는 고급 기능을 사용할 수 있다.

  • 커스텀 격리수준이나 트랜잭션 타임아웃같은 특쟁 트랜잭션의 의미를 적용한다
  • JDBC에 기반한 DAO에 노출하기 위한 트랜잭션이 적용된 JDBC Connection의 획득
  • 스프링이 관리하는 트랜잭션 타임아웃에서 자동으로 계산한 쿼리 타임아웃의 적용
  • 트랜잭션상의 변경사항이 JDBC에 기반한 데이터 접근코드에 보이도록 PersistenceManager의 플러싱 시도(eagerly flushing)
  • 스프링 DataAccessExceptions로 JDOExceptions의 고급 변환
이에 대한 작업과 스프링의 JDO 지원내에서 사용하는 방법에 대한 자세한 내용은 JdoDialect Javadoc를 참고해라.


14.5 JPA
org.springframework.orm.jpa 패키지에 있는 스프링 JPA는 하이버네이트나 JDO를 통합한 것과 유사한 방법으로 추가적인 기능을 제공하는 의존 구현체와 함께 Java Persistence API를 지원한다.


14.5.1 스프링 환경에서 JPA 설정에 대한 세가지 옵션
스프링 JPA 지원은 엔티티 매니저를 얻으려고 어플리케이션이 사용할 JPA EntityManagerFactory를 설정하는 세가지 방법은 제공한다.


14.5.1.1 LocalEntityManagerFactoryBean
Note
독립적인 어플리케이션이나 통합 테스트처럼 간단한 배포 환경에서만 이 방법을 사용한다.

LocalEntityManagerFactoryBean 는 데이터 접근을 할 때 JPA만 사용하는 어플리케이션의 간단한 배포 환경에 적합한 EntityManagerFactory를 생성한다. 팩토리 빈은 JPA PersistenceProvider 자동탐지 메카니즘(JPA의 Java SE 부트스트래핑(bootstrapping)에 따르면)을 사용하고 대부분의 경우에는 퍼시스턴스 유닛 이름만 지정하면 된다.

<beans>
  <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="myPersistenceUnit"/>
  </bean>
</beans>

이 JPA 배포 형식은 가장 간단하고 가장 제한적이다. 이미 존재하는 JDBC DataSource 빈 정의를 참조할 수 없고 기존에 존재하는 전역 트랜잭션을 지원하지 않는다. 게다가 퍼시스턴스 클래스의 위빙(바이트코드 변환)은 프로바이더의 특화되어 있고 때로는 구동할 때 특정 JVM 에이전트를 지정해야 한다. 이 방법은 JPA 스펙의 설계상 독립적인 어플리케이션과 테스트 환경에만 알맞다.


14.5.1.2 JNDI에서 EntityManagerFactory 획득하기
Note
Java EE 5 서버에 배포할 때 이 방법을 사용해라. 서버의 기본 프로바이더가 아닌 다른 프로바이더를 위해 사용하는 서버에 커스텀 JPA 프로바이더를 배포하는 방법은 서버의 문서를 확인해 봐라.

JNDI에서 EntityManagerFactory를 획득하려면(예를 들면 Java EE 5 환경에서) XML 설정만 변경하면 된다.

<beans>
  <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>

이 동작은 표준 자바 EE 5 부트스트래핑을 가정한다. 자바 EE 서버는 자바 EE 배포 디스크립터(예를 들면 web.xml)에서 퍼시스턴스 유닛(실제로는 어플리케이션 jar의 META-INF/persistence.xml 파일들)과 persistence-unit-ref 엔트리를 자동으로 탐지하고 이러한 퍼시스턴스 유닛들의 컨텍스트 위치에 이름을 짓는 환경을 정의한다.

이러한 시나리오에서 퍼시스턴트 클래스의 위빙(바이트코드 변환)을 포함한 전체 퍼시스턴스 유닛 배포는 Java EE 서버에 달려있다. JDBC DataSource는 META-INF/persistence.xml 파일의 JNDI 위치로 정의한다. EntityManager 트랜잭션은 서버의 JTA 하위시스템과 통합된다. 스프링은 획득한 EntityManagerFactory를 그냥 사용하고 EntityManagerFactory를 의존성 주입으로 어플리케이션 객체들에 전달하고 퍼시스턴스 유닛을 위해 트랜잭션을 관리한다. (보통 JtaTransactionManager를 통해서)

같은 어플리케이션에서 어려 퍼시스턴스 유닛을 사용한다면 JNDI로 획득한 퍼시스턴스 유닛들의 빈 이름은 예를 들어 @PersistenceUnit와 @PersistenceContext 어노테이션에서 어플리케이션이 참조하기 위해 사용하는 퍼시스턴스 유닛 이름과 일치해야 한다.


14.5.1.3 LocalContainerEntityManagerFactoryBean
Note
스프링에 기반하는 어플리케이션 환경에서 전체 JPA 기능을 사용하려면 이 방법을 사용해라. 이 방법은 복잡한 퍼시스턴스가 필요한 독립적인 어플리케이션과 통합 테스트뿐만 아니라 톰캣같은 웹 컨테이너에서도 사용할 수 있다.

LocalContainerEntityManagerFactoryBean 으로 EntityManagerFactory 설정을 완전히 제어할 수 있고 세밀한 커스터마이징이 필요한 환경에 알맞다. LocalContainerEntityManagerFactoryBean는 persistence.xml 파일, 제공된 dataSourceLookup 전략, 지정한 loadTimeWeaver에 기반한 PersistenceUnitInfo 인스턴스를 생성한다. 그러므로 JNDI 외부에서 커스텀 데이터소스로 작업하는 것이 가능하고 위빙 접근을 제어할 수 있다. 다음 예제는 LocalContainerEntityManagerFactoryBean의 대표적인 빈 정의를 보여준다.

<beans>
  <bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="someDataSource"/>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
    </property>
  </bean>
</beans>

다음 예제는 대표적인 persistence.xml 파일이다.

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
  <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
    <mapping-file>META-INF/orm.xml</mapping-file>
    <exclude-unlisted-classes/>
  </persistence-unit>
</persistence>

Note
exclude-unlisted-classes 요소는 <exclude-unlisted-classes/> 숏컷을 제공하려고 어노테이션이 붙은 엔티티 클래스에 대한 스캐닝을 하지 않는다는 것을 항상 나타낸다. JPA 명세서에서 이 숏컷을 제안하고 있지만 안타깝게도 이 숏컷의 false를 의미하는 JPA XSD와 충돌한다. 따라서 <exclude-unlisted-classes> false </exclude-unlisted-classes/>는 지원하지 않는다. 엔테티 클래스에 대한 스캐닝이 이뤄지길 원한다면 exclude-unlisted-classes 요소를 그냥 생략해라.

LocalContainerEntityManagerFactoryBean 의 사용하는 것이 어플리케이션내에서 유연한 로컬 설정을 가능하게 하는 가장 강력한 JPA 설정 방법이다. 존재하는 JDBC JDBC DataSource로의 연결을 지원하고 로컬 트랜잭션과 전역 트랜잭션 등을 지원한다. 하지만 런타임 환경에서 퍼시스턴스 프로바이더가 바이트코드 변환을 필요로한다면 위빙이 가능한 클래스 로더를 사용할 수 있어야 하는 조건같은 요구사항도 강제한다.

이 방법은 Java EE 5 서버에 내장된 JPA 기능과 충돌할 것이다. 완전한 Java EE 5 환경에서는 JNDI에서 EntityManagerFactory를 얻는 것을 고려해 봐라. 아니면 예를 들어 META-INF/my-persistence.xml같은 LocalContainerEntityManagerFactoryBean에 커스텀 persistenceXmlLocation를 지정하고 어플리케이션 jar 파일들에 해당 이름을 가진 디스크립터만 포함해라. Java EE 5 서버가 기본 META-INF/persistence.xml 파일들만 찾기 때문에 커스텀 퍼시스턴스 유닛들은 무시해서 스프링이 주도하는 JPA 설정과의 충돌을 피한다.(예를 들어 이는 Resin 3.1에 적용된다.)

LoadTimeWeaver 인터페이스는 스프링이 제공하는 클래스로 웹 컨테이너 환경인지 어플리케이션 서버 환경인지에 따른 방법으로 JPA ClassTransformer 인스턴스를 플러그인할 수 있도록 한다. 자바 5 에이전트로 ClassTransformers를 후킹(hooking)을 하는 것은 보통 효율적이지 않다. 에이전트는 전체 가상머신에서 동작하고 로딩된 클래스를 모두 검사하는데 이는 프로덕션 서버 환경에서는 보통 원치 않는 것이다.

스프링은 다양한 환경에 대한 다수의 LoadTimeWeaver 구현체를 제공해서 VM에 대해서가 아니라 클래스 로더별로만 ClassTransformer 인스턴스를 적용할 수 있도록 한다.

LoadTimeWeaver 구현체와 설정(제너릭이나 여러 플랫폼(톰캣, 웹로직, OC4J, 글래스피시, 레신, JBoss등)에 커스터마이징하는 방법)에 대한 자세한 내용은 Section 8.8.4.5, “스프링 설정”의 AOP 부분을 참고해라.

로드타임 위빙은 언제 필요한가?

모 든 JPA 프로바이더가 JVM 에이전트를 필요로 하는 것은 아니다. 하이버네이트가 그 중 하나이다. 프로바이더나 에이전트를 필요로 하지 않거나 에이전트를 대신할 다른 것(커스텀 컴파일러나 ant 태스크를 통해 빌드시에 강화(enhancements)하는 등의)이 있다면 로드타임 위버를 사용하지 말아야 한다.

앞의 섹션에서 설명했듯이 context:load-time-weaver 설정요소를 사용해서 컨텍스트 범위의 LoadTimeWeaver를 설정할 수 있다. (이는 스프링 2.5부터 사용할 수 있다.) 모든 JPA LocalContainerEntityManagerFactoryBeans는 자동적으로 이러한 전역 위버(weaver)를 선택한다. 이 방법이 로드타임 위버를 설정할 때 선호하는 방법으로 플랫폼(웹로직, OC4J, 글래스피시, 톰캣, 레신, JBoss, VM 에이전트)의 자동 탐지와 위버를 인식한 모든 빈에 위버의 자동전파(propagation)를 제공한다.

<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  ...
</bean>

하지만 필요하다면 loadTimeWeaver 프로퍼티로 전용 위버를 수동으로 지정할 수 있다.

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="loadTimeWeaver">
    <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
  </property>
</bean>

어 떤 방법으로 LTW를 설정하는지에 관계없이 이 기법을 사용해서 인스트루멘테이션(instrumentation)에 기반한 JPA 어플리케이션을 에이전트가 필요없이 대상 플랫폼(예: 톰캣)에서 실행할 수 있다. 이는 JPA 변환자(transformer)가 클래스로더 수준에만 적용되무로 각각 격리되기 때문에 호스팅하는 어플리케이션이 여러 JPA 구현체에 의존하고 있는 경우 특히 중요하다.


14.5.1.4 여러 퍼시스턴스 유닛 다루기
예 를 들어 클래스패스에 다양한 JARS가 저장되어 있는 여러 퍼시스턴스 유닛 위치에 의존하는 어플리케이션에서 스프링은 PersistenceUnitManager가 중앙 레파지토리처럼 동작하고 상당한 비용이 들 수 있는 퍼시스턴스 유닛 탐색과정을 피하게 한다. 기본 구현체는 파싱하고 나중에 퍼시스턴스 유닛이름으로 획득하는 여러가지 위치를 지정할 수 있게 한다. (기본적으로 클래스패스는 META-INF/persistence.xml 파일을 검색한다.)

<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
  <property name="persistenceXmlLocations">
    <list>
     <value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
     <value>classpath:/my/package/**/custom-persistence.xml</value>
     <value>classpath*:META-INF/persistence.xml</value>
    </list>
  </property>
  <property name="dataSources">
   <map>
    <entry key="localDataSource" value-ref="local-db"/>
    <entry key="remoteDataSource" value-ref="remote-db"/>
   </map>
  </property>
  <!-- 데이터소스를 지정하지 않으면 이것을 사용한다 -->
  <property name="defaultDataSource" ref="remoteDataSource"/>
</bean>

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="persistenceUnitManager" ref="pum"/>
  <property name="persistenceUnitName" value="myCustomUnit"/>
</bean>

기 본 구현체는 PersistenceUnitInfo 인스턴스를 JPA 프로바이더에 제공하기 전에 커스터마이징할 수 있게 한다. 이는 모든 호스팅된 유닛에 영향을 주는 프로퍼티들을 사용해서 선언적으로 하거나 퍼시스턴스 유닛을 선택할 수 있는 PersistenceUnitPostProcessor로 프로그래밍적으로 할 수 있다. PersistenceUnitManager를 지정하지 않았다면 새로 생성해서 LocalContainerEntityManagerFactoryBean가 내부적으로 사용한다.


14.5.2 평범한 JPA에 기반한 DAO 구현하기
Note
EntityManagerFactory 인스턴스는 스레드 세이프하지만 EntityManager는 그렇지 않다. 주입한 JPA EntityManager는 JPA 명세서에 정의된 대로 어플리케이션의 JNDI 환경에서 가져온 EntityManager처럼 동작한다. 모든 호출을 (존재한다면)현재의 트랜잭션이 적용된 EntityManager로 위임한다. 존재하지 않는다면 작업마다 EntityManager를 새로 생성해서 폴백(fall back)을 수행해서 사실상 스레드 세이프하게 만든다.

스프링에 대한 의존성은 전혀 두지 않고 주입된 EntityManagerFactory나 EntityManager를 사용해서 평범한 JPA로 코드를 작성할 수 있다. PersistenceAnnotationBeanPostProcessor가 활성화되어 있다면 스프링은 필드 수준과 메서드 수준에서 @PersistenceUnit과 @PersistenceContext 어노테이션을 둘 다 이해할 수 있다. @PersistenceUnit 어노테이션을 사용하는 평범한 JPA DAO 구현체는 다음과 같을 것이다.

public class ProductDaoImpl implements ProductDao {

  private EntityManagerFactory emf;

  @PersistenceUnit
  public void setEntityManagerFactory(EntityManagerFactory emf) {
    this.emf = emf;
  }

  public Collection loadProductsByCategory(String category) {
    EntityManager em = this.emf.createEntityManager();
    try {
      Query query = em.createQuery("from Product as p where p.category = ?1");
      query.setParameter(1, category);
      return query.getResultList();
    }
    finally {
      if (em != null) {
        em.close();
      }
    }
  }
}

위의 DAO는 스프링에 대한 의존성이 없으면서도 여전히 스프링 어플리케이션 컨텍스트에 잘 어울린다. 더욱이 DAO는 기본 EntityManagerFactory의 주입을 필요로하는 어노테이션의 장점을 취한다.

<beans>
  <!-- JPA 어노테이션의 빈 후처리자(bean post-processor) -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

  <bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>

명 시적으로 PersistenceAnnotationBeanPostProcessor 정의하는 대신에 어플리케이션 컨텍스트 설정에 스프링 context:annotation-config XML 요소를 사용하는 것을 고려해 봐라. 이렇게 하면 CommonAnnotationBeanPostProcessor 등을 포함한 어노테이션기반의 설정에 대한 스프링의 모든 표준 후처리자(post-processor)를 자동으로 등록한다.

<beans>
  <!-- 모든 표준 설정 어노테이션의 후처리자 -->
  <context:annotation-config/>

  <bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>

이 러한 DAO의 가장 큰 문제점은 팩토리로 항상 새로운 EntityManager를 생성한다는 것이다. 팩토리 대신에 주입해야하는 트랜잭션이 적용된 EntityManager를(실제 트랜잭션이 적용된 EntityManager에 대한 공유되고 스레드 세이프한 프록시이기 때문에 "공유된(shared) EntityManager"라고도 부른다) 요청해서 이 문제를 피할 수 있다.

public class ProductDaoImpl implements ProductDao {

  @PersistenceContext
  private EntityManager em;

  public Collection loadProductsByCategory(String category) {
    Query query = em.createQuery("from Product as p where p.category = :category");
    query.setParameter("category", category);
    return query.getResultList(); 
  }
}

@PersistenceContext 어노테이션은 기본값이 PersistenceContextType.TRANSACTION인 선택적인 속성값 type을 가진다. 이 기본값은 공유된 EntityManager 프록시를 받는데 필요하다. 다른 값인 PersistenceContextType.EXTENDED는 완전히 다른 것이다. 이는 소위 확장된 EntityManager가 되어 스레드세이프하지 않으므로 스프링이 관리하는 싱글톤 빈처럼 동시에 접근하게 되는 컴포넌트에서는 사용하지 말아야 한다. 확장된 EntityManager는 상태를 가진 컴포넌트에서만 사용하도록 만들어졌다. 예를 들면 현재 트랜잭션에 묶인 것이 아니라 어플리케이션에 완전히 묶인 EntityManager의 생명주기를 가진 세션에 존재하는 상태를 가진 컴포넌트들이다.

주 입된 EntityManager는 스프링이 관리(진행중인 트랜잭션을 인식하는)한다. 새로운 DAO 구현체가 EntityManagerFactory 대신 EntityManager의 메서드 수준 주입을 사용할때 조차도 어노테이션의 사용때문에 어플리케이션 컨텍스트 XML을 변경할 필요는 없다는 점이 중요하다.

이 DAO 방식의 가장 큰 장점은 Java 퍼시스턴스 API에만 의존한다는 것이다. 어떤 스프링 클래스도 임포트할 필요가 없다. 게다가 JPA 어노테이션을 이해하기 때문에 스프링 컨테이너가 자동으로 주입을 적용한다. 이는 비침투적이고 JPA 개발자들이 더 자연스럽게 느낄 것이다.

메서드 수준의 주입과 필드수준의 주입

의 존성 주입(@PersistenceUnit과 @PersistenceContext같은)을 나타내는 어노테이션들은 클래스 내부의 필드나 메서드에도 적용할 수 있으므로 메서드 수준(method-level)의 주입과 필드수준(field-level)의 주입이라고 부른다. 필드수준의 어노테이션들은 간결하고 사용하운 반면 메서드 수준의 어노테이션들은 주입된 의존성에 추가적인 처리를 할 수 있게 한다. 두 가지 모두에서 멤버 가시성(public, protected, private)은 중요치 않다.

클래스수준의 어노테이션들은 어떤가?

Java EE 5 플랫폼에서 클래스 수준의 어노테이션들은 의존성 선언에 사용하고 리소스 주입에는 사용하지 않는다.



14.5.3 트랜잭션 관리
Note
스프링의 선언적인 트랜잭션 지원의 자세한 내용을 알고싶다면 (아직 읽어보지 않았다면) Section 11.5, “선언적인 트랜잭션 관리”를 읽어보기를 강력히 권한다.

트랜잭션 내에서 서비스작업을 실행하려면 스프링의 공통 선언적인 트랜잭션 기능을 사용할 수 있다. 예를 들면 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xmlns:tx="http://www.springframework.org/schema/tx"
      xsi:schemaLocation="
      http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/tx 
      http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
      http://www.springframework.org/schema/aop 
      http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

  <bean id="myTxManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="myEmf"/>
  </bean>

  <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="productDao" ref="myProductDao"/>
  </bean>
  
  <aop:config>
    <aop:pointcut id="productServiceMethods" expression="execution(* product.ProductService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="productServiceMethods"/>
  </aop:config>

  <tx:advice id="txAdvice" transaction-manager="myTxManager">
    <tx:attributes>
      <tx:method name="increasePrice*" propagation="REQUIRED"/>
      <tx:method name="someOtherBusinessMethod" propagation="REQUIRES_NEW"/>
      <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
    </tx:attributes>
  </tx:advice>

</beans>

스 프링 JPA는 같은 JDBC DataSource에 접근하는 JDBC 접근코드에 JPA 트랜잭션을 노출하려고 설정된 JpaTransactionManager를 허용해서 의존하는 JDBC Connection의 획득을 지원하는 등록된 JpaDialect를 제공한다. 스프링은 Toplink, Hibernate, OpenJPA JPA의 방언을 제공한다. JpaDialect 메카니즘에 대한 자세한 내용은 다음 섹션에서 살펴볼 것이다.


14.5.4 JpaDialect
JpaTemplate 의 고급 기능처럼 JpaTransactionManager와 AbstractEntityManagerFactoryBean의 하위클래스들은 jpaDialect 빈 프로퍼티에 전달되는 커스텀 JpaDialect를 지원한다. 이러한 시나리오에서 DAO는 EntityManagerFactory의 참조를 받는 것이 아니라 전체 JpaTemplate 인스턴스를(예를 들어 JpaDaoSupport의 jpaTemplate 프로퍼티에 전달 된다.) 받는다. JpaDialect 구현체는 일반적으로는 벤더에 특화된 방법으로 스프링이 지원하는 몇몇 고급 기능들을 활성화할 수 있다.

  • 커스텀 격리수준이나 트랜잭션 타임아웃같은 특정 트랜잭션 의미를 적용한다.
  • JDBC에 기반한 DAO에 노출하기 위한 트랜잭션이 적용된 JDBC Connection의 획득
    PersistenceExceptions를 스프링의 DataAccessExceptions로의 변환
특수한 트랜잭션 의미와 예외의 고급변환에 특히 가치가 있다. 사용한 기본 구현체 (DefaultJpaDialect)는 어떤 특수한 기능도 제공하지 않는다. 위의 기능이 필요하다면 적절한 방언(dialect)을 지정해야 한다.

JpaDialect의 작업과 JpaDialect Javadoc가 스프링의 JPA 지원내에서 어떻게 사용되는지 알고 싶다면 JpaDialect Javadoc을 참고해라.


14.6 iBATIS SQL 맵
스 프링 프레임워크의 iBATIS 지원은 스프링의 JDBC 지원과 아주 비슷하다. JDBC와 다른 ORM 기술에서 같은 템플릿 방식의 프로그래밍을 지원한다. iBATIS 지원은 스프링의 예외 계층과 동작하고 스프링의 IoC 기능을 사용할 수 있게 한다.

스 프링의 표준 시설(facilities)로 트랜잭션 관리할 수 있다. JDBC Connection외에 특별한 트랜잭션 리소스가 없기 때문에 iBATIS를 위한 특수한 트랜잭션 전략은 필요치 않다. 따라서 스프링의 표준 JDBC DataSourceTransactionManager나 JtaTransactionManager로도 완전히 충분하다.

Note
스프링은 iBATIS 2.x를 지원한다. iBATIS 1.x 지원 클래스들은 더이상 제공하지 않는다.


14.6.1 SqlMapClient 설정하기
iBATIS SQL Map을 사용한다는 것은 스테이트먼트와 리절트맵을 포하함 SqlMap 설정 파일을 생성하는 것을 포함한다. SqlMapClientFactoryBean를 사용해서 스프링이 이러한 것들을 로딩하는 것을 담당한다. 예를 들어 다음의 Account 클래스를 사용할 것이다.

public class Account {

  private String name;
  private String email; 

  public String getName() {
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getEmail() {
    return this.email;
  }

  public void setEmail(String email) {
    this.email = email;
  }
}

이 Account 클래스를 iBATIS 2.x에 매핑하려면 다음의 SQL 맵 Account.xml를 생성해야 한다.

<sqlMap namespace="Account">

  <resultMap id="result" class="examples.Account">
    <result property="name" column="NAME" columnIndex="1"/>
    <result property="email" column="EMAIL" columnIndex="2"/>
  </resultMap>

  <select id="getAccountByEmail" resultMap="result">
    select ACCOUNT.NAME, ACCOUNT.EMAIL
    from ACCOUNT
    where ACCOUNT.EMAIL = #value#
  </select>

  <insert id="insertAccount">
    insert into ACCOUNT (NAME, EMAIL) values (#name#, #email#)
  </insert>

</sqlMap>

iBATIS 2에 대한 설정파일은 다음과 같을 것이다.

<sqlMapConfig>
  <sqlMap resource="example/Account.xml"/>
</sqlMapConfig>

iBATIS가 클래스패스에서 리소스를 로딩하기 때문에 Account.xml 파일을 클래스패스에 두어야 한다는 것을 잊지 말아라.

스 프링 컨테이너에서 SqlMapClientFactoryBean를 사용할 수 있다. iBATIS SQL Map 2.x에서 JDBC DataSource는 지연 로딩을 활성화하는 SqlMapClientFactoryBean에서 보통 지정한다. 다음은 이러한 빈 정의에 필요할 설정이다.

<beans>

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
  </bean>

  <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation" value="WEB-INF/sqlmap-config.xml"/>
    <property name="dataSource" ref="dataSource"/>
  </bean>
</beans>


14.6.2 SqlMapClientTemplate와 SqlMapClientDaoSupport 사용하기
SqlMapClientDaoSupport 클래스는 SqlMapDaoSupport와 유사한 지원 클래스를 제공한다. DAO를 구현할 때 이 클래스를 확장한다.

public class SqlMapAccountDao extends SqlMapClientDaoSupport implements AccountDao {

  public Account getAccount(String email) throws DataAccessException {
    return (Account) getSqlMapClientTemplate().queryForObject("getAccountByEmail", email);
  }

  public void insertAccount(Account account) throws DataAccessException {
    getSqlMapClientTemplate().update("insertAccount", account);
  }
}

DAO 에서 쿼리를 실행하는 데 어플리케이션 컨텍스트의 SqlMapAccountDao를 설정하고 SqlMapAccountDao를 SqlMapClient 인스턴스와 연결한 후에 미리 설정된 SqlMapClientTemplate를 사용한다.

<beans>
  <bean id="accountDao" class="example.SqlMapAccountDao">
    <property name="sqlMapClient" ref="sqlMapClient"/>
  </bean>
</beans>

생 성자 아규먼트로 SqlMapClient를 전달해서 SqlMapTemplate 인스턴스를 수동으로 만들수도 있다. SqlMapClientDaoSupport 기반 클래스가 개발자들을 위해서 SqlMapClientTemplate 인서턴스를 미리 초기화한다.

SqlMapClientTemplate는 인자로 커스텀 SqlMapClientCallback 구현체를 받아들이는 일반적인 execute 메서드를 제공한다. 예를 들어 배치작업에 이를 사용할 수 있다.

public class SqlMapAccountDao extends SqlMapClientDaoSupport implements AccountDao {

  public void insertAccount(Account account) throws DataAccessException {
    getSqlMapClientTemplate().execute(new SqlMapClientCallback() {
      public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
        executor.startBatch();
        executor.update("insertAccount", account);
        executor.update("insertAddress", account.getAddress());
        executor.executeBatch();
      }
    });
  }
}

일 반적으로 네이티브 SqlMapExecutor가 제공하는 작업의 어떤 조합이라도 이러한 콜백에서 사용할 수 있다. 던져진 어떤 SQLException이라도 스프링의 일반적인 DataAccessException 계층으로 자동으로 변환된다.


14.6.3 평범한 iBATIS API에 기반한 DAO 구현하기
어떤 스프링 의존성도 없이 주입된 SqlMapClient를 직접 사용해서 DAO를 평범한 iBATIS API로 작성할 수도 있다. 다음 예제는 이에 대응되는 DAO 구현체를 보여준다.

public class SqlMapAccountDao implements AccountDao {
        
  private SqlMapClient sqlMapClient;
  
  public void setSqlMapClient(SqlMapClient sqlMapClient) {
    this.sqlMapClient = sqlMapClient;
  }

  public Account getAccount(String email) {
    try {
      return (Account) this.sqlMapClient.queryForObject("getAccountByEmail", email);
    }
    catch (SQLException ex) {
      throw new MyDaoException(ex);
    }
  }

  public void insertAccount(Account account) throws DataAccessException {
    try {
      this.sqlMapClient.update("insertAccount", account);
    }
    catch (SQLException ex) {
      throw new MyDaoException(ex);
    }
  }
}

이 시나리오에서 관행적인 방법으로 iBATIS API가 던진 SQLException를 처리해야 한다. 보통은 SQLException을 어플리케이션에 특화된 DAO 예외로 감싸서 처리한다. 평범한 iBATIS에 기반한 DAO가 의존성 주입패턴을 따르고 있기 때문에 어플리케이션 컨텍스트에서의 연결은 SqlMapClientDaoSupport 예제에서의 연결처럼 보인다.

<beans>
  <bean id="accountDao" class="example.SqlMapAccountDao">
    <property name="sqlMapClient" ref="sqlMapClient"/>
  </bean>
</beans>

2013/01/06 05:16 2013/01/06 05:16