Outsider's Dev Story

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

[Spring 레퍼런스] 11장 트랜잭션 관리 #1

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



Part IV. 데이터 접근
레퍼런스 문제에서 이번 파트는 데이터 접근과 데이터접근 계층과 비즈니스 계층이나 서비스 계층간의 데이터 상호작용에 관한 것이다.

스프링의 광범위한 트랜잭션 관리 지원은 스프링 프레임워크와 통합되는 여러가지 데이터 접근 프레임웍이나 기술의 영역까지 다룬다.

  • Chapter 11, 트랜잭션 관리
  • Chapter 12, DAO 지원
  • Chapter 13, JDBC를 사용한 데이터 접근
  • Chapter 14, Object Relational Mapping (ORM) Data Access
  • Chapter 15, Marshalling XML using O/X Mappers


11. 트랜잭션 관리

11.1 스프링 프레임워크의 트랜잭션 관리 소개
스프링 프레임워크를 사용하는 주목할 만한 이유중 하나가 광범위한 트랜잭션 지원이다. 스프링 프레임워크는 다음의 이점을 주는 트랜잭션 관리의 일관성있는 추상화를 제공한다.

  • Java Transaction API (JTA), JDBC, Hibernate, Java Persistence API (JPA), Java Data Objects (JDO)같은 여러 가지 트랜잭션 API간에 일관성있는 프로그래밍 모델
  • 선언적인 트랜잭션 관리 지원.
  • 프로그래밍적인 트랜잭션 관리에 대해 JTA 같은 복잡한 트랜잭션 API보다 더 간단한 API.
  • 스프링의 데이터 접근 추상화와의 뛰어난 통합.
스프링 프레임워크 트랜잭션의 가치와 기술을 다음의 섹션들에서 설명한다.(이번 장은 베스트 프렉티스, 어플리케이션 서버 통합, 일반적인 문제에 대한 해결책에 대한 설명도 포함하고 있다.)

  • 스프링 프레임워크의 트랜잭션 지원 모델의 장점에서는 EJB의 컨테이너가 관리하는 트랜잭션(Container-Managed Transactions, CMT)이나 하이버네이트같은 소유권이 있는 API(proprietary API)를 통해서 로컬 트랜잭션을 유도하도록 선택하는 대신에 스프링 프레임워크의 트랜잭션 추상화를 왜 사용해야 하는지 설명한다.
  • 스프링 프레임워크의 트랜잭션 추상화 이해하기에서는 핵심 클래스를 설명하고 다양한 소스의 DataSource 인스턴스를 어떻게 설정하고 획득하는지 설명한다.
  • 리소스와 트랜잭션 동기화하기에서는 리소스를 생성하고 재사용하고 적절히 정리하는 것을 어플리케이션 코드가 어떻게 보장하는지 설명한다.
  • 선언적인 트랜잭션 관리에서는 선언적인 트랜잭션 관리에 대한 지원을 설명한다.
  • 프로그래밍적인 트랜잭션 관리에서는 프로그래밍적인(즉, 명시적으로 코딩해서) 트랜잭션 관리에 대한 지원을 다룬다.

11.2 스프링 프레임워크의 트랜잭션 지원 모델의 장점
전통적으로 Java EE 개발자들은 트랜잭션 관리의 두가지 선택권을 가졌다. 전역(global)이나 지역(local) 두가지 선택권 모두 난해한 제약을 가지고 있다. 전역 트랜잭션 관리와 지역 트랜잭션 관리는 다음 두 섹션에서 살펴보고 그 뒤에는 스프링 프레임워크의 트랜잭션 관리 지원이 전역과 지역 트랜잭션 모델의 제약사항을 어떻게 다루는지에 대한 설명이 이어진다.


11.2.1 전역 트랜잭션
전역 트랜잭션은 보통 관계형 데이터베이스와 메시지 큐같은 여러가지 트랜잭션이 적용된 리소스로 작업할 수 있게 한다.어플리케이션 서버는 사용하기 어려운 API를 가진 JTA를 사용해서 전역 트랜잭션을 관리한다(부분적으로 예외모델 때문에). 게다가 보통 JNDI에서 JTA UserTransaction을 얻어야 한다. 즉, JTA를 사용하기 위해 JNDI를 사용해야할 필요도 있다. JTA가 보통 어플리케이션 서버 환경에서만 사용할 수 있는 것처럼 전역 트랜잭션을 사용하면 분명히 어플리케이션 코드의 재사용 가능성을 제한한다.

이전에 전역 트랜잭션을 사용하는데 선호하는 방법은 EJB CMT (Container Managed Transaction)를 사용하는 것이었다. CMT는 선언적인 트랜잭션 관리 ( 프로그래밍적인 트랜잭션 관리와는 구분되는)의 형식이다. 물론 EJB를 사용하면 JNDI의 사용해야 함에도 불구하고 EJB CMT는 트랜잭션과 관련된 JNDI 검색의 필요성을 제거한다. 대부분을 제거하지만 트랜잭션을 관리하는 자바 코드를 전혀 작성하지 않아도 되는 것은 아니다. 또한, EJB에서 비즈니스 로직을 구현하기로 한 경우에만 이용할 수 있다.(최소한 트랜잭션이 적용된 EJB 퍼사드(facade) 뒤에서) 특히 선언적인 트랜잭션 관리에 대한 설득력있는 대안임에도 불구하고 보통 EJB의 안좋은 점은 꽤 많다.(매력적인 제안은 아니다.)


