[Info]Tags categorized posts and contents patterns..

[AJAX] Ajax Code E xamples.. [Book] About the book.. [CSS] CSS Code E xamples.. [DB] Sql Code E xamples.. [DEV] All development stor...

2016년 6월 16일 목요일

[JAVA] 14장 객체 관계 매핑 (ORM) 데이터 접근 #2..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

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

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

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


1
2
3
4
5
6
7
Xml

<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 설정이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 구현체의 예제이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java

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로 코드를 작성한 것처럼 스프링 컨테이너와 궁합이 잘 맞는다.

1
2
3
4
5
6
7
Xml

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Xml

<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 블럭이 간결해진다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Java

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 플래그를 끄도록 하는 것을 권장한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Xml

<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, “선언적인 트랜잭션 관리”를 읽어보기를 강력히 권장한다. 스프링의 선언적인 트랜잭션 지원의 자세한 내용을 알 수 있을 것이다.

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


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Xml

<?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)에 따르면)을 사용하고 대부분의 경우에는 퍼시스턴스 유닛 이름만 지정하면 된다.


1
2
3
4
5
6
7
Xml

<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 설정만 변경하면 된다.


1
2
3
4
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의 대표적인 빈 정의를 보여준다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Xml

<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 파일이다.

1
2
3
4
5
6
7
8
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)를 제공한다.


1
2
3
4
5
6
Xml

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

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

1
2
3
4
5
6
7
Xml

<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 파일을 검색한다.)


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 구현체는 다음과 같을 것이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Java

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의 주입을 필요로하는 어노테이션의 장점을 취한다.

1
2
3
4
5
6
7
8
Xml

<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)를 자동으로 등록한다.

1
2
3
4
5
6
7
8
Xml

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Java

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, “선언적인 트랜잭션 관리”를 읽어보기를 강력히 권한다.

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


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Xml

<?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 클래스를 사용할 것이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Java

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를 생성해야 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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에 대한 설정파일은 다음과 같을 것이다.

1
2
3
4
5
Xml

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

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

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


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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="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를 구현할 때 이 클래스를 확장한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Java

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를 사용한다.

1
2
3
4
5
6
7
Xml

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

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

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


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Java

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 구현체를 보여준다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Java

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 예제에서의 연결처럼 보인다.

1
2
3
4
5
6
7
Xml

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

[JAVA] 14장 객체 관계 매핑 (ORM) 데이터 접근 #1..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

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

14. 객체 관계 매핑 (ORM) 데이터 접근
14.1 스프링의 ORM 소개
스프링 프레임워크는 리소스관리, 데이터접근 객체 (DAO)의 구현체, 트랜잭션 전략에 Hibernate, Java Persistence API (JPA), Java Data Objects (JDO), iBATIS SQL 맵의 통합을 지원한다. 예를 들어 여러 가지 하이버네이트 통합 이슈들을 다루는 여러 가지 편리한 IoC 기능으로 하이버네이트를 훌륭하게 지원한다. 의존성 주입으로 O/R (객체-관계) 매핑 도구를 지원하는 모든 기능을 설정할 수 있다. 이러한 기능들은 스프링의 리소스 관리와 트랜잭션 관리에 참여할 수 있고 스프링의 일반적인 트랜잭션과 DAO 예외 계층을 따른다. 권장하는 통합방식은 평범한 하이버네이트, JPA, JDO API로 DAO를 작성하는 것이다. 스프링의 DAO 템플릿을 사용하는 과거의 방식은 더이상 권장하지 않는다. 하지만 이 방식을 부록의 Section A.1, “Classic ORM usage”에서 찾아볼 수 있다.

데이터 접근 어플리케이션을 작성할 때 스프링은 선택한 ORM 계층을 상당히 개선한다. 원하는 만큼 통합지원을 사용할 수 있고 유사한 인하우스(in-house) 인프라스트럭처를 구성하는 비용과 위험을 이 통합에 드는 노력과 비교해 봐야한다. 모든 것이 재사용가능한 JavaBean으로 설계되었으므로 기술에 관계없이 ORM 지원 라이브러리를 사용할 수 있다. 스프링 IOC 컨테이너의 ORM은 설정과 배포를 쉽게 한다. 그러므로 이번 섹션 대부분의 예제는 스프링 컨테이너내부에서의 설정을 보여준다.

