Chapter 7. 트랜잭션 관리

7.1. Spring 트랜잭션 추상화

Spring은 트랜잭션 관리를 위한 일관된 추상화를 제공한다. 이 추상화는 Spring의 추상화들 중 가장 중요한 것 중 하나이며 다음과 같은 장점들을 가져다준다.

  • JTA, JDBC, Hibernate, iBATIS 데이터베이스 계층과 JDO와 같은 서로 다른 트랜잭션 API를 포괄하는 일관된 프로그래밍 모델을 제공한다.

  • 이러한 대부분의 트랜잭션 API들이 제공해주는 것보다 보다 간단하고 사용하기 쉬운 프로그래밍적인 트랜잭션 관리 API를 제공한다.

  • Spring의 데이터 접근 추상화와 통합된다.

  • Spring의 선언적 트랜잭션 관리를 지원한다.

전통적으로, J2EE 개발자들은 트랜잭션 관리에 있어 두 가지 선택사항들을 가지는데 글로벌 혹은 로컬 트랜잭션을 사용하는 것이다. 글로벌 트랜잭션은 JTA를 사용하여 어플리케이션 서버에 의해 관리된다. 로컬 트랜잭션은, 예를 들어 JDBC 커넥션과 연관된 트랜잭션처럼, 리소스 특성을 따른다. 이 선택은 심오한 의미를 가진다. 글로벌 트랜잭션은 다중 트랜잭션 리소스들을 가지고 동작할 수 있게 해준다. (이것은 대부분의 어플리케이션들이 하나의 트랜잭션 리소스를 사용하기 때문에 그다지 유용하지 않다.) 로컬 트랜잭션을 사용한다면, 어플리케이션 서버는 트랜잭션 관리에 관여하지 않으며, 다중 리소스에 걸쳐 정확함을 보증해주지 않는다.

글로벌 트랜잭션은 중요한 약점을 가진다. 사용하기에 번거로운(부분적으로 예외 모델의 탓인) API인 JTA를 사용해야만 한다는 것이다. 더군다나, JTA UserTransaction은 일반적으로 JNDI를 통해 얻어야만 한다. 이것은 우리가 JTA를 사용하기 위해서는 JNDI와 JTA 모두 사용해야만 한다는 것을 의미한다. 명백하게 글로벌 트랜잭션을 사용하는 것은 JTA가 일반적으로 오로지 어플리케이션 서버 환경에서만 가능하기 때문에, 어플리케이션 코드의 재사용성을 제약할 것이다.

글로벌 트랜잭션을 사용할 때 선호되는 방법은, 선언적 트랜잭션 관리 형태 (왜냐하면 이것은 프로그래밍적인 트랜잭션 관리와는 구별되기 때문이다.)인 EJBCMT(Container Managed Transaction)를 경유하는 방법이다. EJB CMT는 비록 EJB 자신이 JNDI의 사용을 필요로 함에도 불구하고, 트랜잭션과 연관된 JNDI 룩업의 필요성을 제거해준다. 이것은 또한 트랜잭션을 컨트롤하기 위한 자바 코드를 작성할 필요성 역시 대부분--전부는 아니지만-- 없애준다. 중요한 약점은 CMT는 (명백히) JTA와 어플리케이션 서버에 묶여 있다는 것이다; 그리고 이것은 우리가 비지니스 로직을 EJB 혹은 최소한 트랜잭션적인 EJB 퍼싸드의 뒤에서 구현하는 경우에만 가능하다. EJB를 둘러싼 부정적인 측면은 일반적으로 매우 크기 때문에 선언적 트랜잭션 관리에 대한 대안이 있을 때에는 그다지 매력적인 제안은 되지 못한다.

로컬 트랜잭션은 훨씬 더 사용하기 쉽지만, 중요한 단점 역시 가지고 있다. 이것은 다중 트랜잭션 리소스에 걸쳐서 사용할 수 없으며 프로그래밍 모델을 침범하는 경향이 있다. 예를 들어, JDBC 커넥션을 사용하여 트랜잭션을 관리하는 코드는 글로벌 JTA 트랜잭션 내에서는 동작하지 못한다.

Spring은 이러한 문제점들을 해결해준다. Spring은 어플리케이션 개발자들로 하여금 어떠한 환경에서라도 일관적인 프로그래밍 모델을 사용할 수 있게 해준다. 당신이 코드를 한 번 작성하면 그것은 다른 환경에서의 다른 트랜잭션 관리 전략에서도 (잘 작동함으로써) 이익을 가져다 줄 것이다. Spring은 선언적/프로그래밍적 트랜잭션 관리방법 모두를 지원한다. 선언적 트랜잭션 관리는 대부분의 사용자들에게 선호되며 대부분의 경우 추천되는 방법이다.

