스프링 버전 5.3.18 기준으로 작성됨

 

이 글에서는 용어를 다음과 같이 줄여서 표현한다.

OpenEntityManagerInViewFilter => 필터

WebApplicationContext => wac

TransactionSynchronizationManager => tsm

EntityManagerFactory => emf

EntityManager => em

EntityManagerHolder => emh

AsyncRequestInterceptor => ari

 

Spring-orm 에서 OSIV 처리를 담당하는 주요 클래스는 org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter 이다. 이 클래스는 다음과 같이 작동한다.

 

이 클래스는 OncePerRequestFilter 타입이다. request가 들어오면 doFilterInternal 에서 다음을 수행한다.

 

step 1. 현재 필터 자신의 인스턴스에 emf 인스턴스가 존재하는지(초기화되어 있는지) 확인한다. 없으면 wac 에서 emf를 찾아서 세팅한다.

- 이 동작은 필터가 받는 첫 요청 처리에서 이루어질 것이다.

 

step 2. tsm에 emf를 키로 리소스 존재 여부를 확인해서, 존재할 경우 participate = true 를 실행한다.

- tsm에 이 리소스가 존재한다는 것은 이미 이 요청에 대해 생성된 em이 존재한다는 뜻이다. 때문에 글자 그대로 기존 요청에 '참여' 함을 표시한다.

 

step 3. 이 요청이 async request이면서 AsyncManager가 ari 를 가지고 있다면, ari 에 em 을 바인딩한다. (bindEntityManager) 그리고 step 6 으로 넘어간다.

 

step 4. 이 요청이 async request 가 아니거나, async 라도 AsyncManager가 ari 를 가지고 있지 않다면, 다음 과정을 수행한다. (일반적인 mvc요청은 이 방식으로 이루어진다.)

1. emf 로 em을 생성한다.

2. 생성한 em 을 가지는 emh 를 생성한다.

3. tsm 에 bindResource(emf, emh) 를 호출한다.

- tsm의 bindResource 는 내부 쓰레드로컬에 맵을 세팅하여 그곳에 리소스를 저장해둔다. 때문에 이미 request에 대해 생성된 em이 존재하는 경우, tsm를 통해 이를 확인할 수 있도록 한다. (step 2)

4. emf, emh 를 가지는 ari를 생성하고, AsyncManager에 ari를 등록한다.

- 이렇게 등록된 ari 가 다음번 async 요청에서 바로 ari 에 em을 바인딩할 수 있게 된다. (step 3)

 

step 5. filterChain.doFilter

 

step 6. finally 에서, participate = false 인 경우 tsm에서 em을 제거하고(unbindResource(emf)), async요청이 아닐 경우 EntityManagerFactoryUtils.closeEntityManager 로 em을 완전히 닫는다.

- '참여' 요청이라면 기존 요청이 존재하므로 여기서 em을 unbind 해서는 안 된다. 때문에 participate를 체크한다.

 

아주 좋은 글이다.



Spring Interceptor(혹은 Servlet Filter)에서 POST 방식으로 전달된 JSON 데이터 처리하기

https://meetup.toast.com/posts/44

'Java > Spring' 카테고리의 다른 글

Spring OSIV 작동 방식 (OpenEntityManagerInViewFilter)  (0) 2022.05.19
스프링의 트랜잭션 관리  (0) 2017.06.30

스프링의 트랜잭션 관리


종합적인 트랜잭션 관리는 스프링 프레임워크를 사용하는 가장 중요한 이유 중 하나이다. 스프링 프레임워크는 다음과 같은 장점을 지닌 트랜잭션 관리를 위한 일관된 추상화를 제공한다.


  • 자바 트랜잭션 API(JTA), JDBC, 하이버네이트, 자바 영속성 API(JPA), 자바 데이터 객체(JDO) 와 같은 서로 다른 트랜잭션을 아우르는 일관된 프로그래밍 모델
  • 선언적 트랜잭션 관리
  • JTA 와 같은 복잡한 트랜잭션 API 보다 간단한 프로그래밍 방식의 트랜잭션 관리 API
  • 스프링 데이터 접근 추상화를 통한 훌륭한 통합

다음 섹션은 스프링 프레임워크릐 부가 가치와 기술에 대하여 설명한다. (또한 가장 실용적인 예제와 어플리케이션 서버 통합, 그리고 공통된 문제들에 대한 솔루션에 대한 논의를 포함한다.)


  • 스프링 프레임워크의 트랜잭션 관리 모델의 장점 에서는 왜 당신이 EJB 컨테이너 관리 트랜잭션(CMT) 이나 하이버네이트와 같은 상용 로컬 트랜잭션 관리 대신 스프링 프레임워크의 트랜잭션 관리를 사용해야 하는지 설명한다.
  • 스프링 프레임워크 추상화의 이해 에서는 핵심 클래스를 선보이며 다양한 자원으로부터 DataSource 인스턴스를 획득하고 설정하는 방법에 대해 설명한다.
  • 트랜잭션을 통한 동기화 에서는 어플리케이션 코드가 어떻게 자원의 생성, 재사용, 제거를 안전하게 수행하는지 설명한다.
  • 프로그래밍 방식 트랜잭션 관리 에서는 프로그래밍 방식(프로그램 코드를 통한) 트랜잭션 관리에 대해 알아본다.
  • 트랜잭션 바운드 이벤트 에서는 당신이 트랜잭션을 이용해 어떻게 어플리케이션 이벤트를 사용할 수 있는지 설명한다.


스프링 프레임워크의 트랜잭션 관리 모델의 장점

전통적으로 자바 EE 개발자들은 트랜잭션 관리를 위한 두 가지 선택지를 가지고 있었다. 글로벌 트랜잭션과 로컬 트랜잭션이 그것인데, 둘 모두 큰 한계를 가지고 있다. 글로벌와 로컬 트랜잭션 관리에 대해서는 다음 두 섹션에서 논하며 스프링 트랜잭션 관리가 이 두 트랜잭션 관리의 한계점을 다루는 방식을 설명한다.


글로벌 트랜잭션

글로벌 트랜잭션은 다수의 트랜잭션 관련 자원들을 다룰 수 있도록 해준다. 전형적인 트랜잭션 관련 자원은 관계형 데이터베이스와 메시지 큐와 같은 것들이다. 어플케이션 서버는 JTA 라는 다루기 힘든 API 를 통해 글로벌 트랜잭션을 관리한다. 더 나아가서 JTA 의 UserTransaction 은 일반적으로 JNDI 를 통해 얻어져야 하는데, 이는 JTA 를 사용하기 위해서는 JNDI 도 필요하다는 것을 의미한다. JTA 가 보통 어플리케이션 서버 환경에서만 유효하듯이, 확실히 글로벌 트랜잭션을 사용함은 어플리케이션의 잠재적인 재사용성을 제한하게 된다.

이전에는 EJB CMT(컨테이너 관리 트랜잭션) 을 사용하는 것이 글로벌 트랜잭션을 관리하기 위해 선호되는 방법이었다. CMT 는 하나의 선언적 트랜잭션 관리 형식이다. EJB CMT 는 트랜젹선 관련 JNDI 룩업을 사용할 필요가 없었으나, EJB 자체가 JNDI 를 필요로 했다. 자바 코드를 통한 트랜잭션 관리의 필요성을 거의 없앴긴 했지만, 완전히 없애지는 못했다. 중요한 단점은 CMT 는 JTA 와 어플리케이션 서버 환경에 묶여 있었다는 점이고, 또한 비즈니스 로직을 EJB 에 구현하거나, 적어도 EJB 의 외관 뒤에 구현하는 경우에만 사용할 수 있었다. EJB 의 선언적 트랜잭션 관리를 위한 강력한 대안에도 불구하고 이러한 단점들은 너무나 큰 것이어서 EJB 는 매력적인 제안이 되지 못했다.


로컬 트랜잭션

로컬 트랜잭션은 자원 종속적이다. JDBC 커넥션과 관련된 트랜잭션이 그렇다. 로컬 트랜잭션은 사용하지 쉬울 수 있지만, 큰 약점을 가지고 있다. 여러가지 트랜잭션 자원으로 작업할 수 없다. 가령, JDBC 커넥션을 사용하는 트랜잭션 관리 코드는 JTA 글로벌 트랜잭션에서 동작하지 않는다. 왜냐하면 어플리케이션 서버는 트랜잭션 관리와 관련이 없기 때문에, 여러 자원에 대한 정확성을 보장할 수 없다. (대부분의 어플리케이션은 단일 트랜잭션 자원을 사용한다는 점은 알아둬야 한다.) 또다른 단점은 로컬 트랜잭션은 프로그래밍 모델 안으로 급속히 퍼지게 된다.


스프링 프레임워크의 일관된 프로그래밍 모델

스프링은 글로벌와 로컬 트랜잭션의 약점을 해결한다. 스프링은 어플리케이션 개발자로 하여금 어떤 환경에서든 일관된 프로그래밍 모델을 사용할 수 있도록 한다. 한 번 작성된 코드는 다른 환경의 다른 트랜잭션 관리 전략에서도 사용될 수 있다. 스프링 프레임워크는 선언적 트랜잭션 관리와 프로그래밍 방식 트랜잭션 관리를 제공한다. 대부분 선언적 트랜잭션 관리가 선호되는데, 이 방식은 거의 모든 상황에 적합하다.