ORM DAO를 작성할 때 스프링 프레임워크를 사용하는 장점은 다음과 같다.

  • 테스트를 쉽게 한다. 스프링의 IoC 접근방법은 하이버네이트 SessionFactory 인스턴스, JDBC DataSource 인스턴스, 트랜잭션 매니저, (필요하다면)매핑된 객체구현체의 구현체와 설정위치를 쉽게 바꿀 수 있게 한다. 이는 퍼시스턴스와 관계된 코드를 각각 독립적으로 쉽게 테스트할 수 있게 한다.
  • 공통적인 데이터 접근 예외. 스프링은 ORM도구의 예외를 감싸서 소유자의 예외(아다고 체크드 예외)를 공통적인 런타임 DataAccessException 계층으로 변환한다. 이 기능은 불편하게 catches, throws, exception 선언을 매번 하지 않고도 적절한 계층해서만 복구할 수 없는 대부분의 퍼시스턴스 예외를 다룰 수 있게 한다. 여전히 필요에 따라 예외를 잡아서 처리할 수 있다. JDBC 예외(DB에 특화된 방언들을 포함해서)도 같은 계층으로 변환된다는 점을 기억해라. 이는 일관된 프로그래밍 모델내에서 JDBC로 하는 직업할수 있다는 것을 의미한다.
  • 일반적인 리소스 관리. 스프링 어플리케이션 컨텍스트는 하이버네이트 SessionFactory 인스턴스, JPA EntityManagerFactory 인스턴스, JDBC DataSource 인스턴스, iBATIS SQL 맵 설정 객체와 그외 관련된 리소스들의 위치와 설정을 다룰 수 있다. 이 기능은 이러한 값들을 쉽게 관리하고 바꿀 수 있게 한다. 스프링은 퍼시스턴스 리소스를 효율적이고 쉽고 안전하게 다루도록 해준다. 예를 들어 하이버네이트를 사용하는 코드는 일반적으로 효율성과 적절한 트랜잭션 처리를 보장하도록 같은 하이버네이트 Session을 사용해야 한다. 스프링은 하이버네이트 SessionFactory로 현재의 Session을 노출함으로써 투명하게 현재 쓰레드에 Session을 쉽게 생성해서 바인딩할 수 있게 해준다. 그러므로 일반적으로 로컬이나 JTA 트랜잭션 환경에서 하이버네이트를 사용할 때의 만성적인 많은 문제들을 스프링이 해결한다.
  • 통합된 트랜잭션 관리. @Transactional 어노테이션을 사용하거나 XML 설정파일에 트랜잭션 AOP 어드바이스를 명시적으로 설정함으로써 선언적이면서 관점지향 프로그래밍(AOP) 방식의 메서드 인터셉터로 ORM 코드를 감쌀 수 있다. 두 경우 모두에서 트랜잭션의 의미와 예외처리(롤백 등)를 개발자 대신 다뤄준다. 아래의 리소스와 트랜잭션 관리에서 설명하는 것처럼 ORM과 관련된 코드에 영향을 주지 않고 다양한 트랜잭션 관리자를 바꿔치기 할 수도 있다. 예를 들어 두가지 시나리오 모두에서 사용할 수 있는 동일한 전체 서비스(선언적인 트랜잭션같은)에서 로컬 트랜잭션과 JTA를 바꿔치기 할 수 있다. 게다가 JDBC와 관계된 코드를 트랜잭션을 적용해서 ORM을 사용하는 코드와 완전히 통합할 수 있다. 이는 ORM 작업과 공통된 트랜잭션을 공유다는데 여전히 필요한 배치작업이나 BLOB 스트리밍같은 ORM에 맞지 않은 데이터접근에 유용하다.
TODO: 현재 샘플에 링크 제공하기

14.2 일반적인 ORM 통합에 고려해야할 사항들
이번 섹션에서는 모든 ORM 기술을 적용할 때의 고려사항을 설명한다. Section 14.3, “하이버네이트(Hibernate)” 섹션에서 더 자세한 내용을 제공하고 고정된(concrete) 컨텍스트에서의 이러한 기능과 설정도 보여준다.

스프링 ORM 통합의 주요 목표는 어떤 데이터접근 기술이나 트랜잭션 기술에서도 어플리케이션 객체와 커플링하지 않고 깔끔하게 어플리케이션을 계층화하는 것이다. 더이상 비즈니스 서비스가 데이터 접근전략이나 트랜잭션 전략에 의존하지 않고 더이상 하드코딩된 리소스 검색을 사용하지 않고 더이상 직접 싱글톤을 교체하지 않고 더이상 커스텀 서비스를 등록하지 않는다. 어플리케이션 객체들을 연결하는 간단하고 일관된 하나의 접근방법은 이 객체들을 재사용가능하고 가능한한 컨테이너 의존성에서 자유롭게 유지하는 것이다. XML에 기반한 설정과 스프링에 의존하지 않는 평범한 JavaBean 인스턴스의 상호참조를 제공하면서 개별적인 모든 데이터 접근 기능은 스프링의 어플리케이션 컨텍스트의 개념과 잘 통합해서 사용할 수 있다. 전형적인 스프링 어플리케이션에서 중요한 객체들의 대다수는 JavaBean이다: 데이터접근 템플릿, 데이터접근 객체, 트랜잭션 관리자, 데이터접근 객체와 트랜잭션 관리자를 사용하는 비즈니스 서비스, 웹뷰 리졸버, 비즈니스 서비스를 사용하는 웹 컨트롤러 등이다.