11.2.2 지역 트랜잭션
지역 트랜잭션은 JDBC 연결과 연관된 트랜잭션같은 리소스에 한정적이다. 지역 트랜잭션은 사용하기가 더 쉽지만 중요한 단점을 가지고 있다. 지역 트랜잭션은 트랜잭션이 적용된 여러 리소스에 걸쳐서 동작할 수 없다. 예를 들어 JDBC 연결을 사용하는 트랜잭션을 관리하는 코드는 전역 JTA 트랜잭션 내에서 실행될 수 없다. 어플리케이션 서버가 트랜잭션 관리에 포함되지 않기 때문에 여러 리소스에 걸쳐서 정확함을 보장할 수 없다. (대부분의 어플리케이션이 하나의 트랜잭션 리소스를 사용한다는 것은 전혀 가치가 없다.) 또다른 단점은 지역 트랜잭션이 프로그래밍 모델에 침투적이라는 것이다.


11.2.3 스프링 프레임워크의 일관성있는 프로그래밍 모델
스프링은 전역 트랜잭션과 지역 트랜잭셩의 이러한 단점들을 해결한다. 스프링은 어플리케이션 개발자가 어떤 환경에서도 일관적인 프로그래밍 모델을 사용하도록 해준다. 코드는 한번 작성하고 여러 환경에서 여러가지 트랜잭션 관리 전략의 이점을 얻을 수 있다. 스프링 프레임워크는 선언적인 트랜잭션 관리와 프로그래밍적인 트랜잭션 관리 둘다 제공한다. 대부분의 사용자들은 대부분에 경우에 권장하는 선언적인 트랜잭션 관리를 더 선호한다.

프로그래밍적인 트랜잭션 관리에서 개발자들은 어떤 의존하는 트랜잭션 인프라에서라도 실행되는 스프링 프레임워크 트랜잭션 추상화로 작업한다. 선호하는 선언적인 모델에서는 개발자들은 보통 트랜잭션 관리와 관련된 약간의 코드를 작성하거나 전혀 작성하지 않으므로 스프링 프레임워크의 트랜잭션 API나 다른 트랜잭션 API에 의존하지 않는다.

트랜잭션 관리를 위해서 어플리케이션 서버가 필요한가?

스프링 프레임워크의 트랜잭션 관리 지원은 엔터프라이즈 자바 어플리케이션이 어플리케이션 서버를 필요로 하는 경우와 같은 전통적인 규칙을 바꾸었다.

특히, EJB를 통한 선언적인 트랜잭션에 어플리케이션 서버가 필요없다. 사실 어플리케이션 서버가 강력한 JTA 기능을 가지고 있지만 스프링 프레임워크의 선언적인 트랜잭션이 EJB CMT보다 더 강력하고 더 생산적인 프로그래밍 모델을 제공하도록 할 수 있다.

일반적으로 많은 어플리케이션의 요구사항은 아니지만 어플리케이션이 여러 리소스에 걸쳐서 트랜잭션을 다루어야 할 때만 어플리케이션 서버의 JTA 기능이 필요하다. 많은 하이엔드 어플리케이션은 대신에 높은 확장성을 가진 하나의 데이터베이스를 사용한다.(오라클 RAC같은) Atomikos Transactions와 JOTM같은 단독 트랜잭션 매니저는 또다른 선택사항이다. 물론 Java Message Service (JMS)와 J2EE Connector Architecture (JCA)같은 다른 어플리케이션 서버 기능이 필요할 것이다.

스프링 프레임워크는 어플리케이션을 완전히 로딩된 어플리케이션 서버에 확장할 때 선택권을 준다. JDBC 연결에 지역 트랜잭션으로 코드를 작성하는 것이 EJB CMT나 JTA의 유일한 대안이어서 전역이면서 컨테이너가 관리하는 트랜잭션내에서 실행되는 코드가 필요할 때 대대적 변경을 해야하던 시절은 갔다. 스프링 프레임워크에서는 코드가 아니라 설정 파일의 빈 정의 중 일부만 변경하면 된다.


11.3 스프링 프레임워크의 트랜잭션 추상화 이해하기
스프링 트랜잭션 추상화의 핵심은 트랜잭션 전략의 개념이다. 트랜잭션 전략은 org.springframework.transaction.PlatformTransactionManager 인터페이스가 정의한다.

public interface PlatformTransactionManager {

  TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

  void commit(TransactionStatus status) throws TransactionException;

  void rollback(TransactionStatus status) throws TransactionException;
}

어플리케이션 코드에서 프로그래밍적으로 사용할 수 있더라도 이는 주로 서비스 프로바이더 인터페이스(service provider interface, SPI)다. PlatformTransactionManager가 인터페이스이기 때문에 필요하다면 쉽게 모킹하거나 스터빙(stub)할 수 있다. 이는 JNDI같은 검색 전략에 의존하지 않는다. PlatformTransactionManager 구현체는 스프링 프레임워크 IoC 컨테이너의 다른 객체(또는 빈)처럼 정의한다. 이러한 이점은 JTA로 작업할 때조차도 스프링 프레임워크 트랜잭션을 훌륭한 추상화로 만들어준다. 트랜잭션이 적용된 코드는 JTA를 직접 사용하는 것보다 훨씬 쉽게 테스트할 수 있다.