프로그래밍적인 트랜잭션 관리를 하는 개발자들은 어떠한 기반 트랜잭션 하부구조와도 동작할 수 있는 Spring 트랜잭션 추상화로 개발한다. 선호되는 선언적 모델 개발자들은 전형적으로 트랜잭션 관리와 관련된 코딩을 매우 적거나 혹은 아예 하지 않는다. 그리고 Spring 혹은 어떤 다른 트랜잭션 API에 의존하지 않는다.

7.2. 트랜잭션 전략

Spring 트랜잭션 추상화의 핵심은 transaction strategy에 대한 개념이다.

아래의 코드는 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;
}

비록 이것이 프로그래밍적으로 사용될 수 있다고는 하지만, 근본적으로는 SPI 인터페이스이다. (역자주 : SPI 인터페이스는 하드웨어쪽 용어로 '두 개의 주변장치간에 직렬 통신으로 데이터를 교환할 수 있게 해주는 직렬 인터페이스(serial peripheral interface)'라는 의미인데, DB와 어플리케이션의 트랜잭션을 연결해주는 인터페이스라는 의미로 사용된 것으로 생각된다.) Spring의 철학처럼 이것은 인터페이스라는 데 주목하라. 따라서, 이것은 필요하다면 쉽게 mock되거나 stub될 수 있다.. 더군다나 이것은 JNDI처럼 룩업 전략에 묶이지도 않는다 : PlatformTransactionManager의 구현은 Spring IoC 컨테이너에서의 다른 객체들처럼 동일하게 정의된다. 이러한 장점은 심지어 JTA로 작업조차도 구현할 보람이 있는 추상화로 만들어 준다는 것이다 : 트랜잭션 코드는 직접 JTA를 사용하는 것보다 훨씬 더 쉽게 테스트될 수 있다.

Spring의 철학에서처럼, TransactionException unchecked 예외이다. 트랜잭션 하부구조의 실패는 대부분 늘 치명적이다. 어플리케이션 코드가 그것들로부터 복구될 수 있는 아주 드문 경우에는, 어플리케이션 개발자는 여전히 TransactionException을 캐치하고 핸들링하는 것을 선택할 수 있다.

getTransaction() 메써드는 TransactionDefinition 파라미터에 따라 TransactionStatus 객체를 반환한다. 반환된 TransactionStatus는 아마도 새롭게 생성되거나 (만약 현재의 call스택에 동일한 트랜잭션이 있다면) 존재하고 있는 트랜잭션을 의미할 것이다.

J2EE 트랜잭션 컨텍스트처럼 TransactionStatus는 실행 쓰레드와 연관되어 있다.

TransactionDefinition 인터페이스는 다음과 같이 명기하고 있다 :

  • 트랜잭션 고립성: 이 트랜잭션의 고립성의 등급은 다른 트랜잭션들의 작업으로부터 가진다. 예를 들어, 이 트랜잭션이 다른 트랜잭션들로부터 커밋되지 않은 쓰기작업 내용을 볼 수 있는가?

  • 트랜잭션 전달: 일반적으로 트랜잭션 영역 내에서 실행되는 모든 코드는 그 트랜잭션 내에서 실행될 것이다. 그러나, 만약 트랜잭션 컨텍스트가 이미 존재하는 상황에서 트랜잭션적인 메써드가 실행된다면 그 동작을 지정하는 몇가지 옵션들이 있는데, 예를 들어, (대부분의 경우) 현존하는 트랜잭션 내에서 단순히 실행되기 혹은 현존 트랜잭션을 중지하고 새로운 트랜잭션 생성하기 등이 그것이다. Spring은 EJB CMT로부터 익숙한 트랜잭션 전달 옵션을 제공한다.

  • 트랜잭션 타임아웃: 이 트랜잭션이 타임아웃(자동적으로 기반 트랜잭션 하부구조에 의해 롤백되는)되기까지의 시간

  • read-only 상태: read-only 트랜잭션은 어떠한 데이터도 수정하지 않는다. read-only 트랜잭션은 (Hibernate를 사용할 때와 같이) 몇몇 경우에서 유용한 최적화 방식이 될 수 있다.

이러한 세팅들은 기본적인 개념을 반영한다. 만약 필요하다면, 트랜잭션 고립성과 다른 핵심적인 트랜잭션 개념들에 대한 논의자료들을 참조하길 바란다. 그런 핵심 개념들을 이해하는 것은 Spring 혹은 다른 트랜잭션 관리 솔루션을 사용함에 있어서 필수적인 것이다.

TransactionStatus 인터페이스는 트랜잭션 실행과 쿼리 트랜잭션 상태를 제어하기 위한 트랜잭션 코드를 작성하기 위한 간단한 방법을 제공해준다. 모든 트랜잭션 API에 공통적인 것이기 때문에 기본적인 개념은 매우 친숙하게 느껴질 것이다.

public interface TransactionStatus {

    boolean isNewTransaction();

    void setRollbackOnly();

    boolean isRollbackOnly();
}

아무리 Spring 트랜잭션 관리를 사용한다고 해도 PlatformTransactionManager 인터페이스를 구현하는 것은 필수적이다. 훌륭한 Spring 형태에서는, 중요한 정의는 IoC를 사용하여 만들어진다.