14.2.1 리소스 관리와 트랜잭션 관리
전형적인 비즈니스 어플리케이션들은 반복적인 리소스관리 코드로 엉망이 된다. 많은 프로젝트들이 자신만의 해결책을 만들려고 시도하고 때로는 프로그래밍이 편리하도록 적절한 실패처리를 희생하기도 한다. 스프링은 JDBC를 사용하는 경우와 ORM 기술에 AOP 인터셉터를 적용하는 경우에 적절한 리소스 처리 즉, 템플릿을 통한 IoC로 간단한 해결책을 지지한다.

인프라스트럭처가 리소스를 적절히 처리하고 특정 API 예외를 언체크드 인프라스트럭처 예외 계층으로 적절히 변환한다. 스프링은 어떤 데디터접근 전략에도 적용할 수 있는 DAO 예외 계층을 적용했다. JDBC를 직접 사용할 때 앞의 섹션에서 얘기한 JdbcTemplate 클래스는 커넥션을 관리하고 SQLException을 DataAccessException 계층으로(데이터베이스에 특화된 SQL 오류코드를 의미있는 예외클래스로 변환하는 것을 포함해서) 변환한다. ORM 기술을 사용할 때는 동일한 예외 변환의 이점을 어떻게 얻는지 다음 섹션에서 볼 것이다.

트랜잭션 관리에 대해서 JdbcTemplate 클래스는 스프링 트랜잭션 지원을 따르고 JTA와 JDBC 트랜잭션을 각각의 스프링 트랜잭션 관리자를 통해서 모두 지원한다. ORM 기술과 관련해서 스프링은 JTA 지원만큼이나 하이버네이트, JPA, JDO 트랜잭션 관리자로 하이버네이트, JPA, JDO를 지원한다. 트랜잭션 지원에 대한 자세한 내용은 Chapter 11, 트랜잭션 관리장을 참고해라.

14.2.2 예외 변환(translation)
DAO에서 하이버네이트, JAP, JDO를 사용할 때는 반드시 퍼시스턴스 기술의 네이티브 예외클래스들을 어떻게 다룰 것인지 결정해야 한다. DAO는 기술에 따라 HibernateException, PersistenceException, JDOException의 하위 클래스를 던진다. 이러한 예외들은 모두 런타임 예외이고 선언하지 않거나 잡지(caught) 않아야 한다. IllegalArgumentException와 IllegalStateException도 다루어야 한다. 즉 호출자(caller)는 퍼시스턴스 기술 자체의 예외 구조에 의존하지 않고 예외들을 일번적으로 치명적인 것으로만 다룰 수 있다. 호출자가 구현전략에 묶이지 않고는 낙관적인 작금(locking) 실패같은 특정 원인을 잡는 것은 불가능하다. 이 트래이드오프는 어플리케이션이 상당히 ORM기반이거나 특별한 예외처리가 필요없다면 수긍할만할 것이다. 하지만 스프링은 @Repository 어노테이션으로 예외 변환을 투명하게 적용할 수 있게 했다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Java

@Repository
public class ProductDaoImpl implements ProductDao {
  // 클래스 바디...
}

Xml

<beans>
  <!-- Exception translation bean post processor -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

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

postprocessor는 자동으로 모든 예외변환자(exception translators)를 검색하고(PersistenceExceptionTranslator 인터페이스의 구현체) 찾아낸 변환자가 던져진 예외를 가로채서 적절히 변화하도록 @Repository 어노테이션이 붙은 모든 빈을 어드바이즈한다.

요약하자면 스프링이 관리하는 트랜잭션, 의존성 주입, (원한다면)스프링 커스텀 예외계층으로의 투명한 예외 변환의 이점을 가지면서 평범한 퍼시스턴스 기술의 API와 어노테이션에 기반해서 DAO를 구현할 수 있다.

14.3 하이버네이트(Hibernate)
스프링환경에서 Hibernate 3를 사용해서 스프링이 O/R 매퍼를 통합하는 접근방법을 보여줄 것이다. 이 섹션은 많은 이슈들을 자세히 다루고 여러가지 다양한 DAO 구현체와 트랜잭션 경계를 보여줄 것이다. 이러한 방식은 지원하는 다른 모든 ORM 모두에도 바로 적용할 수 있다. 이 장에서 이어지는 섹션에서는 다른 ORM 기술들을 다루고 핵심 예제들을 보여줄 것이다.



Note
스프링 3.0부터는 하이버네이트 3.2 이상의 버전이 필요하다.