다시 스프링 철학과 일치하도록 PlatformTransactionManager 인터페이스의 어떤 메서드라도 던질 수 있는 TransactionException은 언체크드(unchecked)이다. (즉, 이는 java.lang.RuntimeException 클래스를 확장한다.) 트랜잭션 인프라스트럭처의 실패는 거의 예외없이 치명적이다. 어플리케이션 코드가 트랜잭션 실패를 실제로 복구할 수 있는 드문 경우에 어플리케이션 개발자는 여전히 TransactionException를 잡아서 다룰 수 있다. 개발자가 이렇게 하도록 강제하지 않는다는 것이 두드러진 점이다.

getTransaction(..) 메서드는 TransactionDefinition 파라미터에 따라 TransactionStatus 객체를 반환한다. 반환된 TransactionStatus는 새로운 트랜잭션을 나타내거나 현재 콜스택에 존재하는 트랜잭션 중 일치하는 것이 있다면 존재하는 트랜잭션을 나타날 수 있다. 후자의 경우는 Java EE 트랜잭션 컨텍스트처럼 TransactionStatus가 실행 스레드와 연결되었다는 것을 암시한다.

TransactionDefinition 인터페이스는 다음을 지정한다.

  • 격리(Isolation): 해당 트랜잭션이 다른 트랜잭션의 작업과 격리되는 정도. 예를 들어 해당 트랜잭션이 다른 트랜잭션에서 아직 커밋되지 않은 쓰기작업을 볼 수 있는가?
  • 전파(Propagation): 보통 트랜잭션 범위내에서 실행되는 모든 코드는 해당 크랜잭션에서 실행될 것이다. 하지만 트랜잭션 컨텍스트가 이미 존재하는 경우 트랜잭션이 적용된 메서드가 실행할 때의 동작을 지정하는 옵션이 있다. 예를 들어 코드를 존재하는 트랜잭션 (일반적인 경우)에서 계속 실행할 수 있다. 또는 존재하는 트랜잭션을 일시정지하고 새로운 트랜잭션을 생성할 수도 있다. 스프링은 EJB CMT에서 익숙한 모든 트랜잭션 전파 옵션을 제공한다. 스프링의 트랜잭션 전파의 의미를 읽어보려면 Section 11.5.7, “트랜잭션 전파”를 참고해라.
  • 시간만료(Timeout): 시간이 만료되기 전에 해당 트랜잭션이 얼마나 오랫동안 실행되고 의존 트랜잭션 인프라스트럭처가 자동으로 롤백하는 지를 나타낸다.
  • 읽기 전용 상태(Read-only status): 코드가 데이터를 읽기는 하지만 수정하지는 않는 경우 읽기 전용 트랜잭션을 사용할 수 있다. 읽기 전용 트랜잭션은 하이버네이트를 사용하는 경우처럼 몇가지 경우에 유용한 최적화가 될 수 있다.

이러한 설정은 표준 트랜잭션 개념을 반영한다. 필요하다면 트랜잭션 격리 수준과 다른 핵심 트랜잭션 개념에 대한 설명을 참고해라. 스프링 프레임워크나 다른 트랜잭션 관리 솔루션을 사용할 때 이러한 개념을 이해하는 것이 핵심이다.

TransactionStatus 인터페이스는 트랜잭션 실행을 제어하고 트랜잭션 상태를 조회하는 트랜잭션 코드에 대한 간단한 방법을 제공한다. 이 개념은 모든 트랜잭션 API에 일반적이므로 익숙해져야 한다.

public interface TransactionStatus extends SavepointManager {

  boolean isNewTransaction();

  boolean hasSavepoint();

  void setRollbackOnly();

  boolean isRollbackOnly();

  void flush();

  boolean isCompleted();
}

스프링에서 선언적인 트랜잭션 관리나 프로그래밍적인 트랜잭션 관리 중에 어느 것을 선택했는지에 관계없이 제대로된 PlatformTransactionManager 구현체를 정의하는 것이 정말로 가장 중요하다. 보통은 의존성 주입으로 이 구현체를 정의한다.

PlatformTransactionManager 구현체는 일반적으로 JDBC, JTA, Hibernate 등과 같은 동작하는 환경에 대한 지식을 필요로 한다. 다음의 예제는 어떻게 지역 PlatformTransactionManager 구현체를 정의할 수 있는지 보여준다.(이 예제는 평범한 JDBC에서 동작한다.)

JDBC DataSource를 정의한다.

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

관련된 PlatformTransactionManager 빈 정의는 DataSource 정의에 대한 참조를 가질 것이다. 이는 다음과 같을 것이다.

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>

Java EE 컨테이너에서 JTA를 사용하면 JNDI로 획득한 컨테이너 DataSource를 스프링의 JtaTransactionManager와 결합해서 사용한다. JTA와 JNDI 검색예제는 다음과 같을 것이다.

<?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:jee="http://www.springframework.org/schema/jee"
     xsi:schemaLocation="
     http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/jee 
     http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">

  <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/> 

  <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
  
  <!-- 다른 <bean/> 정의는 여기에 정의한다 -->