PlatformTransactionManager의 구현은 일반적으로 작업환경이 JDBC인지, JTA인지, Hibernate인지 등에 대한 지식을 필요로 한다.

Spring jPetStore 샘플 어플리케이션의 dataAccessContext-local.xml에서 추출한 다음의 예제는 로컬 PlatformTransactionManager 구현이 정의되는 방법을 보여준다. 이것은 JDBC 환경에서 작동하는 것이다.

우리는 JDBC 데이터소스를 정의해야만 한다. 그리고나서 DataSource에 대한 참조를 넘겨줌으로써 Spring의 DataSourceTransactionManager를 사용할 것이다.

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

PlatformTransactionManager 정의는 다음과 같을 것이다 :

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

동일한 샘플 어플리케이션에 있는 dataAccessContext-jta.xml 파일에서처럼, 만약 우리가 JTA를 사용한다면 JNDI를 경유해 얻어진 우리는 컨테이너 DataSource를 사용할 필요가 있으며, JtaTransactionManager를 구현해야 한다. JtaTransactionManager는 컨테이너의 글로벌 트랜잭션 관리를 사용할 것이기 때문에, DataSource 혹은 어떤 다른 리소스들에 대해 에 대해 알 필요가 없다.

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName"><value>jdbc/jpetstore</value></property>
</bean>

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

우리는 Spring PetClinic 샘플 어플리케이션에서 가져온 다음의 예제에서처럼 Hibernate 로컬 트랜잭션을 쉽게 사용할 수 있다.

이 경우, 우리는 어플리케이션 코드가 Hibernate Session들을 가져오기 위해 사용할 Hibernate LocalSessionFactory를 정의할 필요가 있다.

DataSource 빈 정의는 위의 예제들과 비슷하지만 보여지지 않는다. (만약 이것이 컨테이너 DataSource라면, 이것은 Spring처럼 컨테이너보다 트랜잭션적이지 않을 것이다.)

이 경우 "transactionManager" 빈은 HibernateTransactionManager 클래스이다. DataSource에 대한 참조를 필요로 한 DataSourceTransactionManager에서와 비슷하게, HibernateTransactionManager는 세션 팩토리에 대한 참조를 필요로 한다.

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

<bean id="transactionManager" 
    class="org.springframework.orm.hibernate.HibernateTransactionManager">
    <property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>

Hibernate와 JTA 트랜잭션을 같이 사용하려면 우리는 JDBC 혹은 어떤 다른 리소스 전략들처럼 JtaTransactionManager를 그냥 사용하면 된다.

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

이것은 어떤 트랜잭션 리소스에 참여하는 글로벌 트랜잭션처럼, 어떤 리소스들에 대한 JTA 설정과 동일하다는 점을 명심하라.

이런 모든 경우에서, 어플리케이션 코드는 아무것도 변경될 필요가 없을 것이다. 우리는 로컬에서 글로벌 트랜잭션으로 혹은 그 반대의 경우 역시 단지 설정을 바꾸는 것만으로 트랜잭션이 관리되는 방법을 변경할 수 있다.

글로벌 트랜잭션을 사용하지 않을 때에는 하나의 특별한 코딩 규칙을 따를 필요가 있다. 운좋게도 이것은 매우 간단하다. 커넥션 사용을 끌어오고 필요에 따라 트랜잭션 관리를 적용하기 위해 적절한 PlatformTransactionManager 구현을 허용하는 특별한 방법으로 커넥션 혹은 세션 리소스를 획득할 필요가 있다.

예를 들어, JDBC를 사용할 경우, 당신은 DataSource의 getConnection() 메써드를 호출해서는 안되고, 다음의 예에서 보여지는 것처럼 Spring의 org.springframework.jdbc.datasource.DataSourceUtils 클래스를 사용해야만 한다.

Connection conn = DataSourceUtils.getConnection(dataSource);

이것은 어떤 SQLException도 Spring의 (Spring의 체크되지 않은 DataAccessExceptions 하위 클래스의 하나인) CannotGetJdbcConnectionException으로 싸여진다는 부가적인 이점을 가진다. 이러한 점은 당신이 SQLException으로부터 얻을 수 있는 것보다 더 많은 정보를 줄 것이며, 다른 데이터베이스들, 심지어 다른 영속전략들에 걸쳐 이식성을 보장해 줄 것이다.

이것은 Spring 트랜잭션 관리가 없이도 잘 동작할 것인데, 그래서 Spring을 쓰건 쓰지 않건 이 방법을 사용할 수 있다.