14.3.1 스프링 컨테이너의 SessionFactory 설정
어플리케이션 객체가 하드코딩된 리소스 검색에 묶이는 것을 피하려면 JDBC DataSource나 하이버네이트 SessionFactory같은 리소스를 스프링 컨테이너의 빈으로 정의할 수 있다. 리소스에 접근해야 하는 어플리케이션 객체들은 다음 섹션의 DAO 정의에서 보여주듯이 빈 레퍼런스로 미리 정의한 인스턴스에 대한 참조를 받는다.

XML 어플리케이션 컨텍스트 정의에서 인용한 다음 코드는 JDBC DataSource와 Hibernate SessionFactory를 설정하는 방법을 보여준다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Xml

<beans>
  <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
    <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
    <property name="username" value="sa"/>
    <property name="password" value=""/>
  </bean>

  <bean id="mySessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource"/>
    <property name="mappingResources">
      <list>
        <value>product.hbm.xml</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <value>
        hibernate.dialect=org.hibernate.dialect.HSQLDialect
      </value>
    </property>
  </bean>
</beans>

로컬 Jakarta Commons DBCP BasicDataSource를 JNDI에 있는 DataSource(보통 어플리케이션 서버가 관리하는)로 바꾸는 것은 단지 설정의 문제이다.

1
2
3
4
5
Xml

<beans>
  <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

JNDI에 위치한 SessionFactory를 획득해서 노출하는 스프링의 JndiObjectFactoryBean / <jee:jndi-lookup>을 사용해서 JNDI에 위치한 SessionFactory에 접근할 수도 있다. 하지만 대체적으로 EJB 컨텍스트의 외부에서 일반적인 방식은 않다.

14.3.2 평범한 하이버네이트 3 API에 기반해서 DAO 구현하기
하이버네이트 3는 하이버네이트가 직접 트랜잭션마다 하나의 Session을 관리하는 문맥상의 세션(contextual sessions)이라는 기능을 가진다. 이는 대략적으로는 트랜잭션마다 하나의 하이버네이트 Session에 대한 스프링의 동기화와 동일하다. 이에 맞는 DAO 구현체는 평범한 하이버네이트 API에 기반한 다음에 예제와 유사하다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Java

public class ProductDaoImpl implements ProductDao {

  private SessionFactory sessionFactory;

  public void setSessionFactory(SessionFactory sessionFactory) {
    this.sessionFactory = sessionFactory;
  }

  public Collection loadProductsByCategory(String category) {
    return this.sessionFactory.getCurrentSession()
        .createQuery("from test.Product product where product.category=?")
        .setParameter(0, category)
        .list();
  }
}

이 방식은 인스턴스 변수에 SessionFactory를 가지고 있는 것을 빼면 하이버네이트 레퍼런스문서와 예제에 있는 것과 유사하다. 하이버네이스 CaveatEmptor 예제 어플리케이션의 오래된 static HibernateUtil 클래스보다 이러한 인스턴스 기반의 설정을 훨씬 더 추천한다.(일반적으로 절대적으로 필요한게 아니라면 static 변수에 어떤 리소스도 가지지 말아야 한다.)

위의 DAO는 의존성 주입 패턴을 따른다. 스프링의 HibernateTemplate로 코딩했다면 DAO는 스프링 IoC 컨테이너에 아주 잘 맞다. 물론 DAO는 평범한 자바로도 설정할 수 있다. (예를 들면 유닛테스트) 그냥 DAO를 인스턴스화하고 원하는 팩토리의 참조로 setSessionFactory(..)를 호출해라. 스프링의 빈 정의처럼 DAO는 다음과 같을 것이다.


1
2
3
4
5
6
7
Xml

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

이 DAO 방식의 가장 큰 이점은 DAO가 하이버네이트 API에만 의존한다는 것이다. 어떤 스프링 클래스도 임포트하지 않아도 된다. 물론 이는 비침투적인 관점에서 매력적이고 하이버네이트 개발자들이 훨씬 자연스럽게 느낄 것이다.

하지만 DAO는 평범한 HibernateException(언체크드이므로 선언하거나 잡지(caught) 말아야 한다.)를 던진다. 그래서 하이버네이트의 예외 계층에 의존하지 않는다면 호출자가 일반적으로 치명적인 오류로만 예외를 다룰 수 있다. 호출자와 구현 전략이 묶이지 않는 한 낙관적인 작금(locking) 실패같은 특정 원인을 잡는 것은 불가능하다. 이 트레이드오프는 이 트래이드오프는 어플리케이션이 크게 하이버네이트 기반이거나 특별한 예외처리가 필요없다면 수긍할만할 것이다.