</beans>

JtaTransactionManager는 컨테이너의 전역 트랜잭션 관리 인프라스트럭처를 사용하기 때문에 DataSource나 다른 특정 리소스에 대해서 알 필요가 없다.

Note
위의 dataSource 빈의 정의는 jee 네임스페이스의 <jndi-lookup/> 태그를 사용한다. 스키마기반 설정에 대한 더 자세한 정보는 Appendix C, XML Schema-based configuration를 참고하고 <jee/> 태그에 대한 자세한 정보는 Section C.2.3, “The jee schema” 부분을 참고해라.

다음 예제에서 보듯이 하이버네이트 지역 트랜잭션도 쉽게 사용할 수 있다. 이 경우에 어플리케이션 코드가 하이버네이트 Session 인스턴스를 획득하기 위해서 사용할 하이버네이트 LocalSessionFactoryBean를 정의해야 한다.

DataSource 빈 정의는 앞에서 본 지역 JBDC 예제와 유사하므로 다음 예제에는 나와있지 않다.

Note
JTA가 아닌 트랜잭션 관리자가 사용하는 DataSource가 JNDI로 검색되고 Java EE 컨테이너가 관리된다면 Java EE 컨테이너가 아닌 스프링 프레임워크가 트랜잭션을 관리할 것이므로 DataSource는 트랜잭션이 적용되지 않을 것이다.

이 클래스의 txManager는 HibernateTransactionManager 타입이다. DataSourceTransactionManager가 DataSource에 대한 참조를 필요로 하는 것과 같은 방법으로 HibernateTransactionManager는 SessionFactory에 대한 참조를 필요로 한다.

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mappingResources">
  <list>
    <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
  </list>
  </property>
  <property name="hibernateProperties">
    <value>
      hibernate.dialect=${hibernate.dialect}
    </value>
  </property>
</bean>

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

하이버네이트와 Java EE 컨테이너가 관리하는 JTA 트랜잭션을 사용한다면 앞에서 제공한 JDBC에 대한 JTA 예제와 같은 JtaTransactionManager를 사용해야 한다.

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

Note
JTA를 사용한다면 트랜잭션 관리자 정의는 JDBC, Hibernate, JPA나 다른 지원 기술 등 어떤 데이터접근 기술을 사용한지와 상관없이 같을 것이다. 이는 JTA 트랜잭션이 트랜잭션이 적용된 어떤 리소스와도 동작하는 전역 트랜잭션이기 때문이다.

이러한 모든 경우에 어플리케이션 코드는 변경할 필요가 없다. 지역 트랜잭션을 전역 트랜잭션으로 바꾸거나 전역 트랜잭션을 지역 트랜잭션을 바꾸어야 하더라도 그냥 설정을 변경해서 트랜잭션을 어떻게 관리하는지만 변경할 수 있다.


11.4 리소스와 트랜잭션 동기화하기
이제 어떻게 다른 트랜잭션 관리자들을 생성하고 어떻게 이 관리자들이 트랜잭션과 동기화해야하는 관련 리소스에 연결되는 지(예를 들어 JDBC DataSource에 DataSourceTransactionManager이나 하이버네이트 SessionFactory에 HibernateTransactionManager 등등)가 명확해 질 것이다. 이번 섹션에서는 JDBC, 하이버네이트, JDO같은 퍼시스턴스 API를 직접 혹은 간접적으로 사용하는 어플리케이션 코드가 이러한 리소스들이 적절히 생성되고 재사용되고 정리된다는 것을 어떻게 보장하는지 설명한다. 그리고 이번 섹션에서는 관련 PlatformTransactionManager를 통해서 어떻게 트랜잭션 동기화가 실행되는지(선택적으로)를 설명한다.


11.4.1 고수준 동기화 접근
스프링의 최고수준 템플릿에 기반한 퍼시스턴스 통합 API를 사용하거나 네이티브 리소스 팩토리를 관리하기 위해 transaction-을 인지하는 팩토리빈이나 프록시를 가진 네이티브 ORM을 사용하는 접근을 선호한다. 트랜잭션 친화적인(transaction-aware) 솔루션들은 내부적으로 리소스 생성, 재사용, 정리, 선택적으로 리소스의 트랜잭션 동기화, 예외 맵핑을 다룬다. 그러므로 사용자 데이터에 접근하는 코드는 이러한 작업에는 포함되지 않지만 보일러플레이트가 아닌 퍼시스턴스 로직에만 순수하게 집중할 수 있다. 보통 네이티브 ORM API를 사용하거나 JdbcTemplate를 사용해서 JDBC 접근에 대한 템플릿 접근을 취한다. 이러한 솔루션들은 이 레퍼런스 문서의 다음 장에서 자세히 설명한다.


11.4.2 저수준 동기화 접근
DataSourceUtils (JDBC를 위한), EntityManagerFactoryUtils (JPA를 위한), SessionFactoryUtils (Hibernate를 위한), PersistenceManagerFactoryUtils (JDO를 위한) 등과 같은 클래스들은 저수준으로 존재한다. 어플리케이션 코드가 네이티브 퍼시스턴스 API의 리소스 타입을 직접 다루기 원한다면 스프링 프레임워크가 관리하는 적절한 인스턴스를 획득하고 트랜잭션이 (선택적으로) 동기화되고 프로세스에서 발생한 예외를 일관성있는 API에 적절히 맵핑하도록 이러한 클래스들을 사용한다.