프로그래밍 방식 트랜잭션 관리를 사용하면 개발자는 어떠한 트랜잭션 기반 환경에서도 동작하는 스프링 프레임워크 트랜잭션 추상화로 작업할 수 있다. 선언적 트랜잭션 관리를 사용하면 개발자는 트랜잭션 관리와 관련된 코드를 아주 조금, 혹은 전혀 작성하지 않아도 되기 때문에, 코드는 스프링 프레임워크 트랜잭션 API 혹은 그 어떤 트랜잭션 API 에도 의존성을 갖지 않는다.

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

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

특히 EJB 를 통한 선언적 트랜잭션을 위한 어플리케이션 서버는 필요하지 않다. 사실 당신의 어플리케이션 서버가 강력한 JTA 기능을 가지고 있다고 해도, 스프링 프레임워크의 선언적 트랜잭션은 EJB CMT 보다 더 강력하고 더 생산성이 뛰어난 프로그래밍 모델을 제공한다.

보통 어플리케이션 서버의 JTA 기능이 요구되는 경우는 어플리케이션이 여러가지 자원을 다뤄야 할 때 뿐인데, 많은 어플리케이션이 이런 기능을 필요로 하진 않는다. 많은 수의 고급 어플리케이션은 대신 높은 확장성을 지닌 단일한 데이터베이스를 사용한다. (오라클 RAC 와 같은) Atomikos Transactions, JOTM 과 같은 독립형 트랜잭션 관리자는 다른 선택 사항이다. 물론 자바 메시지 서비스(JMS), 자바 EE 컨테이너 아키텍처(JCA) 와 같은 어플리케이션 서버의 다른 기능을 필요로 할 수는 있다.

스프링 프레임워크는 어플리케이션을 언제 완전한 어플리케이션 서버로 확장할지 선택할 수 있게 해준다. JDBC 커넥션과 같은 로컬 트랜잭션으로 코드를 작성하기 위한 선택이 EJB CMT 나 JTA 뿐이던 시절은 지났다. 그리고 그런 코드를 글로벌, 컨테이너 관리 트랜잭션 안에서 실행하기 위해서는 막대한 재작업이 요구된다. 스프링 프레임워크를 사용하면 코드가 아닌 설정 파일 안의 빈 정의를 조금 바꿀 뿐이다.



스프링 프레임워크 트랜잭션 추상화의 이해


스프링 트랜잭션 추상화의 핵심은 트랜잭션 전략의 개념이다. 트랜잭션 전략은 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)이다. PlatformTransactionManager 는 하나의 인터페이스이기 때문에 필요에 따라 손쉽게 모의화 또는 스텁 될 수 있다. 이 인터페이스는 JNDI 와 같은 룩업 전략에 구애받지 않는다. PlatformTransactionManager 구현체는 스프링 프레임워크 IoC 컨테이너 안의 다른 객체들(또는 빈)과 같은 방식으로 정의된다. 이런 장점은 스프링 프레임워크 트랜잭션 추상화를 가치있게 만들고, JTA 를 사용할 때조차도 이 장점은 유효하다. 트랜잭션 관련 코드의 테스트는 JTA 를 직접 사용할 때보다 훨씬 쉬워진다.


다시 스프링의 철학을 따라서, PlatformTransactionManager 의 모든 메서드에서 발생할 수 있는 예외인 TransactionException 은 unchecked(런타임 예외)이다. 트랜잭션 인프라의 에러는 거의 대부분 치명적이다. 어플리케이션 코드가 실제로 트랜잭션 실패를 복구할 수 있는 경우에는 TransactionException 을 캐치할 것인지 선택할 수 있다. 여기서 중요한 점은 개발자에게 트랜잭션 예외 캐치를 강제하지 않고 선택권을 준다는 것이다.


getTransaction(..) 메서드는 TransactionDefinition 파라미터에 따른 TransactionStatue 객체를 반환한다. 반환된 TransactionStatus 객체는 새로운 트랜잭션을 나타내거나, 혹은 현재 호출 스택에 매칭되는 기존 트랜잭션을 나타낼 수 있다. 후자의 경우는 자바 EE 트랜잭션 컨텍스트와 마찬가지로 TransactionStatus 가 실행 쓰레드와 연관되어 있음을 의미한다.


TransactionDefinition 인터페이스는 다음 사항들을 명시한다.


  • 격리: 이 트랜잭션이 다른 트랜잭션으로부터 격리되는 정도. 예로, 이 트랜잭션이 다른 트랜잭션의 커밋되지 않은 쓰기 작업을 볼 수 있는가?
  • 전파: 보통 한 트랜잭션 범위 안에서 실행된 코드는 그 트랜잭션 안에서 동작한다. 그러나 이미 트랜잭션이 존재할 때 트랜잭션 관련 메서드가 실행될 경우 트랜잭션을 어떻게 묶을 것인지 선택할 수 있다. 예로, 기존 트랜잭션 안에서 실행할 것인가, 혹은 생성된 새로운 트랜잭션으로 실행하여 기존 트랜잭션과 분리할 것인가. 스프링은 EJB CMT 와 유사한 모든 트랜잭션 전파 옵션을 제공한다. 스프링의 트랜잭션 전파의 의미에 대해서는 트랜잭션 전파 에서 읽어볼 수 있다.
  • 타임아웃: 트랜잭션 인프라에 의하여, 이 트랜잭션 수행이 자동 타임아웃-롤백 될 때까지 얼마만큼의 시간이 주어지는가.
  • 읽기 전용 상태: 읽기 전용 트랜잭션은 트랜잭션이 데이터를 읽기는 하지만 수정할 수는 없도록 할 때 사용된다. 읽기 전용 트랜잭션은 하이너베이트를 사용할 때와 같은 몇몇 경우에 유용할 최적화가 될 수 있다.

이 설정들은 표준적인 트랜잭션 개념을 반영한다. 필요하다면 트랜잭션 격리 수준과 다른 핵심적인 트랜잭션 개념을 논한 자료를 참조하라. 스프링 프레임워크나 다른 트랜잭션 관리 솔루션을 사용하기 위해서는 이 개념들을 이해해야 한다.

TransactionStatus 인터페이스는 트랜잭션 코드가 트랜잭션 실행과 쿼리 트랜잭션 상태를 제어하기 위한 쉬운 방법을 제공한다. 그 개념은 모든 트랜잭션 API 에게 공통된 사항인 것처럼 친숙해야 한다.

public interface TransactionStatus extends SavepointManager {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

}

스프링의 선언적 트랜잭션 관리와 프로그래밍 방식 트랜잭션 관리 중 어떤 것을 선택하느냐에 상관없이 반드시 알맞는 PlatformTransactionManager 구현체가 정의되어야 한다. 일반적으로 의존성 주입을 통해 구현체를 정의한다.

PlatformTransactionManager 구현체에는 JDBC, JTA, 하이버네이트, 기타 등등의 트랜잭션이 작동할 환경에 대한 지식이 요구된다. 다음 예제는 로컬 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>


JTA 를 사용하는 자바 EE 컨테이너라면 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.xsd

        http://www.springframework.org/schema/jee

        http://www.springframework.org/schema/jee/spring-jee.xsd">


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


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


    <!-- other <bean/> definitions here -->


</beans>


JtaTransactionManager 는 DataSource 나 다른 어떤 자원에 대해서도 알 필요가 없다. 컨테이너의 글로벌 트랜잭션 관리를 기반으로 하기 때문이다.


다음 예제에서 보이듯, 하이버네이트 로컬 트랜잭션 또한 쉽게 사용할 수 있다. 이 경우 하이버네이트 LocalSessionFactoryBean 을 정의해야 한다. LocalSessionFactoryBean 은 어플리케이션 코드가 하이버네이트 Session 인스턴스를 얻기 위해 사용된다. 


DataSource 빈 정의는 이전의 로컬 JDBC 예제와 유사하다. 따라서 다음 예제에선 생략한다.


non-JTA 트랜잭션 관리자가 사용하는 DataSource 가 자바 EE 컨테이너가 관리하는 JNDI 룩업에 의해 얻어진다면, 이 DataSource 는 트랜잭션과 관련이 없도록 해야한다. 트랜잭션을 관리하는 것은 자바 EE 컨테이너가 아닌 스프링 프레임워크이다.


여기서의 txManager 빈은 HibernatetransactionManager 타입이다. DataSourceTransactionManager 가 DataSource 로의 참조를 필요로 하는 것과 같이, HibernateTransactionManager 도 SessionFactory 참조를 필요도 한다.


<bean id="sessionFactory" class="org.springframework.orm.hibernate5.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.hibernate5.HibernateTransactionManager">

    <property name="sessionFactory" ref="sessionFactory"/>

</bean>


하이버네이트와 자바 EE 컨테이너 관리 JTA 트랜잭션을 사용할 때는 간단히 JDBC JTA 예제에서와 같은 JtaTransactionManager 를 사용할 수 있다.


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