다행히도 스프링의 LocalSessionFactoryBean이 어떤 스프링 트랜잭션 전략에서도 하이버네이트의 SessionFactory.getCurrentSession() 메서드를 지원해서 현재 스프링이 관리하는 트랜잭션이 적용된 Session을 HibernateTransactionManager와 함께 반환한다. 물론 SessionFactory.getCurrentSession() 메서드의 표준동작은 진행중인 JTA 트랜잭션(존재한다면)과 관련된 현재 Session을 반환하는 메서드인채로 남아있다. 이 동작은 스프링의 JtaTransactionManager를 사용하든지 EJB 컨테이터가 관리하는 트랜잭션 (CMT)를 사용하든지 JTA를 사용하든지에 관계없이 적용된다.

요약하면 스프링이 관리하는 트랜잭션에 여전히 참여할 수 있으면서 평범한 하이버네이트 3 API에 기반한 DAO를 구현할 수 있다.

14.3.3 선언적인 트랜잭션 경계
스프링의 선언적인 트랜잭션 지원을 사용하기를 권장한다. 스프링의 선언적인 트랜잭션 지원으로 AOP 트랜잭션 인터셉터를 가진 자바코드에서 명백한 트랜잭션 경계 API 호출을 교체할 수 있다. 이 트랜잭션 인터셉터는 자바 어노테이션이나 XML을 사용해서 스프링 컨테이너에서 설정할 수 있다.이 선언적인 트랜잭션 기능으로 비즈니스 서비스가 반복적인 트랜잭션 경계 코드에서 자유롭게 해서 어플리케이션의 실제 가치인 비즈니스 로직을 추가하는데 집중할 수 있게 한다.



Note
계속 읽기 전에 Section 11.5, “선언적인 트랜잭션 관리”를 읽어보기를 강력히 권장한다.

게다가 전파 동작과 격리수준같은 트랜잭션의 의미는 설정파일에서 바꿀 수 있고 비즈니스 서비스 구현체에는 영향을 끼치지 않는다.

다음 예제는 XML을 사용해서 간단한 서비스 클래스에 AOP 트랜잭션 인터셉터를 설정하는 방법을 보여준다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Xml

<?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">

  <!-- SessionFactory, DataSource등은 생략한다 -->

  <bean id="transactionManager" 
            class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
  </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>

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

다음은 어드바이즈된 서비스 클래스다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Java

public class ProductServiceImpl implements ProductService {

  private ProductDao productDao;

  public void setProductDao(ProductDao productDao) {
    this.productDao = productDao;
  }

  // 이 메서드에는 트랜잭션 경계 코드가 빠져있다
  // 스프링의 선언적인 트랜잭션 인프라스트럭쳐가 개발자를 위해서 트랜잭션 경계를 정한다
  public void increasePriceOfAllProductsInCategory(final String category) {
    List productsToChange = this.productDao.loadProductsByCategory(category);
    // ...
  }
}

다음 예제에서는 속성-지원(attribute-support)에 기반한 설정도 보여주고 있다. 서비스 계층에 @Transactional 어노테이션을 붙혀서 스프링 컨테이너가 이러한 어노테이션을 찾아서 어노테이션이 붙은 메서드들에 트랜잭션의 의미를 적용하도록 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Java

public class ProductServiceImpl implements ProductService {

  private ProductDao productDao;

  public void setProductDao(ProductDao productDao) {
    this.productDao = productDao;
  }

  @Transactional
  public void increasePriceOfAllProductsInCategory(final String category) {
    List productsToChange = this.productDao.loadProductsByCategory(category);
    // ...
  }

  @Transactional(readOnly = true)
  public List<Product> findAllProducts() {
    return this.productDao.findAllProducts();
  }
}

다음 예제 설정에서 볼 수 있듯이 앞의 XML 예제와 비교해서 설정이 훨씬 간단해졌고 서비스계층 코드의 어노테이션이 주도해서 같은 기능을 제공한다. TransactionManager 구현체와 "<tx:annotation-driven/>" 엔트리가 제공해야 하는 내용의 전부이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Xml

<?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">

  <!-- SessionFactory, DataSource등은 생략한다 -->

  <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
  </bean>
  
  <tx:annotation-driven/>

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


14.3.4 프로그래밍적인 트랜잭션 경계
어플리케이션의 더 높은 수준(다수에 작업에 걸쳐있는 저수준의 데이터 접근 서비스 상위에)에서 트랜잭션의 경계를 정할 수 있다. 비즈니스 서비스 환경에 대한 구현체에 제약은 전혀 없다. 단지 스프링 PlatformTransactionManager가 필요하다. 다시 말하자면 PlatformTransactionManager는 어디에나 올 수 있지만 productDAO를 setProductDao(..)로 설정해야 하는 것처럼 setTransactionManager(..) 메서드를 통한 빈 참조가 낫다. 다음 코드는 스프링 어플리케이션 컨텍스트의 트랜잭션 관리자와 비즈니스 서비스 정의를 보여주고 비즈니스 메서드 구현체에 대한 예제를 보여준다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Xml

