Spring Framework - 트랜잭션 관리

Spring의 Transaction management

트랜잭션에 대한 포괄적인(comprehensive) 지원은 스프링의 장점 중 하나이다. 스프링이 제공하는 트랜잭션 관리에는 다음과 같은 장점이 있다.

스프링 트랜잭션 모델의 장점

EJB의 CMT 또는 Hibernate와 같은 API를 사용하여 로컬 트랜잭션을 처리하는 방식보다 스프링 프레임워크의 transaction abstraction을 사용해야 하는 이유

Java EE 개발자들은 트랜잭션 관리에 대해 두 가지 선택사항이 있었다. Global 또는 Local 트랜잭션이다. 둘 다 상당한 한계가 있었다. 스프링 프레임워크의 트랜잭션 관리는 이 한계에 대해 다룬다.

Global transactions

한 개 이상의 리소스(영구 저장소)에 대한 작업을 트랜잭션으로 묶은 것을 전역 트랜잭션(global transaction)이라고 한다. JTA를 사용하여 전역 트랜잭션을 관리할 수 있지만 JTA는 무겁고 다루기 힘든 API이다. 뿐만 아니라 JTA를 사용하기 위해서는 JNDI를 사용해야 한다. 이런 방식으로 전역 트랜잭션을 사용하는 것은 명백히 코드의 재사용성을 낮춘다. JTA는 일반적으로 서버 애플리케이션 환경에서 사용 가능하기 때문이다.
이전에 전역 트랜잭션을 사용할때 선호되던 방식은 EJB CMT(Container Managed Transaction)를 통한 것이였다. CMT는 선언적 트랜잭션 관리(declarative transaction management) 형태이다. 이는 프로그래밍적 트랜잭션 관리(programmatic transaction management)와 구별되는 방식이다. EJB CMT는 트랜잭션 관련 JNDI 조회(lookup)는 제거했지만 EJB를 사용하기 위한 JNDI는 없애지 못했다. CMT의 가장 큰 단점은 JTA에 종속되어 있다는 점과 애플리케이션 서버 환경에서만 사용 가능하다는 점이다. 또한 비즈니스 로직이 EJB 코드로 작성되어야 한다는 단점도 있다.

Local transaction

로컬 트랜잭션은 하나의 리소스를 다룬다. 예를 들면 JDBC connection과 관련된 트랜잭션이다. 로컬 트랜잭션은 사용하기 쉽지만 치명적인 단점이 있다. 다양한 리소스(저장소)에 대한 트랜잭션을 관리할 수 없다는 점이다. 예를 들어 JDBC connection을 사용하여 트랜잭션을 관리하는 코드는 글로벌 JTA 트랜잭션에서 작동할 수 없다. 또다른 단점은 로컬 트랜잭션들은 프로그래밍 모델에 침투적(invasive)이라는 것이다.

Spring transaction abstraction

스프링은 기존의 global, local transaction의 문제점을 해결한다. 스프링은 개발자들이 어떤 환경에서든 일관된 프로그래밍 모델을 사용할 수 있도록 해준다. 한 번 코드를 작성하면 그것은 다른 환경의 다른 트랜잭션 관리 전략(strategies)에서도 사용될 수 있다. 스프링은 선언적 트랜잭션 관리와 프로그래밍적 관리를 모두 지원한다. 대부분의 사용자가 선언적 관리를 선호하며 그것은 일반적인 대부분의 상황에서 권장되는 방식이다.
Programmatic transaction management 방식을 사용하면 개발자가 직접 트랜잭션 관리 API(Spring Framework transaction abstraction)를 사용하여 트랜잭션을 관리하게 되지만 선호되는 방식인 declarative model을 사용하면 개발자는 일반적으로 트랜잭션 관리와 관련된 코드를 작성할 일이 없다. 결국 어떤 트랜잭션 관리 API에도 의존하지 않는다.

Programmatic transaction management는 아래와 같이 개발자가 코드를 통해 트랜잭션의 life cycle을 조작하는 방식이다.

  • JDBC를 사용하는 경우, Connection 객체의 setAutoCommit(false)를 통해 트랜잭션을 시작하고 commit 메서드를 호출하거나 rollback 메서드를 실행한다.
  • JTA의 UserTransaction 인터페이스를 사용해 분산 트랜잭션을 관리한다.
  • ORM 프레임워크(예를 들면 Hibernate)의 API를 사용한다.

트랜잭션의 관리를 개발자가 세밀하게 제어할 수 있지만 비즈니스 로직과 트랜잭션 관리 코드가 강하게 결합된다.

Declarative transaction management

대부분의 스프링 사용자들은 선언적 트랜잭션 관리 방식을 선택한다. 이 방식은 애플리케이션 코드에 미치는 영향이 작고 비침투적(non-invasive) 경량 컨테이너라는 이상에 가깝기 때문이다.

스프링의 선언적 트랜잭션 관리는 Spring AOP를 통해 구현되어있지만, 효과적으로 사용하기 위해 AOP 개념을 이해할 필요는 없다.