JTA 를 사용한다면 데이터 접근에 JDBC, 하이버네이트, JPA 또는 다른 어떤 기술을 사용하든 트랜잭션 매니저 정의는 같아야 한다. JTA 트랜잭션은 어떠한 트랜잭션 관련 자원이든 요청할 수 있는 글로벌 트랜잭션이기 때문이다.


어떤 경우든 어플리케이션 코드는 바뀌지 않는다. 트랜잭션 관리 방식을 변경하려면 그저 설정을 바꾸기만 하면 된다. 심지어 트랜잭션이 로컬에서 글로벌로 바뀌거나 혹은 그 반대일지라도 마찬가지이다.



트랜잭션을 통한 자원 동기화


이제 트랜잭션 관리자가 어떻게 생성되며 트랜잭션으로 동기화가 필요한 관련 자원으로 어떻게 연결되는지 알아본다. (예를 들어 DataSourceTransactionManager 는 JDBC DataSource 로, HibernateTransactionManager 는 하이버네이트 SessionFactory 로, 그리고 기타 등등) 이 섹션에서는 직접적으로든 간접적으로든 JDBC, 하이버네이트, 혹은 JDO 와 같은 영속성 API 를 사용하는 어플리케이션 코드가 어떻게 자원을 생성하고 재사용하고 제거하는지 설명한다. 그리고 관련 PlatformTransactionManager 를 통해 트랜잭션 동기화가 어떻게 (선택적으로)발생하는지에 대해 논한다.



고수준 동기화 처리 방법


선호된는 방법은 스프링의 영속성 통합 API 에 기반하는 가장 높은 수준의 템플릿을 사용하거나, 또는 트랜잭션 인지 팩토리 빈이나 네이티브 자원 팩토리 관리용 프록시를 통한 네이티브 ORM API 를 사용하는 것이다. 트랙잭션 인지 솔루션은 내부적으로 자원의 생성, 재사용, 제거, 선택적 트랜잭션 동기화, 그리고 예외 매핑을 다룬다. 따라서 사용자의 데이터 접근 코드는 이러한 작업을 처리할 필요는 없지만 순수한 비 형식적 영속성 로직에만 초점을 맞출 수 있다. 일반적으로 네이티브 ORM 을 사용하거나 JdbcTemplate 를 사용한 JDBC 접근을 위한 템플릿을 취한다. 이런 솔루션들에 대해서는 이 문서의 다음 챕터들에서 자세히 다룬다.



저수준 동기화 처리 방법


저수준 처리 방법에는 DataSourceUtils (JDBC), EntityManagerFactoryUtils (JPA), SessionFactoryUtil (하이버네이트), PersistenceManagerFactoryUtil (JDO), 기타 등등과 같은 클래스들이 존재한다. 어플리케이션 코드가 네이티브 영속성 API 의 자원 타입을 직접적으로 다루길 원한다면 이런 클래스들을 사용해서 적절한 스프링 프레임워크 관리 인스턴스가 얻어지고, 트랜잭션이 (선택적으로) 동기화되며, 프로세스에서 발생하는 예외가 일관된 API 에 적절하게 매핑되도록 한다.


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


Connection conn = DataSourceUtils.getConnection(dataSource);


기존 트랜잭션이 이미 동기화된 (링크된) 커넥션을 가지고 있다면, 그 인스턴스가 반환된다. 그렇지 않다면, 이 메서드 호출은 새로운 커넥션이 생성되도록 한다. 이 커넥션은 기존 트랜잭션과 (선택적으로) 동기화되고, 같은 트랜잭션 안에서 재사용된다. 언급된대로 어떠한 SQLException 이든 스프링 프레임워크의 unchecked DataAccessException 중 하나인 CannotGetJdbcConnectionException 으로 래핑된다. 이 처리법은 SQLException 에서 얻을 수 있는 정보보다 많은 정보를 제공하며 다른 지속성 기술에서도 데이터베이스 전반에 걸친 이식성을 보장한다.


이 처리 방법은 스프링 트랜잭션 관리 (트랜잭션 동기화는 선택적이다) 를 사용하지 않아도 유효하다. 때문에 이 방법은 트랜잭션 관리를 위해 스프링을 사용하든 사용하지 않든 사용할 수 있다.


물론 스프링의 JDBC, JPA, 또는 하이버네이트 지원을 한 번 사용해보면 보통 DataSourceUtils 이나 다른 헬퍼 클래스들은 선호하지 않게 된다. 왜냐하면 다른 관련 API 를 직접 사용하는 것보다 스프링 추상화를 통한 작업이 훨씬 편하기 때문이다. 예를 들어 스프링 JdbcTemplate 나 jdbc.object 패키지를 사용해서 JDBC 사용을 단순화하면 알맞는 커넥션 검색은 뒤에서 알아서 처리하기 때문에 이를 위한 특별한 코드를 작성할 필요가 없어진다.



TransactionAwareDataSourceProxy


TransactionAwareDataSourceProxy 클래스는 가장 저수준에 존재한다. 이 클래스는 타겟 DataSource 의 프록시이다. 타겟 DataSource 를 래핑하여 스프링 관리 트랜잭션을 인지하도록 한다. 이 점에 있어서는 자바 EE 서버가 제공하는 전통적인 JNDI DataSource 와 유사하다.


대부분의 경우 이 클래스의 사용은 절대 필요하거나 매력적이지는 않는데, 예외적으로 기존 코드가 반드시 표준 JDBC 인터페이스 구현체로 호출되고 전달되어야 하는 경우는 다르다. 이 경우, 기존 코드를 사용하면서 스프링 관리 트랜잭션에 참여하도록 할 수 있다. 위에서 언급한 더 높은 수준의 추상화를 사용하여 새 코드를 작성하는 편이 좋다.



선언적 트랜잭션 관리


대부분의 스프링 프레임워크 사용자는 선언적 트랜잭션 관리를 사용한다. 이 방법은 어플리케이션 코드로의 영향도가 가장 적고, 때문에 비 침습성 경량 컨테이너의 이상과 가장 일치한다.


스프링 프레임워크의 선언적 트랜잭션 관리는 스프링 관점 지향 프로그래밍 (AOP) 으로 가능하지만, 트랜잭션 측면의 코드는 스프링 프레임워크의 배포와 함께 제공되며 보일러플레이트 방식으로 사용될 수 있기 때문에 AOP 개념이 일반적으로 이 코드의 사용을 효과적으로 만들어준다고 이해할 필요는 없다. 


스프링 프레임워크의 선언적 트랜잭션 관리는 트랜잭션 동작을 독립적인 메서드 단위로 지정할 수 있다는 점에서 EJB CMT 와 유사하다. 필요하다면 setRollbackOnly() 메서드를 트랜잭션 컨텍스트 안에서 호출하는 일도 가능하다. 이 두 트랜잭션 관리의 차이는 다음과 같다.


  • EJB CMT 가 JTA 에 묶여 있었던 것과는 달리, 스프링 프레임워크의 선언적 트랜잭션 관리는 어느 환경에서나 유효하다. 간단히 설정을 바꾸는 것으로 JTA 트랜잭션, JDBC 를 이용한 로컬 트랜잭션, JPA, 하이버네이트 또는 JDO 등의 다양한 트랜잭션 환경에서 작동할 수 있다.
  • 스프링 프레임워크의 선언적 트랜잭션 관리는 어떤 클래스에나 적용할 수 있다. EJB 에서와 같은 특별한 클래스는 필요하지 않다.
  • 스프링 프레임워크는 EJB 에 존재하지 않는 선언적 롤백 규칙을 제공한다. 롤백 규칙은 프로그래밍 방식와 선언적 방식 모두 지원한다.
  • 스프링 프레임워크는 AOP 를 사용하여 트랜잭션 동작에 대한 커스터마이징을 지원한다. 가령 트랜잭션 롤백 수행 안에 특정한 동작을 지정할 수 있다. 트랜잭션 어드바이스와 함께 임의의 어드바이스를 추가할 수 있다. EJB CMT 를 사용하면 setRollbackOnly() 외에는 컨테이너의 트랜잭션 관리에 영향을 줄 수 없다.
  • 스프링 프레임워크는 고수준의 어플리케이션 서버에서 지원하는 원격 호출 간 트랜잭션 컨텍스트 전파를 지원하지 않는다. 이 기능을 원한다면 EJB 를 사용하길 권한다. 하지만 이런 기능을 사용하지 전에 신중히 검토해야 한다. 왜냐하면 보통은 트랜잭션이 원격 호출까지 이어지기를 원하지 않기 때문이다.

TransactionProxyFactoryBean 은 어디에 있는가?

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

스프링 2.0 전의 설정은 여전히 100% 유효하다. 새로운 <tx:tags/> 설정 대신 TransactionProxyFactoryBean 빈을 설정한다고 생각하면 된다.

롤백 규칙의 개념은 중요하다. 이 규칙은 어떤 예외 (그리고 throwables) 에서 자동 롤백이 수행될 것인지 지정하도록 한다. 이 규칙을 자바 코드가 아닌, 설정 안에 선언적으로 지정한다. 여전히 TransactionStatus 객체의 setRollbackOnly() 호출로 현재 트랜잭션을 롤백 할 수는 있지만 대부분은 MyApplicationException 가 던져졌을 때 항상, 반드시 롤백 하도록 지정할 수 있다. 이 옵션의 큰 장점은 비즈니스 객체가 트랜잭션 인프라에 의존하지 않는다는 것이다. 예를 들어, 비즈니스 클래스는 스프링 트랜잭션 API 나 기타 스프링 API 를 임포트 할 필요가 없다.