<beans>
  <bean id="myTxManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="mySessionFactory"/>
  </bean>

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

Java

public class ProductServiceImpl implements ProductService {

  private TransactionTemplate transactionTemplate;
  private ProductDao productDao;

  public void setTransactionManager(PlatformTransactionManager transactionManager) {
    this.transactionTemplate = new TransactionTemplate(transactionManager);
  }

  public void setProductDao(ProductDao productDao) {
    this.productDao = productDao;
  }

  public void increasePriceOfAllProductsInCategory(final String category) {
    this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {

        public void doInTransactionWithoutResult(TransactionStatus status) {
          List productsToChange = this.productDao.loadProductsByCategory(category);
          // price를 증가시킨다...
        }
      }
    );
  }
}

스프링의 TransactionInterceptor는 어떤 체크드 어플리케이션 예외라도 콜백코드와 함게 던져지도록 하고 TransactionTemplate는 콜백내의 언체크드 예외로만 제한된다. TransactionTemplate는 언체크드 어플리케이션 예외가 생기거나 어플리케이션이 롤백전용(rollback-only)으로 트랜잭션으로 표시한(TransactionStatus를 통해서) 경우 롤백을 실행한다. 기본적으로 TransactionInterceptor도 같은 방식으로 동작하지만 메서드마다 롤백 정책을 설정할 수 있다.

14.3.5 트랜잭션 관리 전략
TransactionTemplate와 TransactionInterceptor 둘 다 실제 트랜잭션 처리를 PlatformTransactionManager 인스턴스에 위임한다. PlatformTransactionManager는 하이버네이트 어플리케이션에서 HibernateTransactionManager(내부에서 ThreadLocal Session를 사용하는 하나의 하이버네이트 SessionFactory에 대해서)이거나 JtaTransactionManager(컨테이너의 JTA 하위시스템에 위임하는)가 될 수 있다. 커스텀 PlatformTransactionManager 구현체를 사용할 수도 있다. 어플리케이션의 특정 배포에서 분산 트랜잭션 요구사항이 필요한 경우에서처럼 네이티브 하이버네이트 트랜잭션 관리에서 JTA로 전환하는 것은 설정만으로 가능하다 그냥 하이버네이트 트랜잭션 메니저를 스프링의 JTA 트랜잭션 구현체로 교체해라. 트랜잭션 경계와 데이터접근 코드는 일반적인 트랜잭션 관리 API를 사용할 것이므로 변경없이 모두 동작할 것이다.

여러 하이버네이트 세션 팩토리들 사이에 분산된 트랜잭션에서는 트랜잭션 전략인 JtaTransactionManager를 여러 LocalSessionFactoryBean 정의와 합친다. 그러면 각 DAO는 하나의 특정 SessionFactory 참조를 DAO의 대응되는 빈 프로퍼티로 얻는다. 의존하는 모든 JDBC 데이터소스가 트랜잭션이 적용된 컨테이너의 데이터소스라면 전략으로 JtaTransactionManager를 사용하는 한 특별히 신경쓰지 않고 비즈니스 서비스는 다수의 DAO와 다수의 세션 팩토리에 걸친 트랜잭션의 경계를 나눌 수 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Xml

<beans>
  <jee:jndi-lookup id="dataSource1" jndi-name="java:comp/env/jdbc/myds1"/>

  <jee:jndi-lookup id="dataSource2" jndi-name="java:comp/env/jdbc/myds2"/>

  <bean id="mySessionFactory1"
            class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource1"/>
    <property name="mappingResources">
      <list>
        <value>product.hbm.xml</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <value>
        hibernate.dialect=org.hibernate.dialect.MySQLDialect
        hibernate.show_sql=true
      </value>
    </property>
  </bean>

  <bean id="mySessionFactory2"
            class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource2"/>
    <property name="mappingResources">
      <list>
        <value>inventory.hbm.xml</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <value>
        hibernate.dialect=org.hibernate.dialect.OracleDialect
      </value>
    </property>
  </bean>

  <bean id="myTxManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="sessionFactory" ref="mySessionFactory1"/>
  </bean>

  <bean id="myInventoryDao" class="product.InventoryDaoImpl">
    <property name="sessionFactory" ref="mySessionFactory2"/>
  </bean>

  <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="productDao" ref="myProductDao"/>
    <property name="inventoryDao" ref="myInventoryDao"/>
  </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>

HibernateTransactionManager와 JtaTransactionManager는 둘 다 컨테이너에 특화된 트랜잭션 관리자 검색이나 JCA 커넥터없이 하이버네이트로 JVM 수준의 적절히 캐치 처리를 할 수 있다.

