Outsider's Dev Story

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

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

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




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 어노테이션으로 예외 변환을 투명하게 적용할 수 있게 했다.

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


<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를 설정하는 방법을 보여준다.

<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(보통 어플리케이션 서버가 관리하는)로 바꾸는 것은 단지 설정의 문제이다.

<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에 기반한 다음에 예제와 유사하다.

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는 다음과 같을 것이다.

<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 트랜잭션 인터셉터를 설정하는 방법을 보여준다.

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

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

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 어노테이션을 붙혀서 스프링 컨테이너가 이러한 어노테이션을 찾아서 어노테이션이 붙은 메서드들에 트랜잭션의 의미를 적용하도록 한다.

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/>" 엔트리가 제공해야 하는 내용의 전부이다.

<?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(..) 메서드를 통한 빈 참조가 낫다. 다음 코드는 스프링 어플리케이션 컨텍스트의 트랜잭션 관리자와 비즈니스 서비스 정의를 보여주고 비즈니스 메서드 구현체에 대한 예제를 보여준다.

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


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와 다수의 세션 팩토리에 걸친 트랜잭션의 경계를 나눌 수 있다.

<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의 실제 예외에 대한 예시이다.

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 콜백으로 해당 트랜잭션을 다시 호출하고 캐시를 적절하게 초기화할 수 있다.

2013/01/06 05:14 2013/01/06 05:14