물론, 한 번 Spring의 JDBC 지원 혹은 Hibernate 지원을 사용한다면, 당신은 DataSourceUtils 혹은 다른 헬퍼 클래스들을 사용하고 싶어하지 않을 것이다. 왜냐하면 관련된 API를 가지고 직접 작업하는 것보다 Spring 추상화를 통해 작업하는 것이 훨씬 행복할 것이기 때문이다. 예를 들어, 만약 JDBC 사용을 간단하게 하기 위해 Spring의 JdbcTemplate 혹은 jdbc.object 패키지를 사용한다면, 정확한 커넥션 복구가 보이지 않는 곳에서 이루어질 것이고, 당신은 어떤 특별한 코드를 추가할 필요가 없을 것이다.

7.3. 프로그래밍적인 트랜잭션 관리

Spring 프로그래밍적인 트랜잭션 관리에 있어 두 가지 방법을 제시한다.

  • TransactionTemplate의 사용

  • 직접 PlatformTransactionManager 구현

우리는 일반적으로 전자의 접근방법을 추천한다.

두 번째 접근방법은 (비록 예외처리는 덜 성가시지만) JTA UserTransaction API의 사용과 비슷하다.

7.3.1. TransactionTemplate 사용하기

TransactionTemplateJdbcTemplateHibernateTemplate와 같은 다른 Spring templates와 동일한 접근 방식을 적용하고 있다. 이것은 콜백(callback) 접근방법을 사용하는데, 리소스 획득과 해제작업으로부터 어플리케이션 코드를 해방시켜준다.(더이상 try/catch/finally를 할 필요가 없다.) 다른 templates처럼, TransactionTemplate는 쓰레드 안전하다.

트랜잭션 컨텍스트 내에서 실행되어야 하는 어플리케이션 코드는 다음과 같을 것이다. TransactionCallback이 값을 반환하기 위해 사용되는 부분에 주목하라.

Object result = tt.execute(new TransactionCallback() {
    public Object doInTransaction(TransactionStatus status) {
        updateOperation1();
        return resultOfUpdateOperation2();
    }
});

만약 반환될 값이 없다면, 다음과 같이 TransactionCallbackWithoutResult를 사용하라.

tt.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});

콜백 내의 코드는 TransactionStatus 객체의 setRollbackOnly() 메써드를 호출함으로써 트랜잭션을 롤백할 수 있다.

TransactionTemplate를 사용하려는 어플리케이션 클래스들은 반드시 PlatformTransactionManager를 통해야 한다. 항상 자바빈 프라퍼티나 생성자 인자처럼 노출된다.

mock 혹은 stub PlatformTransactionManager를 가진 그런 클래스들을 유닛 테스트 하기는 쉬운 일이다. 여기에는 JNDI 룩업 혹은 정적인 마법이 존재하지 않는다 : 이것은 단순한 인터페이스이다. 대개, 당신은 유닛 테스트를 간단하게 만들기 위해 Spring을 사용할 수 있다.

7.3.2. PlatformTransactionManager 사용하기

당신은 트랜잭션을 직접 관리하기 위해 org.springframework.transaction.PlatformTransactionManager도 역시 사용할 수 있다. 단순히 사용하고 있는 PlatformTransactionManager의 구현 클래스의 빈 참조를 당신의 빈에 넘기기만 하면 된다. 그리고나서, TransactionDefinitionTransactionStatus 객체를 사용함으로써, 당신은 트랜잭션을 초기화하고, 롤백, 커밋할 수 있다.

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = transactionManager.getTransaction(def);

try {
    // execute your business logic here
} catch (MyException ex) {
    transactionManager.rollback(status);
    throw ex;
}
transactionManager.commit(status);

7.4. 선언적 트랜잭션 관리

Spring은 또한 선언적 트랜잭션 관리를 제공한다. 이것은 Spring AOP에 의해 가능하다.

대부분의 Spring 사용자들은 선언적 트랜잭션 관리를 선택한다. 이것은 어플리케이션 코드에 대한 최소한의 충격을 주는 선택이다. 그리고, 이것은 (어플리케이션 코드에)침입하지 않는 경량 컨테이너의 이상에 일관된다.