EJB 컨테이너가 기본적으로 시스템 예외 (주로 런타임 예외) 에 대한 트랜잭션 자동 롤백을 수행하긴 하지만, EJB CMT 는 어플리케이션 예외에 대해 자동 롤백을 수행하진 않는다 (java.rmi.RemoteException 을 제외한 checked 예외들). 스프링의 선언적 트랜잭션 관리의 기본 동작은 EJB 의 관습을 따르기 때문에 (자동 롤백은 unchecked 예외에 대해서만 수행), 롤백 동작 커스터마이징은 유용하게 사용된다.



스프링 프레임워크의 선언적 트랜잭션 구현체의 이해


@Transaction 와 @EnableTransactionManagement 어노테이션을 추가하는 것 만으로 트랜잭션이 어떻게 작동하는지 이해하기를 바랄 순 없다. 이 섹션은 트랜잭션 관련 문제가 발생할 경우 스프링 프레임워크의 선언적 트랜잭션 인프라가 내부적으로 어떻게 작동하는지 설명한다.


스프링 프레임워크의 선언적 트랜잭션 지원에 관한 가장 중요한 개념은 이 기능이 AOP 프록시에 의해 지원되며, 트랜잭션 어드바이스는 메타데이터 (현재 XML 또는 어노테이션 기반) 에 의해 작동한다는 것이다. AOP 와 트랜잭션 메타데이터의 결합은 AOP 프록시를 생성하는데, 이 프록시는 적절한 PlatformTransactionManager 구현체와TransactionInterceptor 를 함께 사용하여 메서드 호출 중심으로 트랜잭션을 구동한다.


개념상으로 트랜잭션 프록시 메서드 호출은 다음과 같다.




선언적 트랜잭션 구현체 예제


다음 인터페이스와 그 구현체를 보자. 이 예제는 특정 도메인 모델에 초점을 두지 않고 트랜잭션 사용법에 집중하기 위해 Foo 와 Bar 클래스를 플레이스홀더로 사용한다. 이 예제의 목적에 부합하기 위해 DefaultFooService 클래스가 각 구현 메서드에서 UnsupportedOperationException 인스턴스를 던지는 것이 좋다. 트랜잭션이 생성되고 UnsupportedOperationException 에 대해 롤백을 수행하는 것을 볼 수 있다.


// the service interface that we want to make transactional


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


}


// an implementation of the above interface


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) 는 반드시 읽기 쓰기 트랜잭션 컨텍스트에서 실행되어야 한다고 가정한다. 아래 설정은 다음 절에서 자세하게 설명한다.


<!-- from the file '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.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!-- this is the service object that we want to make transactional -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->

    <tx:advice id="txAdvice" transaction-manager="txManager">

        <!-- the transactional semantics... -->

        <tx:attributes>

            <!-- all methods starting with 'get' are read-only -->

            <tx:method name="get*" read-only="true"/>

            <!-- other methods use the default transaction settings (see below) -->

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- ensure that the above transactional advice runs for any execution

        of an operation defined by the FooService interface -->

    <aop:config>

        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>

        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>

    </aop:config>


    <!-- don't forget the 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>


    <!-- similarly, don't forget the PlatformTransactionManager -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"/>

    </bean>


    <!-- other <bean/> definitions here -->


</beans>


위 설정을 보자. 트랜잭션 서비스 객체인 fooService 빈을 만든다. 트랜잭션 적용 사항은 <tx:advice/> 정의에 캡슐화된다. <tx:advice/> 정의는 "get 으로 시작하는 모든 메서드는 읽기 전용 컨텍스트로 실행되고, 다른 모든 메서드는 기본 트랜잭션으로 실행된다" 를 의미한다. transaction-manager 속성은 트랜잭션을 구동하는 PlatformTransactionManager 빈의 이름을 설정한다. 여기서는 txManager 빈이 그것이다.


PlatformTransactionManager 빈의 이름이 transactionManager 라면 <tx:advice/> 의 transaction-manager 속성 설정을 생략할 수 있다. PlatformTransactionManager 빈이 다른 이름을 가질 경우 예제와 같이 transaction-manager 속성을 반드시 설정해야 한다.


<aop:config/> 정의는 txAdvice 빈에서 정의한 트랜잭션이 프로그램의 적절한 지점에서 실행되도록 보장한다. 먼저 FooService 인터페이스 (fooServiceOperation) 에 정의된 모든 메서드의 실행에 매치되는 포인트컷을 정의한다. 다음으로 advisor 를 이용해서 포인트컷과 txAdvice 를 연결한다. 이 설정은 fooServiceOperation 의 실행 시점에 txAdvice 에서 정의한 어드바이스가 구동됨을 의미한다.


공통 요건은 서비스 레이어 전체를 트랜잭션으로 묶는 것이다. 이를 위한 최선의 방법은 포인트컷 표현식을 모든 서비스 레이어에 매치되도록 변경하는 것이다. 예를 들자면 다음과 같다.


<aop:config>

    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>

    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>

</aop:config>


이제 설정을 분석했다. 이제 이 모든 설정이 실제로 무엇을 하는지 궁금할 것이다.


위 설정은 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 의 스택 트레이스. 간단히 보기 위해 중간에 잘랐음.)


<!-- the Spring container is starting up... -->