예를 들어 JDBC의 경우 DataSource에서 getConnection() 메서드를 호출하는 전통적인 JDBC 접근대신에 다음과 같이 스프링의 org.springframework.jdbc.datasource.DataSourceUtils를 사용한다.

Connection conn = DataSourceUtils.getConnection(dataSource);

이미 존재하는 트랜잭션이 동기화된(연결된, linked) 연결을 가지고 있다면 해당 인스턴스를 반환한다. 그렇지 않으면 이 메서드 호출은 (선택적으로) 이미 존재하는 트랜잭션에 동기화된 새로운 연결을 생성하고 같은 트랜잭션에서 이어서 재사용할 수 있게 한다. 이미 언급했듯이 모든 SQLException는 스프링 프레임워크의 언체크드(unckecked) DataAccessExceptions 계층 중 하나인 CannotGetJdbcConnectionException로 감싸진다. 이 접근은 SQLException로 쉽게 얻을 수 있는 것보다 더 많은 정보를 주고 다른 퍼시스턴스 기술을 사용하더라도 데이터베이스간에 이식성(portability)을 보장한다.

이 접근은 스프링 트랜잭션 관리(트랜잭션 동기화는 선택적이다)이 없이도 동작하기 때문에 트랜잭션 관리에 스프링을 사용하는지와 상관없이 사용할 수 있다.

물론 일단 스프링의 JDBC 지원이나 JPA 지원, 하이버네이트 지원을 사용했다면 DataSourceUtils나 다른 헬러 클래스는 보통 사용하려고 하지 않을 것이다. 왜냐하면 관련된 API를 직접 사용하는 것보다 스프링 추상화를 통해서 작업하는 것이 훨씬 좋을 것이기 때문이다. 예를 들어 JDBC의 사용을 간소화하려고 스프링의 JdbcTemplate나 jdbc.object 패키지를 사용한다면 올바른 연결 검색은 보이지 않게 이뤄지고 특별한 코드를 작성할 필요가 없다.


11.4.3 TransactionAwareDataSourceProxy
가장 최저계층에 TransactionAwareDataSourceProxy 클래스가 존재한다. 이 클래스는 스프링이 관리하는 트랜잭션을 인지하도록 대상 DataSource를 감싸는 프록시이다. 이 관점에서는 Java EE 서버가 제공하는 것처럼 트랜잭션이 적용된 JNDI DataSource와 유사하다.

기존의 코드가 호출되어서 표준 JDBC DataSource 인터페이스 구현체를 전달하는 경우를 제외하고는 이 클래스를 사용하는 것이 바람직하지 않거나 필수가 아니어야 한다. 이러한 경우에 이 코드는 사용가능하지만 스프링이 관리하는 트랜잭션에 포함된다. 위에서 언급한 더 높은 수준의 추상화를 사용해서 새로운 코드를 작성하는 것을 더 추천한다.


11.5 선언적인 트랜잭션 관리
Note
대부분의 스프링 프레임워크의 사용자들은 선언적인 트랜잭션 관리를 선택한다. 선언적인 트랜잭션 관리는 어플리케이션에 최소한으로 영향을 주므로 비침투적인 경량 컨테이터의 이상과 매우 일치한다.

트랜잭션이 적용된 관점 코드가 스프링 프레임워크 배포판에 포함되어 있고 보일러플레이트 방식으로 사용되는 것과 마찬가지로 보통 이 코드를 효율적으로 사용하는데 AOP의 개념을 이해해야 할 필요는 없지만 스프링 프레임워크의 선언적인 트랜잭션 관리는 스프링의 관점지향 프로그래밍(AOP)와 사용가능하도록 만들어졌다.

스프링 프레임워크의 선언적인 트랜잭션 관리는 개별 메서드 수준에 트랜잭션 동작(또는 부족(lack))을 지정할 수 있는 EJB CMT와 비슷하다. 필요하다면 트랜잭션 컨텍스트내에서 setRollbackOnly()를 호출할 수 있다. 두 가지 트랜잭션 관리 종류사이의 차이점은 다음과 같다.

  • JTA와 묶인 EJB CMT와는 다르게 스프링 프레임워크의 선언적인 트랜잭션 관리는 어떤 환경에서도 동작한다. 선언적인 트랜잭션 관리는 단순히 설정파일을 조정함으로써 JTA 트랜잭션이나 JDBC, JPA, 하이버네이트, JDO를 사용하는 지역 트랜잭션과 동작할 수 있다.
  • 스프링 프레임워크의 선언적인 트랜잭션 관리를 EJB같은 전용 클래스만이 아니라 어떤 클래스에도 적용할 수 있다.
  • 스프링 프레임워크는 EJB에는 없는 선언적인 롤백 규칙 기능을 제공한다. 프로그래밍적인 지원과 선언적인 지원 모두 롤백 규칙을 제공한다.
  • 스프링 프레임워크는 AOP를 사용해서 트랜잭션 동작을 커스터마이징 할 수 있게 해준다. 예를 들어 트랜잭션을 롤백하는 경우 커스턴 동작을 추가할 수 있다. 트랜잭션이 적용된 어드바이스 사이에 임의의 어드바이스를 추가할 수도 있다. EJB CMT에서는 setRollbackOnly()를 제외하고는 컨테이너의 트랜잭션 관리에 영향을 줄 수 없다.
  • 스프링 프레임워크 하이엔드 어플리케이션 서버가 지원하는 원격호출에 걸친 트랜잰션 컨텍스트의 전파를 지원하지 않는다. 이 기능이 필요하다면 EJB를 사용하기를 추천한다. 하지만 보통은 원격 호출에 걸친 트랜잭션을 원하지 않으므로 이러한 기능을 사용하기 전에 신중하게 고려해봐야 한다.