EJB CMT를 검토하고 Spring 선언적 트랜잭션 관리와의 유사점과 차이점을 설명함으로써 시작에 도움을 줄 수 있을 것이다. 기본적인 접근방법은 비슷하다 : 개별적인 메쏘드들에 대한 트랜잭션의 행동을 지정함으로써 가능하다. 그리고 만약 필요하다면, 트랜잭션 컨텍스트 내의 setRollbackOnly() 호출을 만드는 것 역시 가능하다. 하지만 차이점은 다음과 같다:

  • JTA에 묶여 있는 EJB CMT와 다르게, Spring의 선언적 트랜잭션 관리는 어떠한 환경에서도 동작한다. 이것은 JDBC, JDO, Hibernate 혹은 내부의 어떠한 다른 트랜잭션과도 동작할 수 있으며, 단지 설정의 변경만으로 가능하다.

  • Spring은 EJB와 같은 특별한 클래스들 뿐만 아니라, 어떠한 POJO에 대해서도 선언적 트랜잭션 관리를 적용할 수 있게 해준다.

  • Spring은 선언적 롤백 규칙을 제공한다 : 아래에서 논의할 EJB와 동등하지 않는 특징인데, 롤백은 단지 프로그래밍적인 방법만이 아니라 선언적으로도 제어가능해진다.

  • Spring은 AOP를 사용해서 트랜잭션적인 행위들을 커스터마이징하기 위한 기회를 제공한다. 예를 들어, 만약 당신이 트랜잭션 롤백 상황에 임의의 행위를 삽입하고자 한다면, 할 수 있다. 또한 트랜잭션적인 통보와 함께 부가적인 통보를 추가할 수도 있다. EJB CMT에서는 당신은 setRollbackOnly() 이상으로 컨테이너의 트랜잭션 관리에 영향을 끼칠 수 있는 방법이 없다.

  • Spring은 높은 성능의 어플리케이션 서버들처럼, 원격 호출에 걸쳐 트랜잭션 컨텍스트의 전달을 지원하지는 않는다. 만약 당신이 이러한 특성이 필요하다면, EJB를 사용하라고 권해주고 싶다. 그러나, 이러한 특성은 거의 사용되지 않는다. 일반적으로 우리는 원격 호출을 확장하기 위해 트랜잭션을 원하지는 않는다.

롤백 규칙의 개념은 중요하다: 이 규칙은 우리가 어떤 예외상황일 때 자동 롤백이 발생되어야 하는지를 지정할 수 있게 해준다. 우리는 이것을 자바 코드가 아니라 설정파일에 선언적으로 지정한다. 그래서 우리가 프로그래밍적으로 현재 트랜잭션을 롤백하기 위해 여전히 TransactionStatus 객체의 setRollbackOnly() 를 호출할 수 있을지라도, 대부분 MyApplicationException이 항상 롤백을 발생시키도록 규칙을 지정할 수 있다. 이것은 비지니스 오브젝트들은 트랜잭션 하부구조에 의지할 필요가 없다는 중요한 장점을 가진다. 예를 들어, 그 클래스들은 어떠한 Spring API, 트랜잭션 혹은 다른 무엇도 import할 필요가 없다.

EJB의 (트랜잭션 롤백에 대한) 디폴트 행위는 시스템 예외 (대개 런타임 예외)시에 EJB 컨테이너가 트랜잭션 롤백을 자동으로 수행하지만, EJB CMT는 어플리케이션 예외(java.rmi.RemoteException를 제외한 체크된 예외)시 자동으로 트랜잭션 롤백을 하지 않는다. 선언적 트랜잭션 관리를 위한 Spring의 기본적인 동작은 EJB 규칙(체크되지 않은 예외에 대해서만 자동 롤백)을 따르지만, 이것을 종종 커스터마이징하기에 유용하다.

우리의 벤치마크에 따르면, Spring 선언적 트랜잭션 관리의 성능은 EJB CMP의 것을 앞서고 있다.

Spring에서 트랜잭션적인 프록시를 세팅하는데 사용되는 일반적인 방법은 TransactionProxyFactoryBean를 통한 것이다. 우리는 트랜잭션 프록시로 감쌀 타겟 객체가 필요하다. 타겟 객체는 일반적으로 POJO 빈 정의를 따른다. TransactionProxyFactoryBean을 정의할 때, 관련된 PlatformTransactionManager에 대한 참조와 트랜잭션 속성을 넘겨주어야 한다. 트랜잭션 속성은 위에서 얘기한 트랜잭션 정의를 포함한다. 다음의 예시를 보자.

<!-- this example is in verbose form, see note later about concise
     for multiple proxies! -->
<!-- the target bean to wrap transactionally -->
<bean id="petStoreTarget">
  ...
</bean>
<bean id="petStore"
    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager"><ref bean="transactionManager"/></property>
    <property name="target"><ref bean="petStoreTarget"/></property>
    <property name="transactionAttributes">
        <props>
            <prop key="insert*">PROPAGATION_REQUIRED,-MyCheckedException</prop>
            <prop key="update*">PROPAGATION_REQUIRED</prop>
            <prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
        </props>
    </property>
</bean>

트랜잭션 프록시는 타겟의 인터페이스를 구현한다. 위 예시의 경우, petStoreTarget이라는 아이디를 가진 빈이다. (CGLIB를 사용하여 타겟 객체에 트랜잭션 프록시를 구현하는 것이 가능하다. 이것을 위해 proxyTargetClass 프라퍼티를 true로 세팅하라. 만약 타겟 객체가 어떠한 인터페이스도 구현하고 있지 않다면 자동으로 발생할 것이다. 물론, 일반적으로 우리는 클래스보다는 인터페이스를 프로그래밍할 것을 원한다.) proxyInterfaces 프라퍼티를 사용하여 단지 특정한 타겟 인터페이스만 프록시하도록 트랜잭션 프록시를 제한하는 것은 가능하다. (그리고 대개의 경우 훌륭한 생각이다.) 그리고 또한 org.springframework.aop.framework.ProxyConfig로부터 상속받은 몇가지 프라퍼티들을 통해 TransactionProxyFactoryBean의 행위를 커스터마이징하고 모든 AOP 프록시 팩토리들과 공유하는 것 역시 가능하다.