스프링의 선언적 트랜잭션 관리는 트랜잭션의 행동을 개별 메서드 수준까지 명시(specify)할 수 있다는 점에서 EJB CMT와 비슷하다. 필요하다면 트랜잭션 컨텍스트 내에서 setRollbackOnly() 메서드를 호출할 수 있다. EJBCMT와의 차이점은 다음과 같다.

  • JTA와 결합된 EJB CMT와 달리 스프링의 선언적 트랜잭션 관리는 어떠한 환경에서도 동작한다. JTA 트랜잭션 또는 JDBC, JPA 등을 사용하는 로컬 트랜잭션과 설정 파일 하나만 바꿔주면 함께 동작할 수 있다.
  • EJB와 같은 특별한 클래스가 아닌 어떠한 클래스에도 적용할 수 있다.
  • EJB에는 없는 선언적 rollback rule을 제공한다. 선언적 방식과 프로그래밍적 방식 모두에서 제공한다.
  • AOP를 이용하여 트랜잭션의 행동(transactional behavior)을 커스텀할 수 있다. 예를 들어 프로그래머는 롤백 상화에서 custom behavior를 추가할 수 있다. 또한 트랜잭션의 어드바이스에 원하는 어드바이스를 추가할 수 있다. EJB CMT에서는 setRollbackOnly()를 제외하면 컨테이너의 트랜잭션 관리에는 관여할 수 없다.
  • Spring Framework는 여러 원격 호출에 대한 트랜잭션 전파(propagation of transaction)는 지원하지 않는다. 분산 시스템들에 보내는 원격 호출을 묶는 트랜잭션을 구성하고싶다면 EJB를 사용해야 한다.

트랜잭션 전파

트랜잭션 전파란 실행중인 하나의 트랜잭션 내부에서 또 다른 트랜잭션이 실행될때의 동작을 결정하는 것이다. 이 새로운 트랜잭션을 독립적인 트랜잭션으로 취급할 것인지 또는 현재 트랜잭션의 일부로 취급할것인지 등을 결정한다.
Spring 프레임워크가 원격 호출(직접 DB와 통신하지 않고 다른 서버를 거치는 경우)에 대한 트랜잭션 전파가 지원하지 않는다는 것은 스프링 앱의 트랜잭션 컨텍스트가 원격 서비스까지 확장되지 않는다는 뜻이다. 즉, 스프링 애플리케이션 A가 다른 애플리케이션 B, C와 통신하는 메서드를 트랜잭션으로 묶었다고 하더라도 의미가 없다는 뜻이다.

롤백 규칙(rollback rule)은 중요한 개념이다. 어떤 Exception에서 자동적인 롤백이 실행되어야 하는지를 규정한다. 사용자는 자바 코드가 아니라 설정파일에 선언적으로 이것을 명시할 수 있다. setRollbackOnly() 메서드를 호출하여 특정 상황에서 현재 트랜잭션이 롤백되도록 설정할 수 있지만 그럴 경우 트랜잭션 관리 코드가 서비스 코드에 섞이게 된다. 선언적 트랜잭션 관리 방식을 적용하면 서비스 코드에는 예외를 던지는 코드까지만 포함되고 그것을 받아서 트랜잭션의 행동을 제어하는 로직은 설정 파일에 명시된다.
스프링의 기본값(default)는 unchecked exception에 대해 롤백이 수행되는 것이다.

Spring의 선언적 트랜잭션의 구현

AOP와 트랜잭션 관련 메타데이터(xml 설정 파일 등)의 조합으로 AOP 프록시가 생성된다. 이 프록시는 적절한 PlatformTransactionManager 구현체와 TransactionInterceptor를 함께 활용하여 메서드 호출 주변에서 트랜잭션을 관리한다.

개념적으로 transactional proxy에서 타겟 메서드가 호출되는 과정은 다음과 같다.

transactional proxy

Example of declarative transaction implementation

DefaultFooService 의 메서드들은 UnsupportedOperationException을 던진다. 이를 통해 트랜잭션이 생성되고 예외에 대응하여 롤백되는 과정을 볼 수 있다.

// the service interface that we want to make trasactional

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>

태그별로 설정하고 있는 내용은 다음과 같다.

  • - 트랜잭션의 advice(트랜잭션 프록시가 **할 일**에 대해 설정)
    • - 특정 메서드(이름이 get으로 시작하는)는 read-only 권한만 가지고 나머지 메서드들은 기본(default) 설정을 유지한다.
  • - 프록시의 pointcut과 advice를 연결하여 Advisor를 구성한다.
  • - 스프링 컨테이너가 관리할 객체들에 대한 정보를 컨테이너에게 넘겨준다.

위의 xml 설정 파일은 Java Configuration으로 변환할 수 있다.

@Configuration
@EnableTransactionManagement
public class AppConfig {

    @Bean
    public FooService fooService() {
        return new DefaultFooService();
    }

    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource.setUrl("jdbc:oracle:thin:@rj-t42:1521:elvis");
        dataSource.setUsername("scott");
        dataSource.setPassword("tiger");
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager txManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean
    public TransactionInterceptor txAdvice() {
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        RuleBasedTransactionAttribute readOnly = new RuleBasedTransactionAttribute();
        readOnly.setReadOnly(true);
        RuleBasedTransactionAttribute transactional = new RuleBasedTransactionAttribute();
        source.addTransactionalMethod("get*", readOnly);
        source.addTransactionalMethod("*", transactional);
        return new TransactionInterceptor(txManager(), source);
    }

    @Bean
    public Advisor txAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* x.y.service.FooService.*(..))");
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

@EnableTransactionManagement 어노테이션은 스프링의 선언적 트랜잭션 관리 기능을 활성화한다.

transaction semantics

Transaction semantics는 트랜잭션을 어떻게 다룰지, 어떤 시나리오에서 롤백할지, 어떻게 커밋할지, 어떻게 고립성을 유지할지 등에 대한 기준이다.

reference

[Spring.io] Transaction management

Comments