[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors


<!-- the DefaultFooService is actually proxied -->

[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]


<!-- ... the insertFoo(..) method is now being invoked on the proxy -->

[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo


<!-- the transactional advice kicks in here... -->

[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]

[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction


<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->

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


<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->

[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 infrastructure stack trace elements removed for clarity -->

at $Proxy0.insertFoo(Unknown Source)

at Boot.main(Boot.java:11)



선언적 트랜잭션 롤백


이전 섹션에서 어플리케이션의 클래스, 주로 서비스 레이어 클래스에 적용하는 선언적 트랜잭션 설정 방법의 기초를 선보였다. 이 섹션에서는 간단한 선언적 방식으로 트랜잭션 롤백을 제어하는 방법에 대해 설명한다.


스프링 프레임워크의 트랜잭션 인프라에 트랜잭션이 롤백되어야 함을 알리는 좋은 방법은 현재 트랜잭션 컨텍스트에서 실행중인 코드에서 예외를 던지는 것이다. 스프링 프레임워크의 트랜잭션 인프라 코드는 호출 스택에 차오르는 처리되지 않은 예외를 잡아서 트랜잭션을 롤백할 것인지 결정한다.


기본 설정에 따르면, 스프링 프레임워크의 트랜잭션 인프라 코드는 오직 런타임, unchecked 예외만을 트랜잭션 롤백 대상으로 본다. RuntimeException 의 인스턴스나 이것의 서브클래스가 던져져야 한다는 뜻이다. (Error 또한 기본적으로 롤백 대상이다.) 트랜잭션 메서드에서 발생한 checked 예외는 기본 설정 상 롤백되지 않는다.


checked 예외를 포함하여 어떤 예외 타입에 대해 트랜잭션 롤백을 수행할지 정확히 설정할 수 있다. 다음 XML 스니핏은 어플리케이션에서 정의한 checked 예외 타임에 대한 롤백을 설정하는 방법을 보여준다.


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


프로그래밍 방식으로 롤백을 처리할 수도 있다. 이 방법은 아주 간단하긴 하지만 상당히 침습적으로, 어플리케이션 코드와 스프링 트랜잭션 인프라 코드의 결합도를 크게 높이게 된다.


public void resolvePosition() {

    try {

        // some business logic...

    } catch (NoProductInStockException ex) {

        // trigger rollback programmatically

        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    }

}


가능하면 선언적 롤백 설정을 사용하는 편이 좋다. 프로그래밍 방식 롤백은 반드시 필요한 경우에 사용하도록 한다. 이 방식은 깨끗한 POJO 기반 아키텍처를 구현할 때 뛰어난 사용법이 된다.



다른 빈에서의 다른 트랜잭션 설정


이런 시나리오를 생각해보자. 다수의 서비스 레이어 객체를 가지며 각각의 레이어 객체에 대해 서로 완전히 다른 트랜잭션 설정을 적용하고자 한다. 이런 설정은 개별적인 <aop:advisor/> 엘레멘트를 정의하여 다른 pointcut 과 advice-ref 속성 값을 설정하는 방법으로 가능하다.


비교해보자면, 먼저 모든 서비스 레이어 클래스는 루트 x.y.service 패키지에 정의되어 있다고 가정한다. 이 패키지 (혹은 서브패키지) 에 정의된, 이름이 Service 로 끝나는 모든 클래스 인스턴스 빈에 기본 트랜잭션 설정을 적용하기 위해 다음과 같이 작성한다.


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

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <aop:config>


        <aop:pointcut id="serviceOperation"

                expression="execution(* x.y.service..*Service.*(..))"/>


        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>


    </aop:config>


    <!-- these two beans will be transactional... -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>


    <!-- ... and these two beans won't -->

    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->

    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->


    <tx:advice id="txAdvice">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->


</beans>


다음으로 두 개의 서로 다른 빈에 각각 완전히 다른 트랜잭션 설정이 적용된 예제를 보자


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

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <aop:config>


        <aop:pointcut id="defaultServiceOperation"

                expression="execution(* x.y.service.*Service.*(..))"/>


        <aop:pointcut id="noTxServiceOperation"

                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>


        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>


        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>


    </aop:config>


    <!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- this bean will also be transactional, but with totally different transactional settings -->

    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>


    <tx:advice id="defaultTxAdvice">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <tx:advice id="noTxAdvice">

        <tx:attributes>

            <tx:method name="*" propagation="NEVER"/>

        </tx:attributes>

    </tx:advice>


    <!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->


</beans>



<tx:advice/> 설정


이 섹션은 <tx:advice/> 태그를 통한 다양한 트랜잭션 설정을 요약해서 설명한다. 기본 <tx:advice/> 설정은 다음과 같다.


  • 전파 설정은 REQUIRED 이다.
  • 격리 수준은 DEFAULT 이다.
  • 트랜잭션은 읽기/쓰기 이다.
  • 트랜잭션 타임아웃은 트랜잭션 기반 시스템의 기본 타임아웃 설정을 따르며, 타임아웃이 지원되지 않는다면 트랜잭션 타임아웃은 없다.
  • 모든 RuntimeException 예외에 대해서 롤백을 수행하며, checked Exception 에는 수행하지 않는다.

이 기본 설정은 변경할 수 있다. 아래의 표는 <tx:advice/> 와 <tx:attributes/> 태그 안에 위치한 <tx:method/> 태그의 다양한 속성을 정리한다.

<tx:method/> 설정

속성 

 필수 여부

 기본값

 설명

 name

 Y

 

트랜잭션 속성이 적용될 메서드 이름. 다수의 메서드에 같은 설정을 적용하기 위해 와일드카드 (*) 를 사용할 수 있다. 예로, get*, handle*, on*Event 등등

 propagation

 N

 REQUIRED

트랜잭션 전파 동작

 isolation

 N

 DEFAULT

트랜잭션 격리 수준

 timeout

 N

 -1

트랜잭션 타임아웃 값

 read-only

 N

 false

읽기 전용 트랜잭션인가?

 rollback-for

 N

 

롤백을 적용할 Exception(s). 콤마로 구분한다. 예로, com.foo.MyBusinessException,ServletException

 no-rollback-for

 N

 

롤백을 적용하지 않을 Exception(s). 콤마로 구분한다. 예로, com.foo.MyBusinessException,ServletException



@Transactional 사용하기


XML 기반 선언적 트랜잭션 설정법에 더해서 어노테이션 기반의 방법을 사용할 수 있다. 트랜잭션을 자바 소스코드에 직접 선언하면 트랜잭션 선언과 트랜잭션이 적용되는 코드가 훨씬 가깝게 된다. 트랜잭션이 적용되는 코드는 거의 항상 그런 방식으로 배포되기 때문에 지나친 결합에 의한 위험성은 크지 않다.


스프링의 어노테이션 대신 표준 javax.transaction.Transactional 어노테이션도 지원된다. 자세한 정보는 JTA 1.2 문서를 참조하라.


@Transactional 어노테이션은 사용하기 쉽다.


// the service class that we want to make transactional

@Transactional

public class DefaultFooService implements FooService {


    Foo getFoo(String fooName);


    Foo getFoo(String fooName, String barName);


    void insertFoo(Foo foo);


    void updateFoo(Foo foo);

}


위 POJO 가 스프링 IoC 컨테이너에 빈으로 정의되었다면 이 빈 인스턴스는 XML 설정을 한 줄 추가하는 것으로 트랜잭션 처리 대상이 된다.


<!-- from the file '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.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!-- this is the service object that we want to make transactional -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- enable the configuration of transactional behavior based on annotations -->

    <tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <!-- (this dependency is defined somewhere else) -->

        <property name="dataSource" ref="dataSource"/>

    </bean>


    <!-- other <bean/> definitions here -->


</beans>


<tx:annotation-driven/> 태그의 transaction-manager 속성도 PlatformTransactionManager 빈의 이름이 transactionManager 인 경우에 생략될 수 있다. 


자바 기반 설정을 사용한다면 @EnableTransactionManagement 어노테이션을 사용해서 동일한 효과를 얻을 수 있다. 간단히 이 어노테이션을 @Configuration 클래스에 추가하면 된다. 


메서드 가시성과 @Transactional


프록시를 사용할 때, @Transactional 어노테이션은 반드시 public 메서드에 적용되어야 한다. protected, private, package-visible 메서드에 @Transactional 어노테이션이 추가되면 에러가 발생하지는 않지만 해당 메서드에 트랜잭션 설정이 적용되지 않는다. non-public 메서드에 어노테이션을 적용하고 싶다면 어스팩트J 사용을 고려하라.


@Transactional 어노테이션은 인터페이스 정의, 인터페이스의 메서드, 클래스 정의, 클래스의 public 메서드에 적용할 수 있다. 하지만 @Transactional 어노테이션을 추가하는 것 만으로 트랜잭션을 활성화 할 수는 없다. @Transactional 어노테이션은 런타임 인프라가 @Transactional 을 인지하고 적절한 빈과 트랜잭션 동작을 설정하기 위해 사용되는 메타메이터가 된다. <tx:annotation-driven/> 엘레멘트가 트랜잭션 동작의 스위치가 된다.


@Transactional 어노테이션은 구체화된 클래스 (그리고 구체화된 클래스의 메서드) 에 적용할 것을 권한다. 인터페이스나 인터페이스의 메서드에 적용하는 것도 가능하긴 하지만, 이 때는 인터페이스 기반 프록시에서만 유효한 트랜잭션 설정이 된다. 자바 어노테이션은 인터페이스로부터 상속되지 않는다는 사실은, 클래스 기반 프록시 (proxy-target-class="true") 나 위빙 기반 어스팩트 (mode="aspectj") 를 사용할 경우 프록싱과 위빙 인프라가 트랜잭션 설정을 인식할 수 없다는 것을 의미하고, 객체는 트랜잭션 프록시로 래핑되지 않게 된다. 확실히 좋은 설정이 아니다.


기본 프록시 모드에서는 외부로부터의 프록시를 통한 메서드 호출만이 인터셉트된다. 한 객체의 메서드에서 같은 객체 (자신) 의 다른 메서드를 호출할 경우, 이 메서드에 @Transactional 어노테이션이 적용되어 있다 할지라도 실제 트랜잭션 처리는 적용되지 않는다. 또한 프록시가 원하는 대로 작동하기 위해서는 반드시 완전한 초기화가 이루어져야 하므로 초기화 코드 (@PostConstructor) 에 이 기능을 사용하지 않아야 한다.


객체 자신에 대한 호출에도 트랜잭션을 적용하려면 어스팩트J 모드 사용을 고려해야 한다. 이 경우 처음에는 프록시가 없다. 대신 @Transactional 을 모든 종류의 런타임 동작으로 바꾸기 위해서 타겟 클래스를 위빙한다 (클래스의 바이트코드가 수정된다.)


어노테이션 기반 트랜잭션 세팅

XML 속성

 어노테이션 속성

 기본값

 설명

 transaction-manager

 N/A

 transactionManager

 사용할 트랜잭션 매니저의 이름. 이름이 transactionManager 가 아닌 경우에만 필수이다.

 mode

 mode

 proxy

 기본 모드인 "proxy" 는 어노테이션이 적용된, 스프링의 AOP 프레임워크를 사용하여 프록시되기 위한 빈을 처리한다. (여기서의 프록시는 언급한대로 프록시를 통한 외부 메서드 호출에만 적용된다.) 다른 모드인 "aspectj" 는 대상 클래스를 스프링의 어스팩트J 트랜잭션 어스팩트로 위빙하는데, 클래스의 바이트코드를 수정하여 모든 종류의 메서드 호출에 트랜잭션 코드를 적용한다. 어스팩트J 로드타임 위빙이나 컴파일타임 위빙에는 spring-aspects.jar 가 필요하다.

 proxy-target-class

 proxyTargetClass

 false

 프록시 모드를 적용한다. @Transactional 어노테이션이 적용된 클래스에 어떤 타입의 트랜잭션 프록시를 생성할 것인지 설정한다. proxy-target-class 가 true 라면 클래스 기반 프록시가 생성된다. false (또는 생략)라면 표준 JDK 인터페이스 기반 프록시가 생성된다.

 order

 order

 Ordered.LOWEST_PRECEDENCE

@Transactional 어노테이션이 적용된 빈에 적용될 트랜잭션 어드바이스의 순서를 정의한다. 따로 지정하지 않으면 AOP 서브시스템이 순서를 정한다.


@EnableTransactionManagement 와 <tx:annotation-driven/> 은 정의되어 있는 어플리케이션 컨텍스트 안에서 빈의 @Transactional 을 찾는다. 이 설정을 DispatcherServlet 에 대한 WebApplicationContext 에 적용한다면 서비스가 아닌 컨트롤러 안의 빈만을 찾는다.


메서드의 트랜잭션 설정을 평가할 때는 가장 많이 얻어지는 위치가 우선권을 갖는다. 다음 예제의 DefaultFooService 클래스에는 읽기 전용 트랜잭션 설정이 클래스 레벨 어노테이션으로 지정되었다. 하지만 같은 클래스에 있는 updateFoo(Foo) 메서드의 @Transactional 어노테이션의 트랜잭션 설정이 클래스 레벨 어노테이션보다 우선한다.


@Transactional(readOnly = true)

public class DefaultFooService implements FooService {


    public Foo getFoo(String fooName) {

        // do something

    }


    // these settings have precedence for this method

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)

    public void updateFoo(Foo foo) {

        // do something

    }

}



@Transactional 설정


@Transactional 어노테이션은 인터페이스, 클래스, 메소드 등에게 반드시 트랜잭션 처리를 하도록 지정하는 메타데이터이다. 예를 들어, "이 메소드가 실행되면 모든 기존 트랜잭션을 중단하고 새로운 읽기 전용 트랜잭션을 시작한다." 와 같은 의미를 가질 수 있다. @Transactional 설정의 기본 설정은 다음과 같다.


  • 전파 설정은 PROPAGATION_REQUIRED.
  • 격리 수준은 ISOLATION_DEFAULT.
  • 읽기/쓰기 트랜잭션
  • 타임아웃은 기반 트랜잭션 시스템의 기본 설정을 따른다. 타임아웃이 지원되지 않는다면 타임아웃은 없다.
  • 모든 RuntimeException 에 대해 롤백을 수행하고, checked Exception 에 대해서는 수행하지 않는다.

이 기본 설정은 변경될 수 있다. @Transactional 어노테이션의 다양한 프로퍼티 설정에 대해서는 아래 요약되어 있다.


@Transactional 설정

 프로퍼티

 타입

 설명

 value

 String 

 사용할 트랜잭션 관리자를 지정하는 선택적 구분자

 propagation

 enum: Propagation

 선택적 전파 설정.

 isolation

 enum: Isolation

 선택적 격리 수준.

 readOnly

 boolean

 읽기/쓰기 vs 읽기 전용 트랜잭션.

 timeout

 int (초)

 트잭션 타임아웃.

 rollbackFor

 Throwable 로부터 얻을 수 있는 Class 객체 배열

 롤백이 수행되어야 하는, 선택적인 예외 클래스의 배열.

 rollbackForClassName

 Throwable 로부터 얻을 수 있는 클래스 이름 배열

 롤백이 수행되어야 하는, 선택적인 예외 클래스 이름의 배열.

 noRollbackFor

 Throwable 로부터 얻을 수 있는 Class 객체 배열

 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스의 배열.

 noRollbackForClassName

 Throwable 로부터 얻을 수 있는 클래스 이름 배열

 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스 이름의 배열.


현재는 트랜잭션의 이름을 명시적으로 설정할 수 없다. 여기서 트랜잭션의 '이름' 이란 트랜잭션 모니터와 (적용 가능한 경우) 로깅 출력에 표시되는 트랜잭션 이름을 의미한다. 선언적 트랜잭션에서 트랜잭션 이름은 항상 완전한 클래스 이름 + "." + 트랜잭션 처리 메서드 이름이 된다. 예를 들어, BusinessService 클래스의 handlePayment(..) 메서드에서 트랜잭션이 시작된다면 트랜잭션 이름은 com.foo.BusinessService.handlePayment 가 된다.



@Transactional 을 이용한 다중 트랜잭션 매니저


스프링 어플리케이션은 대부분 단인 트랜잭션 매니저로 구동되지만, 다수의 독립된 트랜잭션 매니저가 필요한 상황도 있다. @Transacional 어노테이션의 value 속성은 사용할 PlatformTransactionManager 를 선택적으로 지정할 수 있다. 이 값은 빈 이름이나 트랜잭션 매니저 빈의 qualifier 값이 지정될 수 있다. 예를 들어 qualifier 표기법을 사용한다면 자바 코드는 다음과 같다.


public class TransactionalService {


    @Transactional("order")

    public void setSomething(String name) { ... }


    @Transactional("account")

    public void doSomething() { ... }

}


위 자바 코드는 어플리케이션 컨텍스트의 다음 트랜잭션 매니저 빈 선언과 함께 설정된다.


<tx:annotation-driven/>


    <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        ...

        <qualifier value="order"/>

    </bean>


    <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        ...

        <qualifier value="account"/>

    </bean>


여기서 TransactionalService 의 두 메서드는 각각의 트랜잭션 매니저로 실행된다. 각 트랜잭션 매니저는 "order" 와 "account" 구분자로 구분된다. 기본 <tx:annotation-driven> 대상 빈 이름으로는 여전히 transactionManager 가 사용된다.



커스텀 숏컷 어노테이션


여러 메서드들에 같은 속성이 지정된 @Transactional 어노테이션을 사용하는 일이 많아진다면 스프링의 메타 어노테이션 지원의 커스텀 숏컷 어노테이션이 유용하게 사용될 수 있다. 예제로 다음 어노테이션을 보자.


@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Transactional("order")

public @interface OrderTx {

}


@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Transactional("account")

public @interface AccountTx {

}


그리고 위 어노테이션은 다음과 같이 사용한다.


public class TransactionalService {


    @OrderTx

    public void setSomething(String name) { ... }


    @AccountTx

    public void doSomething() { ... }

}


여기서는 트랜잭션 매니저 구분자를 예로 들었지만, 트랜잭션 전파, 롤백 규칙, 타임아웃 등 다른 설정도 이렇게 사용할 수 있다.



트랜잭션 전파


이 섹션은 스프링의 트랜잭션 전파의 의미를 설명한다. 이 섹션은 트랜잭션 전파에 대한 소개가 아니다. 스프링에서의 트랜잭션 전파에 관하여 자세히 설명한다.


스프링 관리 트랜잭션에서는 물리적 트랜잭션과 논리적 트랜잭션의 차이와 트랜잭션 전파 설정이 이 차이를 어떻게 적용하는지 알아야 한다.



Required



PROPAGATION_REQUIRED


전파 설정이 PROPAGATION_REQUIRED 일 때는 각 트랜잭션 처리 대상 메서드에 논리적 트랜잭션 범위가 생성된다. 이 논리적 트랜잭션 범위는 롤백 전용 상태를 개별적으로 결정할 수 있으며, 외부 트랜잭션 범위는 내부 트랜잭션 범위와 논리적으로 독립적이다. 물론 표준 PROPAGATION_REQUIRED 동작의 경우 이런 모든 범위는 동일한 물리적 트랜잭션으로 매핑된다. 따라서 내부 트랜잭션 범위에 설정된 롤백 전용 표시는 외부 트랜잭션의 실제 커밋에 영향을 준다.


하지만 내부 트랜잭션 범위에 롤백 전용이 설정된 경우에는 외부 트랜잭션은 롤백 여부를 스스로 결정하지 않는다. 때문에 (내부 트랜잭션 범위에 의해 자동적으로 발생한) 롤백은 예상치 못하게 발생하게 된다. 이 시점에 여기에 대응하는 UnexpectedRollbackException 이 발생한다. 이 예외는 트랜잭션 호출자로 하여금 트랜잭션이 실제로 커밋 되지 않았음에도 커밋이 된 것으로 오인하는 것을 방지하기 위한 예상된 동작이다. 그래서 내부 트랜잭션이 (외부 트랜잭션 모르게) 트랜잭션을 롤백 전용으로 표시하면 외부 호출자는 커밋을 호출한다. 외부 호출자는 UnexpectedRollbackException 을 받아서 롤백이 대신 수행되었음을 명확하게 나타내야 한다.



RequiresNew



PROPAGATION_REQUIRES_NEW


PROPAGATION_REQUIRES_NEW 는 PROPAGATION_REQUIRED 와 대조적으로, 각 트랜잭션 범위에 대해 완전하게 독립된 트랜잭션을 사용한다. 기반 물리적 트랜잭션이 서로 다르며, 때문에 독립적인 커밋이나 롤백을 수행할 수 있다. 외부 트랜잭션은 내부 트랜잭션의 롤백 상태의 영향을 받지 않는다.



Nested


PROPAGATION_NESTED 는 롤백 가능한 다수의 세이브포인트를 가진 단일 물리적 트랜잭션을 사용한다. 이런 부분적 롤백은 내부 트랜잭션 범위가 자신의 범위에 대한 롤백을 수행할 수 있도록 한다. 외부 트랜잭션은 몇몇 작업이 롤백되어도 물리적 트랜잭션을 지속할 수 있다. 이 설정은 보통 JDBC 세이브포인트에 매핑된다. 때문에 JDBC 리소스 트랜잭션에서만 유효하다.



트랜잭션 작업 조언


트랜잭션 및 기본 프로파일링 어드바이스를 모두 실행한다고 가정한다. <tx:annotation-driven/> 의 컨텍스트 안에서 이것을 어떻게 적용할까?


updateFoo(Foo) 메서드를 실행할 때 다음 동작들을 보고자 한다.


  • 프로파일링 어스팩트 시작

  • 트랜잭션 어드바이스 실행

  • 어드바이스된 객체의 메서드 실행

  • 트랜잭션 커밋

  • 프로파일링 어스팩트의 정확한 전체 트랜잭션 메서드 실행 시간 보고


이 챕터에서는 AOP 의 자세한 설명을 고려하지 않았다. AOP 설정에 대한 자세한 정보는 AOP 챕터를 참조하기 바란다. 


아래는 위에서 언급한 간단한 프로파일링 어스팩트 코드다. 어드바이스 순서는 Ordered 인터페이스를 통해 설정되었다. 


package x.y;


import org.aspectj.lang.ProceedingJoinPoint;

import org.springframework.util.StopWatch;

import org.springframework.core.Ordered;


public class SimpleProfiler implements Ordered {


    private int order;


    // allows us to control the ordering of advice

    public int getOrder() {

        return this.order;

    }


    public void setOrder(int order) {

        this.order = order;

    }


    // this method is the around advice

    public Object profile(ProceedingJoinPoint call) throws Throwable {

        Object returnValue;

        StopWatch clock = new StopWatch(getClass().getName());

        try {

            clock.start(call.toShortString());

            returnValue = call.proceed();

        } finally {

            clock.stop();

            System.out.println(clock.prettyPrint());

        }

        return returnValue;

    }

}


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

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- this is the aspect -->

    <bean id="profiler" class="x.y.SimpleProfiler">

        <!-- execute before the transactional advice (hence the lower order number) -->

        <property name="order" __value="1"__/>

    </bean>


    <tx:annotation-driven transaction-manager="txManager" __order="200"__/>


    <aop:config>

            <!-- this advice will execute around the transactional advice -->

            <aop:aspect id="profilingAspect" ref="profiler">

                <aop:pointcut id="serviceMethodWithReturnValue"

                        expression="execution(!void x.y..*Service.*(..))"/>

                <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>

            </aop:aspect>

    </aop:config>


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


    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"/>

    </bean>


</beans>


위 설정은 fooService 빈에 프로파일링과 트랜잭션 어스팩트를 원하는 순서대로 적용한다. 이런 방식으로 몇 개의 어스팩트든 추가로 설정할 수 있다.


다음 예제는 위와 같은 설정을 적용하지만 순수 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.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- the profiling advice -->

    <bean id="profiler" class="x.y.SimpleProfiler">

        <!-- execute before the transactional advice (hence the lower order number) -->

        __<property name="order" value="1__"/>

    </bean>


    <aop:config>

        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>

        <!-- will execute after the profiling advice (c.f. the order attribute) -->


        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" __order="2__"/>

        <!-- order value is higher than the profiling aspect -->


        <aop:aspect id="profilingAspect" ref="profiler">

            <aop:pointcut id="serviceMethodWithReturnValue"

                    expression="execution(!void x.y..*Service.*(..))"/>

            <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>

        </aop:aspect>


    </aop:config>


    <tx:advice id="txAdvice" transaction-manager="txManager">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- other <bean/> definitions such as a DataSource and a PlatformTransactionManager here -->


</beans>


위 설정은 fooService 빈에 프로파일링과 트랜잭션 어스팩트를 지정된 순서로 적용한다. 프로파일링 어드바이스를 트랜잭션 어드바이스 다음 혹은 이전에 실행하고 싶다면 간단히 order 프로퍼티 값을 바꾸도록 한다.



@Transactional 과 어스팩트J 사용하기


어스팩트J 를 사용해 스프링 프레임워크의 @Transactional 지원을 스프링 컨테이너 외부에서 사용할 수 있다. 먼저 클래스에 (또는 클래스의 메서드에) @Transactional 어노테이션을 설정하고 spring-aspects.jar 에 정의된 org.springframework.transaction.AnnotationTransactionAspect 를 통해 어플리케이션으로 위빙한다. 어스팩트는 반드시 트랜잭션 매니저로 설정되어야 한다. 물론 스프링 프레임워크의 IoC 컨테이너로 어스팩트의 DI 를 제어할 수 있다. 트랜잭션 관리 어스팩트를 설정하는 가장 간단한 방법은 <tx:annotation-driven/> 엘레멘트를 사용하고 mode 속성을 aspectj 로 지정하는 것이다. 여기서는 스프링 컨테이너 외부에서 구동되는 어플리케이션에 포커스를 두기 때문에 프로그래밍 방식으로 보여준다.


// construct an appropriate transaction manager

DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());


// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods

AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);


이 어스팩트를 사용할 때는 반드시 인터페이스가 아닌, 구체화된 클래스 (또는 구체화된 클래스의 메서드) 에 어노테이션을 설정해야 한다. 어스팩트J 는 어노테이션은 인터페이스로부터 상속되지 않는다는 자바의 원칙을 따른다.


클래스의 @Transactional 어노테이션은 클래스의 모든 메서드의 실행에 대한 기본 트랜잭션 처리를 적용한다.


메서드의 @Transactional 어노테이션은 클래스 어노테이션에 의해 주어진 기본 트랜잭션 처리를 적용한다. 메서드 가시성에 상관없이 모든 메서드에 적용 가능하다.


어플리케이션을 AnnotationTransactionAspect 로 위빙하기 위해서는 어플리케이션을 반드시 어스팩트J 와 함께 빌드하거나 로드 타임 위빙을 사용해야 한다. 



프로그래밍 방식 트랜잭션 관리


스프링 프레임워크가 제공하는 프로그래밍 방식 트랜잭션 관리는 두 가지 의미가 있다.


  • TransactionTemplate 사용
  • PlatformTransactionManager 구현체 직접 사용

스프링 팀은 프로그래밍 방식 트랜잭션 관리에 있어 TransactionTemplate 사용을 권장한다. 두 번째 방식은 예외 핸들링이 더 가볍다는 점 외엔 JTA UserTransaction API 를 사용하는 것과 유사하다. 


TransactionTemplate 사용하기

TransactionTemplate 는 JdbcTemplate 와 같은 다른 스프링 템플릿과 같은 접근법을 취한다. 콜백 방식을 사용해서 트랜잭션 리소스 획득, 해제와 같은 보일러플레이트 코드를 어플리케이션 코드에서 분리하고 의도에 맞는, 오로지 개발자가 원하는 동작에 집중하도록 한다.

다음 예제에서 보듯이 TransactionTemplate 사용은 스프링의 트랜잭션 인프라와 API 에 전적으로 묶이게 된다. 프로그래밍 방식이 어플리케이션 개발의 요건에 맞는지 맞지 않는지는 스스로 결정해야 한다.


반드시 트랜잭션 컨텍스트에서 실행하면서 TransactionTemplate 을 사용하는 어플리케이션 코드는 다음과 같다. TransactionCallback 구현체는 (보통 익명 내부 클래스로) 트랜잭션 컨텍스트에서 실행할 코드를 작성하도록 한다. 그리고 커스텀 TransactionCallback 의 인스턴스를 TransactionTemplate 의 execute(..) 메서드로 전달한다.


public class SimpleService implements Service {


    // single TransactionTemplate shared amongst all methods in this instance

    private final TransactionTemplate transactionTemplate;


    // use constructor-injection to supply the PlatformTransactionManager

    public SimpleService(PlatformTransactionManager transactionManager) {

        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");

        this.transactionTemplate = new TransactionTemplate(transactionManager);

    }


    public Object someServiceMethod() {

        return transactionTemplate.execute(new TransactionCallback() {

            // the code in this method executes in a transactional context

            public Object doInTransaction(TransactionStatus status) {

                updateOperation1();

                return resultOfUpdateOperation2();

            }

        });

    }

}


반환값이 없다면 TransactionCallbackWithoutResult 클래스를 사용한다.


transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    protected void doInTransactionWithoutResult(TransactionStatus status) {

        updateOperation1();

        updateOperation2();

    }

});