여기에서의 transactionAttributes는 org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource 클래스에 정의된 프라퍼티 포맷을 사용하여 세팅된다. 와일드카드를 포함한 메써드명의 매핑은 매우 직관적이다. insert* 매핑의 값이 롤백 규칙을 포함하고 있다는 것을 눈여겨 보아라. 여기에서의 -MyCheckedException는 만약 메써드가 MyCheckedException 혹은 그 하위 예외 클래스를 던진다면, 트랜잭션은 자동으로 롤백될 것이다. 다중 롤백 규칙 역시 콤마 구별자로 여기에 지정될 수 있다. - 접두사는 롤백을 수행하고, + 접두사는 커밋을 지정한다. (+ 옵션을 주는 것은 체크되지 않은 예외시에조차 커밋을 허용한다. 당신이 무엇을 하고 있는지 확실히 알아야만 한다!)

TransactionProxyFactoryBean은 추가적으로 끼여드는 행위를 위해, "preInterceptors", "postInterceptors" 프라퍼티를 사용하여 "pre" 혹은 "post" advice를 세팅하게 해준다. pre, post advice는 얼마든지 세팅될 수 있고, 그 타입은 Advisor (이 경우 그것은 pointcut을 포함할 수 있다.) 일 것이다. MethodInterceptor 혹은 어떤 advice 타입도 현재의 Spring 설정 (ThrowsAdvice, AfterReturningtAdvice 혹은 BeforeAdvice 등이 기본적으로 지원된다.)에 의해 지원된다. 이러한 advice들은 반드시 공유-인스턴스 모델을 지원해야 한다. 만약 당신이 상태유지 믹스인 (stateful mixins)처럼 발전된 AOP 특성들을 가진 트랜잭션적인 프록싱을 필요로 한다면, 일반적으로 편리한 프록시 생성자인 TransactionProxyFactoryBean보다는 포괄적인 org.springframework.aop.framework.ProxyFactoryBean를 사용하는 것이 최선일 것이다.

오토프록싱을 세팅하는 것 역시 가능한데, AOP 프레임워크를 세팅함으로써 클래스들은 자동으로 개별 프록시 정의 없이도 프록시될 수 있다.

더 많은 정보와 예제들은 AOP 챕터를 참조하길 바란다.

노트: TransactionProxyFactoryBean 정의를 위에서와 같은 형태로 사용하는 것은 많은 동일한 트랜잭션 프록시들이 생성될 필요가 있을 때 과도하게 장황해보일 수 있다. Section 5.7, “간결한 프록시 정의”에 서술된 바처럼, 당신은 트랜잭션 프록시 정의의 장황함을 상당부분 제거하기 위해, 내부 빈 정의의 장점을 가진 부모/자식 빈 정의의 이점을 가지기를 바랄 것이다.

Spring의 선언적 트랜잭션 관리를 효과적으로 사용하기 위해서, 당신은 AOP 전문가가 될 필요가 없다--혹은 최소한, AOP의 대부분에 대해 알 필요도 없다. 하지만, 당신이 정말로 Spring AOP의 "끝내주는 사용자"가 되고자 한다면, 당신은 강력한 AOP의 능력을 가진 선언적 트랜잭션 관리를 조합하는 것이 간단한 일임을 알 수 있을 것이다.

7.4.1. BeanNameAutoProxyCreator, 또 다른 선언적 접근방법

TransactionProxyFactoryBean은 매우 유용하고, 트랜잭션 프록시로 객체들을 감쌀 때 전체적인 제어를 할 수 있게 해준다. 부모/자식 빈 정의와 타겟을 가지고 있는 내부 빈들을 함께 사용하는 것은 일반적으로 트랜잭션적인 감싸기를 위한 최고의 방법이다. 당신이 완전히 동일한 형태로 많은 수의 빈들을 감쌀 필요가 있을 경우(예를 들어, 반복 사용 어구, '모든 메쏘드들을 트랜잭션하게 만들어라'), BeanNameAutoProxyCreator라고 불리는 BeanFactoryPostProcessor를 사용하는 것은 덜 장황하고 간단한 대안적인 접근방법이 될 것이다.

재감싸기를 위해서, ApplicationContext이 그것의 초기화 정보를 읽을 때, 그 안에 있는 BeanPostProcessor 인터페이스를 구현하는 모든 빈들을 초기화하고, 그 빈들에게 ApplicationContext 내의 모든 다른 빈들을 이후에 처리할(post-process) 기회를 제공한다. 때문에 이러한 메카니즘을 사용하면, 적절하게 설정된 BeanNameAutoProxyCreator는 ApplicationContext 내의 모든 다른 빈들을 (이름으로 그것들을 인식하여) 이후에 처리하기 위해 사용될 수 있다. 생성된 실제 트랜잭션 프록시는 TransactionProxyFactoryBean를 사용하여 생성되었다는 점에서 반드시 동일한데, 여기에 대해서는 이후에도 논의되지는 않을 것이다.