롤백 규칙의 개념은 중요한다. 롤백 규칙은 예외(그리고 throwable) 이 자동으로 롤백을 수행해야한다고 지정할 수 있게 한다. 롤백 규칙은 자바 코드가 아니라 설정파일에 선언적으로 지정한다. 그러므로 현재 트랜잭션을 롤백하기 위해 TransactionStatus 객체에서 setRollbackOnly()을 호출할 수 있다고 하더라도 대부분 MyApplicationException이 항상 롤백의 결과가 된다는 규칙을 지정할 수 있다. 이 옵션의 중요한 이점은 해당 비즈니스 객체가 트랜잭션 인프라스트럭처에 의존하지 않는다는 것이다. 예를 들어 보통 비즈니스 객체들은 스프링 트랜잭션 API나 다른 스프링 API를 임포트할 필요가 없다.

EJB 컨테이너의 기본 동작은 시스템 예외 (보통 런타임 예외)에 자동으로 트랜잭션을 롤백하는 것이더라도 EJB CMT는 어플리케이션 예되에는 자동으로 트랜잭션을 롤백하지 않는다.(즉, java.rmi.RemoteException를 제외한 체크드 익셉션) 선언적인 트랜잭션 관리의 스프링 기본동작이 EJB 관례(언체크드 익센션에만 자동으로 롤백한다.)를 따르지만 스프링의 기본동작을 커스터마이징 하는 것은 종종 유용하다.

TransactionProxyFactoryBean는 어디에 있는가?

스프링 2.0이상의 버전에서 선언적인 트랜잭션 설정은 그 이전 버전과는 상당히 다르다. 더이상 TransactionProxyFactoryBean 빈을 설정할 필요가 없다는 것이 가장 큰 차이점이다.

스프링 2.0 이전의 설정방식은 여전히 100% 유효하다. TransactionProxyFactoryBean 빈을 정의하는 것처럼 새로운 <tx:tags/>를 생각해 봐라.


11.5.1 스프링 프레임워크의 선언적인 트랜잭션 구현체 이해하기
클래스에 @Transactional 어노테이션을 붙히는 것만으로는 충분하지 않고 설정에 트랜잭션 관련설정 (<tx:annotation-driven/>)을 추가하고 어떻게 동작하는지 이해해야 한다. 이번 섹션에서는 트랜잭션과 관련된 이벤트에서 스프링 프레임워크의 선언적인 트랜잭션 인프라스트럭처의 내부 동작을 설명한다.

스프링 프레임워크의 선언적인 트랜잭션 지원을 이해하는 가장 중요한 개념은 트랜잭션이 AOP 프록시를 통해서 활성화되고 메타데이터(현재는 XML기반이거나 어노테이션 기반)로 트랜잭션이 적용된 어드바이스가 유도(driven)된다는 것이다. 트랜잭션이 적용된 메타데이터와 AOP의 조합은 메서드 호출 주위에 트랜잭션을 유도하기 위해 적절한 PlatformTransactionManager 구현체와 결합한 TransactionInterceptor를 사용하는 AOP 프록시를 만든다.

Note
스프링 AOP는 Chapter 8, 스프링의 관점 지향 프로그래밍에서 다루었다.

개념적으로 트랜잭션이 적용된 프록시에서 메서드를 호출하는 것은 다음과 같을 것이다...

사용자 삽입 이미지


11.5.2 선언적인 트랜잭션 구현체 예제
다음의 인터페이스와 그 구현체를 보자. 이 예제는 플레이스홀더로 Foo와 Bar 클래스를 사용해서 특정 도메인 모델에 집중하지 않고 트랜잭션 사용에 집중할 수 있다. 이 예제의 목적을 위해 DefaultFooService 클래스는 구현된 각각의 메서드 바디에서 UnsupportedOperationException 인스턴스를 던진다는 사실은 좋다. 이는 생성된 트랜잭션을 보고 UnsupportedOperationException 인스턴스에 대한 응답으로 롤백할 수 있게 한다.

// 트랜잭션이 적용되기를 원하는 서비스 인터페이스
package x.y.service;

public interface FooService {

  Foo getFoo(String fooName);

  Foo getFoo(String fooName, String barName);

  void insertFoo(Foo foo);

  void updateFoo(Foo foo);
}


// 위 인터페이스의 구현체
package x.y.service;

public class DefaultFooService implements FooService {

  public Foo getFoo(String fooName) {
    throw new UnsupportedOperationException();
  }