콜백 안의 코드는 파라미터로 제공된 TransactionStatus 객체의 setRollbackOnly() 메서드를 호출하여 트랜잭션을 롤백할 수 있다.


transactionTemplate.execute(new TransactionCallbackWithoutResult() {


    protected void doInTransactionWithoutResult(TransactionStatus status) {

        try {

            updateOperation1();

            updateOperation2();

        } catch (SomeBusinessExeption ex) {

            status.setRollbackOnly();

        }

    }

});



트랜잭션 설정 지정하기


프로그래밍 방식이나 설정 방식으로 TransactionTemplate 의 전파, 격리 수준, 타임아웃과 같은 트랜잭션 설정을 지정할 수 있다. TransactionTemplate 인스턴스는 기본적으로 기본 트랜잭션 설정을 가진다. 다음 예제는 프로그래밍 방식으로 커스터마이징된 TransactionTemplate 의 트랜잭션 설정을 보여준다.


public class SimpleService implements Service {


    private final TransactionTemplate transactionTemplate;


    public SimpleService(PlatformTransactionManager transactionManager) {

        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");

        this.transactionTemplate = new TransactionTemplate(transactionManager);


        // the transaction settings can be set here explicitly if so desired

        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);

        this.transactionTemplate.setTimeout(30); // 30 seconds