아래의 설정 예시를 보도록 하자.

  <!-- Transaction Interceptor set up to do PROPAGATION_REQUIRED on all methods -->
  <bean id="matchAllWithPropReq" 
      class="org.springframework.transaction.interceptor.MatchAlwaysTransactionAttributeSource">
    <property name="transactionAttribute"><value>PROPAGATION_REQUIRED</value></property>
  </bean>
  <bean id="matchAllTxInterceptor" 
      class="org.springframework.transaction.interceptor.TransactionInterceptor">
    <property name="transactionManager"><ref bean="transactionManager"/></property>
    <property name="transactionAttributeSource"><ref bean="matchAllWithPropReq"/></property>
  </bean>

  <!-- One BeanNameAutoProxyCreator handles all beans where we want all methods to use 
       PROPAGATION_REQUIRED -->
  <bean id="autoProxyCreator" 
      class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="interceptorNames">
      <list>
        <idref local="matchAllTxInterceptor"/>
        <idref bean="hibInterceptor"/>
      </list>
    </property>
    <property name="beanNames">
      <list>
        <idref local="core-services-applicationControllerSevice"/>
        <idref local="core-services-deviceService"/>
        <idref local="core-services-authenticationService"/>
        <idref local="core-services-packagingMessageHandler"/>
        <idref local="core-services-sendEmail"/>
        <idref local="core-services-userService"/>
      </list>
    </property>
  </bean>

ApplicationContext 내에 이미 TransactionManager 인스턴스를 가지고 있다고 가정한 상태로, 첫번째 우리가 해야 할 일은 사용할 TransactionInterceptor 인스턴스를 생성하는 일이다. TransactionInterceptor는 프라퍼티로 넘겨진 TransactionAttributeSource 구현 객체에 기반하여 어떤 메써드가 인터셉트되어야 하는지를 결정한다. 위의 경우, 우리는 모든 메써드를 일치시키는 매우 간단한 경우를 다루고자 한다. 이것은 물론 가장 효율적인 접근방법은 아니지만, 매우 빨리 세팅될 수 있다. 왜냐하면 우리는 특별히 이미 정의된, 모든 메써드를 단순히 일치시켜주는, MatchAlwaysTransactionAttributeSource를 사용할 수 있기 때문이다. 만약 우리가 좀 더 기술하려면, MethodMapTransactionAttributeSource, NameMatchTransactionAttributeSource 혹은 AttributesTransactionAttributeSource와 같이 다른 것을 사용할 수 있다.

이제 트랜잭션 인터셉터를 가지게 되었다면, 우리가 정의한 BeanNameAutoProxyCreator 인스턴스를 동일한 형태로 포장되길 바라는 ApplicationContext 안의 6개의 빈들의 이름과 함께, 인터셉터에 단순히 넘겨주기만 하면 된다. 당신이 알 수 있듯이, 최종결과는 6개의 빈들을 TransactionProxyFactoryBean으로 감싸는 것보다 매우 간소하다. 7번째의 빈을 감싸는 것 역시 설정에서 한 줄을 추가하기만 하면 된다.

당신은 우리가 다중 인터셉터를 적용할 수 있다는 점을 알아차렸을지 모른다. 이런 경우, 우리는 역시 우리가 이전에 (bean id=hibInterceptor)라고 정의했던 HibernateInterceptor 또한 적용할 수 있으며, 이것은 우리를 위해 Hibernate Sessions를 관리해줄 것이다.

TransactionProxyFactoryBeanBeanNameAutoProxyCreator의 사용 사이를 왔다갔다 할 때, 빈 명명에 관해 당신이 기억해두어야 할 것이 하나 있다. 전자를 위해서는, 만약 타겟빈이 내부 빈으로 정의되지 않았다면, 당신은 일반적으로 당신이 감싸고자 하는 타겟빈을 myServiceTarget와 비슷한 형태의 id로 넘겨주고, 프락시 객체는 myService라는 id로 넘겨줄 것이다. 그리고 감싸여진 객체의 사용하는 모든 사람들은 단지 프락시, 다시 말해, myService만을 참조할 것이다. (이것은 단지 명명규칙의 예시이다. 중요한 점은 목표객체는 프락시와 다른 이름을 가진다는 것이고, 둘 다 ApplicationContext으로부터 사용가능하다는 점이다.) 그러나, BeanNameAutoProxyCreator를 사용할 경우, 당신은 타겟 객체에다 myService 비슷한 이름을 부여한다. 그러면, BeanNameAutoProxyCreator가 타겟 객체를 전처리하고 프락시를 생성할 때, 어플리케이션 컨텍스트의 원래의 빈 이름 아래에 그 프락시를 위치시킨다. 그 지점에서 단지 프락시(감싸여진 객체)만이 ApplicationContext로부터 사용가능하다. 내부 빈으로 정의된 타겟을 가진 TransactionProxyFactoryBean를 사용할 경우, 명명 이슈는 신경쓸 바가 아니다. 왜냐하면, 내부 빈에는 일반적으로 이름이 주어지지 않기 때문이다.