  public Foo getFoo(String fooName, String barName) {
    throw new UnsupportedOperationException();
  }

  public void insertFoo(Foo foo) {
    throw new UnsupportedOperationException();
  }

  public void updateFoo(Foo foo) {
    throw new UnsupportedOperationException();
  }
}

FooService 인터페이스의 처음 두 메서드 getFoo(String)와 getFoo(String, String)가 읽기 적용 트랜잭션의 컨텍스트로 실행되어야 하고 다른 메서드들인 insertFoo(Foo)와 updateFoo(Foo)는 읽기-쓰기 트랜잭션 컨텍스트로 실행되어야 한다고 가정해 보자. 다름 설정은 이어진 두 문단에서 자세히 설명한다.

<!-- 'context.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="fooService" class="x.y.service.DefaultFooService"/>

  <!-- 트랜잭션이 적용된 어드바이스 (무슨 일이 발생하는 지는 아래의 <aop:advisor/> 빈을 봐라.) -->
  <tx:advice id="txAdvice" transaction-manager="txManager">
  <!-- 트랜잭셔널 의미(the transactional semantics)... -->
  <tx:attributes>
    <!-- 'get'으로 시작하는 모든 메서드들은 읽기전용이다 -->
    <tx:method name="get*" read-only="true"/>
    <!-- 다름 메서드들은 기본 트랜잭션 설정을 사용한다. (아래를 참고) -->
    <tx:method name="*"/>
  </tx:attributes>
  </tx:advice>
  
  <!-- 위의 FooService 인터페이스가 정의한 연산의 
    모든 실행에 트랜잭션이 적용된 어드바이스가 실행된다는 것을 보장한다. -->
  <aop:config>
  <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
  <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
  </aop:config>
  
  <!-- DataSource를 잊지 마라 -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
  <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
  <property name="username" value="scott"/>
  <property name="password" value="tiger"/>
  </bean>

  <!-- 유사하게 PlatformTransactionManager도 잊지 마라 -->
  <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
  </bean>
  
  <!-- 다른 <bean/> 정의는 여기에 정의한다 -->
</beans>

앞의 설정을 검사해 봐라. 서비스 객체인 fooService에 트랜잭션을 적용하길 원한다. 적용할 트랜잭션 의미는 <tx:advice/> 정의에 감추어져 있다. <tx:advice/> 정의는 “... 'get'로 시작하는 모든 메서드들은 읽기 전용 트랜잭션 컨텍스트로 설행하고 그외 모든 메서드들은 기본 트랜잭션 으로 실행한다”고 읽는다. <tx:advice/> 태그의 transaction-manager 속성은 트랜잭션을 유도할 PlatformTransactionManager 빈의 이름으로 설정한다. (이 경우에는 txManager 빈이다.)

Tip
연결하려는 PlatformTransactionManager 빈의 이름이 transactionManager라는 이름이라면 트랜잭션이 적용된 어드바이스(<tx:advice/>)에서 transaction-manager 속성을 생략할 수 있다. 연결하려는 PlatformTransactionManager 빈이 다른 이름이라면 앞의 예제처럼 명시적으로 transaction-manager 속성을 사용해야 한다.

<aop:config/> 정의는 txAdvice 빈이 정의한 트랜잭션이 적용된 어브바이스가 프로그램에서 적절한 지점에서 실행된다는 것을 보장한다. 먼저 FooService 인터페이스 (fooServiceOperation)에서 정의한 연산(operation)의 실행과 일치하는 포인트컷을 정의한다. 그 다음 포인트컷을 어드바이저를 사용하는 txAdvice와 연결한다. 그 결과 fooServiceOperation 실행시에 txAdvice가 정의한 어드바이스가 실행될 것이다.

<aop:pointcut/> 요소에서 정의된 표현식은 AspectJ 포인트컷 표현식이다. 스프링 2.0의 포인트컷 표현식에 대한 자세한 내용은 Chapter 8, 스프링의 관점 지향 프로그래밍를 참고해라.

공통적인 요구사항은 전체 서비스계층이 트랜잭션이 가능하게 만든다. 이렇게 하는 가장 좋은 방법은 포인트컷 표현식이 서비스계층의 모든 연산과 일치하도록 변경하는 것이다. 예를 들면 다음과 같다.

<aop:config>
  <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
  <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>

Note
이 예제에서는 모든 서비스 인터페이스가 x.y.service 패키지에 정의되어 있다고 가정한다. 더 자세한 내용은 Chapter 8, 스프링의 관점 지향 프로그래밍를 참고해라.

이제 설정을 살펴보았는데 아마 스스로 다음과 같은 질문을 할 것이다. “좋아.. 근데 이 모든 설정은 실제로 무엇을 하지?”

위의 설정은 fooService 빈 정의에서 생성한 객체주위에 트랜잭션이 적용된 프록시를 생성하는데 사용할 것이다. 프록시는 트랜잭션이 적용된 어드바이스로 설정될 것이므로 프록시에서 적절한 메서드가 호출되었을 때 해당 메서드와 연관된 트랜잭션 설정에 따라 트랜잭션을 시작하거나 일시중지하거나 읽기전용으로 표시하는 등의 작업을 한다. 위의 설정을 테스트하는 다음의 프로그램을 살펴보자.

public final class Boot {