        // and so forth...

    }

}


다음 예제는 스프링 XML 설정을 사용한 TransactionTemplate 의 트랜잭션 설정이다. sharedTransactionTemplate 은 필요에 따라 다수의 서비스로 주입 가능하다.


<bean id="sharedTransactionTemplate"

        class="org.springframework.transaction.support.TransactionTemplate">

    <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>

    <property name="timeout" value="30"/>

</bean>


마지막으로 TransactionTemplate 클래스의 인스턴스는 쓰레드세이프하다. 인스턴스 안에 어떠한 어플리케이션 작동 상태도 유지하지 않는다. TransactionTemplate 인스턴스는 설정 상태를 유지한다. 따라서 TransactionTemplate 인스턴스는 다수의 클래스에 공유될 수 있고, 만약 다르게 설정된 TransactionTemplate 이 필요하다면 두 개의 구분된 TransactionTemplate 를 생성해야 한다.



PlatformTransactionManager 사용하기


트랜잭션 관리를 위해 org.springframework.transaction.PlatformTransactionManager 를 직접 사용할 수도 있다. 간단하게 사용중인 PlatformTransactionManager 의 구현체를 빈에 넘기면 된다. 그리고 TransactionDefinition 과 TransactionStatus 객체를 사용해서 트랜잭션을 초기화하고 롤백하고 커밋할 수 있다.