HibernateTransactionManager는 특정 DataSource에 대한 하이버네이트 JDBC Connection을 평범한 JDBC 접근 코드로 내보낼 수 있다. 이 기능은 데이터베이스를 하나만 사용하고 있다면 하이버네이트와 JDBC 데이터접근을 섞어서 사용하면서 JTA없이 고수준의 트랜잭션 경계를 가능하게 한다. LocalSessionFactoryBean 클래스의 dataSource 프로프터로 DataSource를 가진 SessionFactory를 설정했다면 HibernateTransactionManager는 자동으로 하이버네이트 트랜잭션을 JDBC 트랜잭션으로 노출한다. 다른 방법으로는 HibernateTransactionManager 클래스의 dataSource 프로퍼티를 통해서 노출되도록 트랜잭션이 적용된 DataSource를 명시적으로 지정할 수 있다.

14.3.6 컨테이너가 관리하는 리소스와 로컬에 정의한 리소스 비교
코드는 단 한줄도 바꾸지 않고 컨테이너가 관리하는 JNDI SessionFactory 와 로컬 정의한 SessionFactory간에 전환을 할 수 있다. 컨테이너에 리소스 정의를 유지할 것인지 어플리케이션의 로컬로 리소스 정의를 유지할 것인지는 주로 사용하는 트랜잭션 전략에 달려있다. 스프링이 정의한 로컬 SessionFactory과 비교해서 수동으로 등록한 JNDI SessionFactory는 어떤 장점도 제공하지 않는다. 하이버네이트의 JCA 커텍터를 통한 SessionFactory 배포는 자바 EE 서버의 관리 인프라스트럭처에 참여하는 장점이 있지만 그 이상의 실제적인 가치를 주지는 않는다.

스프링의 트랜잭션 지원은 컨테이너에 의존하지 않는다. JTA외에 다른 전략으로 설정한 트랜잭션 지원도 독립적인 환경이나 테스트환경에서 동작한다. 특히 단일 데이터베이스 트랜잭션을 사용하는 대표적인 경우에 스프링의 단일 리소스 로컬 트랜잭션 지원은 JTA의 가볍고 강력한 대안이다. 트랜잭션을 유도하려고 로컬 EJB의 상태가 없는 세션 빈을 사용하는 경우 하나의 데이터베이스만 사용하고 컨테이너가 관리하는 트랜잭션을 통해 선언적인 트랜잭션을 제공하는데만 상태가 없는 세션 빈을 사용하더라도 EJB 컨테이너와 JTA에 모두 의존한다. 또한, JTA를 프로그래밍적으로 직접 사용하려면 Java EE 환경이 필요하다. JTA는 JTA 자체와 JNDI DataSource 인스턴스와 관련된 컨테이너 의존성만 포함하지 않는다. 스프링이 아니면서 JTA 주도적인 하이버네이트 트랜잭션에서는 하이버네이트 JCA 커넥터를 사용하거나 적절한 JVM 수준의 캐싱으로 설정된 TransactionManagerLookup로 추가적인 하이버네이트 트랜잭션 코드를 사용해야 한다.

하나의 데이터베이스만 접근한다면 스프링이 주도하는 트랜잭션은 로컬 JDBC DataSource 를 사용하듯이 로컬에 정의한 Hibernate SessionFactory와도 잘 동작할 수 있다. 그러므로 분산 트랜잭션이 필요하다면 스프링의 JTA 트랜잭션 전략만 사용해야 한다. JCA 커넥터는 컨테이너에 특화된 배포과정을 필요로 하고 당연히 JCA는 지원해야 한다. 로컬 리소스 정의와 스프링이 주도하는 트랜잭션을 가진 간단한 웹 어플리케이션을 배포하는 것보다는 이 설정이 더 많은 작업을 필요로 한다. JCA를 제공하지 않는 WebLogic Express를 사용한다면 컨테이너의 엔터프라이즈 에디션도 필요할 것이다. 로컬 리소스와 하나의 데이터베이스에 걸친 트랜잭션이 있는 스프링 어플리케이션은 톰캣, 레진(Resin), 제티(Jetty)같은 Java EE 웹 컨테이너(JTA, JCA, EJB없이)에서 잘 동작한다. 추가적으로 데스트탑 어플리케이션이나 테스트 슈트에서 미들티어(middle tier)같은 것을 쉽게 재사용할 수 있다.

EJB를 사용하지 않는다면 모든 것을 고려해서 로컬 SessionFactory와 스프링의 HibernateTransactionManager나 JtaTransactionManager를 사용한다. 불판한 컨테이너 배포없이 JVM 수준의 트랜잭션이 적용된 적절한 캐싱과 분산 트랜잭션같은 모든 이점을 얻게 된다. JCA 커넥터를 통한 하이버네이트 SessionFactory의 JNDI 등록은 EJB와 함께 사용할 때만 추가적인 가치를 준다.