  public static void main(final String[] args) throws Exception {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);
    FooService fooService = (FooService) ctx.getBean("fooService");
    fooService.insertFoo (new Foo());
  }
}

앞의 프로그램을 실행한 출력은 다음과 같을 것이다. (Log4J 출력과 DefaultFooService 클래스의 insertFoo(..) 메서드가 던진 UnsupportedOperationException의 스택 트레이스는 명확함을 위해 생략했다.)

  <!-- 스프링 컨테이너가 시작된다... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy
    for bean 'fooService' with 0 common interceptors and 1 specific interceptors
  <!-- DefaultFooService는 실제로 프록싱된다 -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

  <!-- ... insertFoo(..) 메서드는 이제 프록시에서 호출된다 -->

[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo
  <!-- 여기서 트랜잭션이 적용된 어드바이스를 만든다... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection
    [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

  <!-- DefaultFooService의 insertFoo(..) 메서드는 예외를 던진다... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should
    rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo
    due to throwable [java.lang.UnsupportedOperationException]

   <!-- 그리고 트랜잭션은 롤백된다.(기본값으로 RuntimeException 인스턴스를 롤백을 발생시킨다) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection
    [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource

Exception in thread "main" java.lang.UnsupportedOperationException
    at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
   <!-- AOP 인프라스트럭처 스택 트레이스 요소는 명확함을 위해 제거했다 -->
    at $Proxy0.insertFoo(Unknown Source)
    at Boot.main(Boot.java:11)


11.5.3 선언적인 트랜잭션 롤백
이전 섹션에서 어플리케이션에서 선언적으로 클래스(보통은 서비스계층의 클래스)에 트랜잭션 설정을 어떻게 지정하는가에 대한 기본적인 내용을 살펴보았다. 이 섹션은 간단한 선언적인 방법으로 트랜잭션의 롤백 제어를 어떻게 할 수 있는가를 설명한다.

스프링 프레임워크의 트랜잭션 인프라스트럭처가 트랜잭션 작업을 롤백하도록 추천하는 방법은 트랜잭션 컨텍스트에서 현재 실행되고 있는 코드가 Exception를 던지도록 하는 것이다. 스프링 프레임워크의 트랜잭션 인프라스트럭처 코드는 버블링되는 호출스택처럼 다루지 않는 모든 Exception를 잡아서 트랜잭션을 롤백으로 표시할 것인지를 결정한다.

기본설정에서 스프링 프레임워크의 트랜잭션 인프라스트럭처는 런타입 익셉션과 언체크드 익셉션만 트랜잭션을 롤백으로 표시한다. 즉, RuntimeException의 인스턴스나 하위클래스의 예외를 던졌을 때 뿐이다.(기본적으로 Error도 롤백될 것이다.) 트랜잭션이 적용된 메서드의 체크드 익셉션은 기본설정에서는 롤백하지 않는다.

체크드 익셉션을 포함해서 어떤 타입의 Exception이 트랜잭션을 롤백으로 표시하도록 할 것인지 설정할 수 있다. 다음의 XML 코드는 체크드이면서 어플리케이션에 특화된 Exception 타입을 롤백으로 어떻게 설정하는지 보여준다.

<tx:advice id="txAdvice" transaction-manager="txManager">
  <tx:attributes>
  <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
  <tx:method name="*"/>
  </tx:attributes>
</tx:advice>

예외가 던져졌을 때 트랜잭션을 롤백하기를 원치 않을 때 '롤백 규칙'을 지정하지 않을 수도 있다. 다음의 예제는 스프링 프레임워크의 트랜잭션 인프라스트럭처가 다루지 않은 InstrumentNotFoundException를 만났을 때 조차도 참여한 트랜잭션을 커밋하게 한다.

<tx:advice id="txAdvice">
  <tx:attributes>
  <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
  <tx:method name="*"/>
  </tx:attributes>
</tx:advice>

스프링 프레임워크의 트랜잭션 인프라스트럭처가 예외를 잡고 트랜잭션을 롤백으로 표시할지를 결정하려고 설정된 롤백규칙을 참고했을 때 가장 강하게(strongest) 일치한 규칙을 사용한다. 그래서 다음 설정의 경우 InstrumentNotFoundException를 제외한 모든 예외는 참가한 트랜잭션을 롤백한다.

<tx:advice id="txAdvice">
  <tx:attributes>
  <tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
  </tx:attributes>
</tx:advice>

필요한 롤백을 프로그래밍적으로 지정할 수도 있다. 아주 간단하더라도 이 방법은 상당히 침투적이고 코드가 스프링 프레임워크의 트랜잭션 인프라스트럭처에 강하게 커플링된다.

public void resolvePosition() {
  try {
    // 몇몇 비즈니스 로직...
  } catch (NoProductInStockException ex) {
    // 프로그래밍적으로 롤백을 일으킨다
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  }
}

가능한 모든 경우에 롤백에 대해서 선언적인 접근을 하는 것을 강력히 권장한다. 프로그래밍적인 롤백은 절대적으로 필요한 경우에만 사용해야 하지만 프로그래밍적인 롤백을 사용하면 깔끔한 POJO 기반의 아키텍처를 버리게 된다.
2012/11/28 03:15 2012/11/28 03:15