DefaultTransactionDefinition def = new DefaultTransactionDefinition();

// explicitly setting the transaction name is something that can only be done programmatically

def.setName("SomeTxName");

def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);


TransactionStatus status = txManager.getTransaction(def);

try {

    // execute your business logic here

}

catch (MyException ex) {

    txManager.rollback(status);

    throw ex;

}

txManager.commit(status);



프로그래밍 방식 vs 선언적 방식


트랜잭션 작업이 많지 않다면 프로그래밍 방식은 대개 좋은 선택이 된다. 예를 들어, 트랜잭션 작업은 몇 개의 update 가 전부인 웹 어플레케이션을 개발한다면 스프링이나 다른 기술을 사용한 트랜잭션 프록시 설정을 원치 않을 수 있다. 이런 경우에 TransactionTemplate 은 좋은 처리 방법이다. 명시적으로 트랜잭션 이름을 세팅하는 일은 오직 프로그래밍 방식 트랜잭션 관리에서만 할 수 있는 일이기도 하다.


다른 한편으로는, 어플리케이션에 수많은 트랜잭션 작업이 필요하다면 일반적으로 선언적 트랜잭션 관리가 좋다. 이 방식은 트랜잭션 관리를 비즈니스 로직과 분리하며, 설정하기 어렵지 않다. 스프링 프레임워크를 사용하면 EJB CMT 를 사용할 때보다 선언적 트랜잭션 관리 설정 비용이 크게 줄어든다.



트랜잭션 바운드 이벤트


스프링 4.2 에서는 트랜잭션의 한 단계에 이벤트 리스너를 바인딩할 수 있다. 전형적인 예로는 트랜잭션이 성공적으로 완료됐을 때의 이벤트를 핸들링하는 것이다. 리스너에게 현재 트랜잭션의 결과가 중요한 경우, 이런 이벤트 처리가 더 유연하게 사용될 수 있다.


보통의 이벤트 리스너는 @EventListener 어노테이션을 통해 등록한다. 리스너를 트랜잭션에 바인딩하려면 @TransactionalEventListener 를 사용한다. 이 어노테이션을 사용하면 기본적으로 리스너는 트랜잭션의 커밋 단계에 바인딩된다.


이제 이벤트 바인딩 개념의 예제를 보자. 한 컴포넌트가 주문 생성 이벤트를 게시하고, 성공적으로 커밋된 트랜잭션 이벤트를 핸들링하는 리스너를 정의한다고 가정한다.


@Component

public class MyComponent {


    @TransactionalEventListener

    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {

          ...

    }

}


TransactionalEventListener 어노테이션은 트랜잭션의 어느 단계에 리스너를 바인딩할지 설정하는 phase 속성을 노출한다. 유효한 트랜잭션 단계는 BEFORE_COMMIT, AFTER_COMMIT (기본값), AFTER_ROLLBACK, 그리고 완료된 트랜잭션을 종합하는 AFTER_COMPLETION (커밋 또는 롤백) 이 있다.


구동중인 트랜잭션이 없다면 리스너는 실행되지 않는다. 하지만 어노테이션의 fallbackExecution 속성을 true 로 세팅하여 동작을 오버라이딩할 수 있다.



어플리케이션 서버 별 통합


스프링의 트랜잭션 추상화는 일반적으로 어플리케이션 서버에 무관심하다. 스프링의 JtaTransactionManager 클래스는 선택적으로 JTA UserTransaction 과 TransactionManager 객체에 대한 JNDI 룩업을 수행할 수 있다. JtaTransactionManager 클래스는 TransactionManager 객체를 자동으로 탐지하는데, 이 객체는 어플리케이션 서버마다 다르다. JTA TransactionManager 에 대한 접근 권한을 갖게되면 향상된 트랜잭션을 얻게 되는데, 특히 트랜잭션 일시 중단을 지원한다. 


스프링의 JtaTransactionManager 는 자바 EE 어플리케이션 서버 구동의 표준적인 선택이며 모든 공통된 서버에서 작동한다고 알려져 있다. 트랜잭션 일시 중단과 같은 발전된 기능은 GlassFish, JBoss, Geronimo 를 포함한 많은 서버에서 작동한다. 여기에 특별한 설정은 필요하지 않다. 하지만 완전하게 지원되는 트랜잭션 일시 중단과 더 발전된 통합을 위해 스프링은 WebLogic 과 WebSphere 서버를 위한 특별한 어댑터를 제공한다. 이 어댑터들에 대해서는 다음 섹션에서 다룬다.


WebLogic 과 WebSphere 서버를 포함한 표준적인 시나리오에서는 편리하게 <tx:jta-transaction-manager/> 설정 엘레멘트 사용을 고려하라. 이 엘레멘트를 설정하면 기반이 되는 서버를 자동으로 감지하고 플랫폼에서 유효한 최선희 트랜잭션 매니저를 선택한다. 이는 서버 별 어댑터 클래스 (다음 섹션에서 다루는) 를 명시적으로 설정할 필요가 없다는 뜻이다. 어댑터 클래스는 사용으로 선택된다. 기본적으로 표준 JtaTransactionManager 가 사용된다.



IBM WebSphere


WebSphere 6.1.0.9 와 그 위의 버전에서 권장되는 스프링 JTA 트랜잭션 매니저는 WebSphereUowTransactionManager 이다. 이 특별한 어댑터는 IBM 의 UOWManager API 를 사용한다. 이 API 는 WebSphere 어플리케이션 서버 6.1.0.9 와 이후 버전에서 유효하다. 이 어댑터를 사용하면 스프링 기반 트랜잭션 일시 중단 (PROPAGATION_REQUIRES_NEW 에 의한 중단/재개) 은 공식적으로 IBM 이 지원한다.



오라클 WebLogic 서버


WebLogic 서버 9.0 또는 그 위의 버전에서 일반적으로 WebLogicJtaTransactionManager 를 사용한다. 이 특별한 WebLogic 전용 클래스는 JtaTransactionManager 의 서브클래스이다. 이 클래스는 WebLogic 관리 트랜잭션 환경에서 표준 JTA 를 넘어서 스프링 트랜잭션 정의의 모든 기능을 지원한다. 지원되는 기능은 트랜잭션 이름, 트랜잭션 별 격리 수준, 그리고 모든 경우의 트랜잭션에서의 알맞은 재개를 포함한다.



일반적인 문제의 해결책


특정 DataSource 에 대한 잘못된 트랜잭션 관리자 사용


선택한 트랜잭션 기술과 요건에 맞는 알맞은 PlatformTransactionManager 구현체를 사용하라. 제대로 사용된 스프링 프레임워크는 간단하고 포터블한 추상화를 제공한다. 글로벌 트랜잭션을 사용한다면 반드시 org.springframework.transaction.jta.JtaTransactionManager 클래스 (또는 이 클래스의 어플리케이션 서버 별 서브클래스) 를 사용하라. 그렇지 않으면 트랜잭션 인프라는 컨테이너의 DataSource 인스턴스와 같은 리소스에 대해 로컬 트랜잭션을 수행한다. 이런 로컬 트랜잭션은 의미가 없으며, 좋은 어플리케이션 서버는 이를 오류로 처리한다.



추가 정보


스프링 프레임워크의 트랜잭션 지원에 대한 정보는 여기서 더 얻을 수 있다.


  • 스프링의 분산 트랜잭션 with or without XA 는 JavaWorld 에 기재된, 스프링의 David Syer 의 스프링 어플리케이션의 7 가지 분산 트랜잭션 패턴 가이드이다. 패턴 중 3 가지는 XA 를 사용하고, 4 가지는 사용하지 않는다.
  • 자바 트랜잭션 디자인 전략 은 InfoQ 에서 판매중인 책으로, 자바 트랜잭션에 대해 훌륭하게 소개한다. 또한 스프링 프레임워크과 EJB3 를 사용한 트랜잭션 설정 방법 예제도 포함되어 있다.






+ Recent posts