14.3.7 하이버네이트의 잘못된(spurious) 어플리케이션 서버 경고
아주 엄격한 XADataSource 구현체가 있는 몇몇 JTA 환경에서(현재는 WebLogic 서버와 WebSphere의 일부 버전에서만) 이 환경에 대한 JTA PlatformTransactionManager 객체와 관련된 것 없이 하이버네이트를 설정했을 때 어플리케이션 서버 로그에 잘못된 경고나 예외가 나타날 수 있다. 이러한 경고나 예외는 트랜잭션이 더이상 활성화상태가 아니기 때문에 접근한 연결이 더이상 유효하지 않거나 JDBC 접근이 더이상 유용하지 않다고 나타낼 수 있다. 다음은 WebLogic의 실제 예외에 대한 예시이다.


1
2
3
4
C-like

java.sql.SQLException: The transaction is no longer active - status: 'Committed'.
  No further JDBC access is allowed within this transaction.

하이버네이트를 인식하고 (스프링과 일치하도록) 동기화할 JTA PlatformTransactionManager 인스턴스를 만들어서 이 경고를 해결한다. 이 처리를 하는데는 두가지 선택사항이 있다.
  • 예를 들어 어플리케이션 컨텍스트에서 JTA PlatformTransactionManager 객체를 이미 직접 획득했고 이 객체를 스프링의 JtaTransactionManager에 제공하는 경우 가장 쉬운 방법은 이 JTA PlatformTransactionManager 인스턴스를 LocalSessionFactoryBean.의 jtaTransactionManager 프로퍼티의 값으로 정의하는 빈의 참조를 지정하는 것이다. 그러면 스프링이 이 객체를 하이버네이트가 이용할 수 있게 한다.
  • 스프링의 JtaTransactionManager가 JTA PlatformTransactionManager 인스턴스를 직접 찾을 수 있기 때문에 아마 JTA PlatformTransactionManager 인스턴스를 이미 가지고 있는 경우는 드물 것이다. 그러므로 하이버네이트가 JTA PlatformTransactionManager를 직접 검색하도록 설정해야 한다. 하이버네이트 메뉴얼에 나와있듯이 하이버네이트 설정에 어프리케이션 서버에 특화된 TransactionManagerLookup 클래스를 설정해서 이 설정을 할 수 있다.
이 섹션의 남은 부분에서는 하이버네이트가 JTA PlatformTransactionManager를 인식하거나 인식하지 않고 발생하는 이벤트의 순서를 설명한다.

하이버네이트를 JTA PlatformTransactionManager를 인식하도록 설정하지 않았을 경우 JTA 트랜잭션을 커밋할 때 다음 이벤트들이 발생한다.

  1. JTA 트랜잭션을 커밋한다.
  2. 스프링의 JtaTransactionManager를 JTA 트랜잭션과 동기화해서 JTA 트랜잭션 관리자가 afterCompletion 콜백으로 다시 트랜잭션을 호출한다.
  3. 다른 활동중에 이 동기화는 하이버네이트 afterTransactionCompletion 콜백(하이버네이트의 캐시를 초기화하는데 사용하는)으로 스프링의 콜백을 하이버네이트로 발생시킬 수 있다. 이어서 하이버네이트 세션에서 명시적으로 close()를 호출해서 하이버네이트가 JDBC 연결을 close()하도록 할 수 있다.
  4. 몇몇 환경에서는 어플리케이션 서버가 Connection을 더이상 사용할 수 없다고 간주해서 트랜잭션은 이미 커밋되었기 때문에 이 Connection.close()를 호출하면 경고나 오류가 발생한다.
JTA PlatformTransactionManager를 인식하도록 하이버네이트를 설정했을 때 JTA 트랜잭션을 커밋하면 다음의 이벤트가 발생한다.
  1. JTA 트랜잭션이 커밋할 준비를 한다.
  2. 스프링의 JtaTransactionManager는 JTA 트랜잭션과 동기화하므로 JTA 트랜잭션 관리자가 beforeCompletion 콜백으로 해당 트랜잭션을 다시 호출한다.
  3. 스프링은 하이버네이트가 직접 JTA 트랜잭션과 동기화했다는 것을 인지하고 앞의 시나리오와는 다르게 동작한다. 하이버네이트 Session을 완전히 닫아야 한다고 가정하면 스프링을 바로 닫을 것이다.
  4. JTA 트랜잭션을 커밋한다.
  5. 하이버네이트는 JTA 트랜잭션과 동기화하기 때문에 JTA 트랜잭션 관리자가 afterCompletion 콜백으로 해당 트랜잭션을 다시 호출하고 캐시를 적절하게 초기화할 수 있다.