7.5. 프로그래밍적/선언적 트랜잭션 관리 중 선택하기

프로그래밍적인 트랜잭션 관리는 대개 당신이 매우 적은 수의 트랜잭션적인 동작들을 다룰 때에만 좋은 생각이다. 예를 들어, 만약 당신이 어떤 업데이트 동작들에만 트랜잭션이 필요한 웹 어플리케이션을 개발한다면, 당신은 Spring 혹은 다른 기술들을 사용해서 트랜잭션적인 프락시를 세업하는 것을 바라지 않을지도 모른다. 이 경우, TransactionTemplate을 사용하는 것은 좋은 접근방법이 될 것이다.

반면, 만약 당신의 어플리케이션이 매우 많은 트랜잭션적인 동작들을 가진다면, 선언적 트랜잭션 관리는 대개 매우 가치있는 판단일 것이다. 이것은 비지니스 로직의 외부에서 트랜잭션 관리를 유지해주며, Spring 에서 그 설정을 하는 것은 어려운 것이 아니기 때문이다. EJB CMT보다 Spring을 사용한다면, 선언적 트랜잭션 관리 설정의 비용은 매우 감소된다.

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

Spring의 트랜잭션 관리 능력들--그리고 특히 그것의 선언적 트랜잭션 관리--은 J2EE 어플리케이션이 어플리케이션 서버를 필요로할 때 트랜잭션적인 판단을 현저하게 변화시킨다.

특히, 당신이 단지 EJB를 통해 선언적 트랜잭션을 얻고자 한다면 어플리케이션 서버는 필요가 없다. 사실, 강력한 JTA의 능력을 가진 어플리케이션 서버를 가지고 있다고 할지라도, 당신은 EJB CMT보다 더욱 강력하고 더더욱 생산적인 프로그래밍 모델을 제공해주는 Spring 선언적 트랜잭션을 선택하는 것이 좋을 지도 모른다.

오로지 당신이 다중 트랜잭션 리소스를 지원할 필요가 있을 때에만, 어플리케이션 서버의 JTA 능력이 필요하다. 많은 어플리케이션들은 이러한 요구에 직면하지 않는다. 예를 들어, 많은 고성능 어플리케이션들은 Oracle 9i RAC와 같이 크게 확장가능한 하나의 데이터베이스를 사용한다.

물론 당신은 JMS와 JCA와 같은 어플리케이션 서버의 또 다른 능력들을 필요로 할 지 모른다. 그러나, 당신이 만약 JTA만을 필요로 하는 것이라면, 당신은 JOTM과 같은 JTA가 첨부된 오픈 소스의 사용을 고려할 수 있다. (Spring은 JOTM과 외부에서 통합된다.) 그렇지만, 2004년 초반, 고성능 어플리케이션 서버들은 XA 트랜잭션들에 대한 보다 튼튼한 지원을 제공한다.

가장 중요한 점은 Spring을 사용하게 되면, 언제 당신의 어플리케이션을 최종적인 어플리케이션 서버로 확장시킬 것인가에 대한 시점을 선택할 수 있다. EJB CMT 혹은 JTA를 사용하는 것에 대한 대안이라고는 오로지 JDBC 커넥션과 같은 로컬 트랜잭션들을 사용해서 코딩했다가 그 코드가 글로벌 컨테이너 관리 트랜잭션에서 작동할 필요가 생겼을 때 엄청난 개정작업에 직면해야만 했던 시절은 갔다. Spring을 사용한다면 바꾸기 위해 필요한 것은 오로지 설정뿐이다. 당신의 코드는 바꿀 필요가 없다.

7.7. 공통적인 문제

개발자들은 그들의 요구사항들에 맞는 적절한 PlatformTransactionManager 구현을 사용하는데 주의를 기울여야 한다.

Spring 트랜잭션 추상화가 JTA 글로벌 트랜잭션과 동작하는 방식을 이해하는 것은 중요한 일이다. 적절하게 사용되었을 때, 여기엔 아무런 문제가 없다. Spring은 단지 간소화하고 이식가능한 추상화만을 제공한다.

만약 당신이 글로벌 트랜잭션을 사용한다면, 당신은 모든 트랜잭션적인 동작들에 대해 Spring의 org.springframework.transaction.jta.JtaTransactionManager반드시 사용해야만 한다. 만약 그렇지 않으면 Spring은 컨테이너 데이터소스들과 같은 리소스들에서 로컬 트랜잭션을 수행하고자 할 것이다. 그런 로컬트랜잭션들은 말이 안되며, 좋은 어플리케이션 서버라면 그것들을 에러로 간주할 것이다.