transaction ; 트랜잭션

컴퓨터 프로그램에서 트랜잭션의 일반적인 의미는 정보의 교환이나 데이터베이스 갱신 등 연관되는 작업들에 대한 일련의 연속을 의미하는데, 데이터베이스의 무결성이 보장되는 상태에서 요청된 작업을 완수하기 위한 작업의 기본 단위로 간주된다.

전형적인 트랜잭션의 예로, 고객의 전화 주문을 받아 대리인이 주문내용을 컴퓨터에 입력하는 것을 들 수 있는데, 이 주문 트랜잭션은 다음과 같은 여러 개의 작업단계로 이루어진다.

  • 데이터베이스로부터 재고량 조사하기
  • 그 상품이 가용한지(혹시, 다른 고객으로부터 예약된 것인지의 여부) 확인하기
  • 주문하기
  • 주문이 이루어졌는지 확인하기
  • 예상 선적시간 확인하기

위의 작업단계를 하나의 트랜잭션으로 보았을 때, 트랜잭션이 성공적으로 끝나기 위해서는 각 작업 단계들이 모두 완성되어야만 하며, 그랬을경우 비로소 이 새로운 주문 내용이 데이터베이스에 실제로 반영된다. 만약 그렇지 못했을 경우, 즉 어떤 한 작업 단계에서라도 오류가 발생하면 데이터베이스에는 아무런 수정이 이루어지지 않으며, 트랜잭션이 시작되기 이전 상태로 유지된다. 트랜잭션이 성공리에 끝났을때 이루어지는 데이터베이스의 갱신을 "commit" 이라고 부르며, 트랜잭션이 실패되었을때 데이터베이스의 수정 내용이 취소되는 것을 "rollback"이라고 부른다. 트랜잭션의 각 사건들을 관리 감독하는 프로그램을 트랜잭션 모니터라고 하며, 트랜잭션은 SQL에 의해 제공된다.


가끔 어떤 컴퓨터에서 트랜잭션이라는 용어는 다른 의미를 갖는다. 예를 들어 IBM 대형기종 운영체계의 배치 처리에서, 트랜잭션은 작업(job) 또는 작업단계(job step)를 의미한다.


출처 - terms.co.kr






[Spring] TransactionManager와 DataSource


들어가며


Spring에서 트랜잭션을 적용하는 방법은 여러가지가 있지만, 보통 선언적 트랜잭션(declarative transaction)을 많이 사용한다. 필자도 XML 설정을 통해 어플리케이션의 상태를 변경하도록 하는 선언적 접근방식을 좋아하는 편이라 선언적 트랜잭션을 사용하고 있다. 스프링에서의 트랜잭션 적용 방법에 대해서는 여타 다른 기본서들이나 spring 문서에도 잘 나와있으므로 여기에서 설명하지는 않는다. 이 글에서는 필자의 경험을 예로 들어 트랜잭션 매니저의 역할에 대한 이해를 돕고, Propagation에 대해 간략하게 설명하도록 한다.



스프링 TransactionManager

스프링에서는 다양한 TransactionManager를 제공하는데 보통 일반적인 웹어플리케이션에서는 DataSourceTransactionManager를 많이 사용할 것이다. 

(사실 스프링에서 사용하는 트랜잭션 매니저의 동작을 이해하기란 쉽지 않다. 아래 그다지 간단하지 않은 트랜잭션 설정을 예제로 포함했지만, 트랜잭션 적용 설정을 설명하고자 함이 아니기 때문에 아래 설정 코드는 굳이 볼 필요는 없다.)

접기

    <!-- Data Source -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName">
            <value>${dataSource.driverClassName}</value>
        </property>
        <property name="url">
            <value>${dataSource.url}</value>
        </property>
        <property name="username">
            <value>${dataSource.username}</value>
        </property>
        <property name="password">
            <value>${dataSource.password}</value>
        </property>
        <property name="maxActive">
            <value>${dataSource.maxActive}</value>
        </property>
        <property name="maxIdle">
            <value>${dataSource.maxIdle}</value>
        </property>
        <property name="maxWait">
            <value>${dataSource.maxWait}</value>
        </property>
        <property name="defaultAutoCommit">
            <value>true</value>
        </property>
        <property name="validationQuery">
            <value>${dataSource.validationQuery}</value>
        </property>
        <property name="testOnBorrow">
            <value>${dataSource.testOnBorrow}</value>
        </property>
        <property name="testWhileIdle">
            <value>${dataSource.testWhileIdle}</value>
        </property>
        <property name="timeBetweenEvictionRunsMillis">
            <value>${dataSource.timeBetweenEvictionRunsMillis}</value>
        </property>
        <property name="minEvictableIdleTimeMillis">
            <value>${dataSource.minEvictableIdleTimeMillis}</value>
        </property>
    </bean>
    
    <!-- SqlMapClient -->
    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="configLocation" value="/WEB-INF/conf/ibatis/sqlmap-config.xml" />
        <property name="dataSource" ref="dataSource" />
    </bean>
    <bean id="sqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">
        <property name="sqlMapClient" ref="sqlMapClient" />
    </bean>
    
    <!-- TrasactionManager -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- auto name proxy for DataManager Transaction -->
    <bean id="nameMatchAS"class="org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
        <property name="properties">
            <props>
                <prop key="*">PROPAGATION_REQUIRED,
                    -RuntimeException, -Exception</prop>
            </props>
        </property>
    </bean>

    <!-- TransactionInterceptor -->
    <bean id="transactionInterceptor"class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager">
            <ref bean="transactionManager" />
        </property>
        <property name="transactionAttributeSource">
            <ref bean="nameMatchAS" />
        </property>
    </bean>
    <bean id="autoProxyCreator"class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="interceptorNames">
            <list>
                <idref bean="transactionInterceptor" />
            </list>
        </property>
        <property name="beanNames">
            <value>*Service</value>
        </property>
    </bean>

보기만해도 어지럽다. 스프링 설정의 큰 특징은 객체간의 의존관계가 명확하게 드러난다는 점이다. 물론 모든 경우가 다 그런것을 아니지만 일반적으로 빈 A가 빈 B의 parent로 설정되어 있다면 B는 A를 상속한 클래스일 것이다. 

하지만 위의 설정은 객체간의 관계를 전혀 알 수 없다. 대표적으로 실제로 SQL구분을 수행하는 매퍼인 iBatis의 sqlMapClient는 DataSource를 참조하고 있고, TransactionManager 설정도 DataSource를 참조하고 있다.

접기



보통 트랜잭션 설정을 하게 되면 DataSource를 선언하고 DataSourceTransactionManager를 선언하게 된다. 그리고 트랜잭션 매니저를 AOP 방식으로 특정 빈의 메소드에 반영하도록 하는 설정이 일반적이다.

그렇다면 트랜잭션 매니저를 사용할 때 DataSource (또는 Connection)의 defaultAutoCommit (또는 autoCommit) 설정을 false로 해놓아야 할까?


DataSourceTransactionManager의 내부를 보면 다음과 같은 코드를 발견할 수 있다.

접기

protected void doBegin(Object transaction, TransactionDefinition definition) {
    ....
    // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
    // so we don't want to do it unnecessarily (for example if we've explicitly
    // configured the connection pool to set it already).
    if (con.getAutoCommit()) {
        txObject.setMustRestoreAutoCommit(true);
        if (logger.isDebugEnabled()) {
            logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
        }
        con.setAutoCommit(false);
    }
    ...

접기


위 코드에서도 알 수 있듯이, 트랜잭션 매니저 내부에서 Connection의 autoCommit 설정이 true로 넘어오더라도 강제로 false로 세팅한다. 따라서 DataSource에서 defaultAutoCommit 설정을 true로 놓더라도 별다른 문제는 생기지 않는다. 

오히려 DataSource의 defaultAutoCommit 설정은 true로 하는 것이 좋다. 어차피 DataSource를 커넥션 풀의 역할을 할 뿐이지 트랜잭션 관리의 책임까지 지지 않기 때문이다. 무슨 말이나면, DataSource에서 직접 Connection을 얻어서 사용할 때(이런 과정을 커넥션을 Fetch한다고 한다.) autoCommit을 false로 준다고 하더라도 나중에 커넥션이 반환되었을때 DataSource는 commit/rollback 처리를 보장하지 않는다는 말이다. 즉, 커넥션을 얻어서 트랜잭션을 시작할지, 커밋을할지 롤백을할지에 대한 책임은 커넥션을 사용하는 어플리케이션에게 있다.

트랜잭션 매니저와 DataSource의 역할을 이해한다면, 왜 트랜잭션 매니저 내부에서 강제로 커넥션의 autoCommit 을 false로 세팅하는지 이해할 수 있을 것이다.



스프링 Propagation

그렇다면 Propagation은 무엇인가? 바로 트랜잭션의 전파(propagation) 규칙을 정의하는 것이다. 
사실 트랜잭션의 전파라는 말 자체가 상당히 이해하기 힘들다. 스프링에서 제공하는 트랜잭션 전파 규칙들을 살펴보자.

접기

- PROPAGATION_MADATORY
  : 반드시 트랜잭션 내에서 메소드가 실행되야 함
  : 트랜잭션이 없는 경우에는 예외 발생
 
- PROPAGATION_NESTED
  : 트랜잭션에 있는 경우, 기존 트랜잭션 내의 nested transaction 형태로 메소드 실행
  : nested transaction 자체적으로 commit, rollback 가능
  : 트랜잭션이 없는 경우, PROPAGATION_REQUIRED 속성으로 행동
 
- PROPAGATION_NEVER
  : 트랜잭션 컨텍스트가 없이 실행되야 함
  : 트랜잭션이 있으면 예외 발생
 
- PROPAGATION_NOT_SUPPORTED
  : 트랜잭션이 없이 메소드 실행
  : 기존의 트랜잭션이 있는 경우에는 이 트랜잭션을 호출된 메소드가 끝날때까지 잠시 보류
  : JTATransactionManager를 사용하는 경우에는, TransactionManager가 필요
 
- PROPAGATION_REQUIRED
  : 트랜잭션 컨텍스트 내에서 메소드가 실행되야 함
  : 기존 트랜잭션이 있는 경우, 기존 트랜잭션 내에서 실행
  : 기존 트랜잭션이 없는 경우, 새로운 트랜잭션 생성
 
- PROPAGATION_REQUIRED_NEW
  : 호출되는 메소드는 자신 만의 트랜잭션을 가지고 실행
  : 기존의 트랜잭션들은 보류됨
  : JTATransactionManager를 사용하는 경우에는, TransactionManager가 필요
 
- PROPAGATION_SUPPORTS
  : 새로운 트랜잭션을 필요로 하지는 않지만, 기존의 트랜잭션이 있는 경우에는 트랜잭션 내에서 실행

접기


사실 스프링에서의 트랜잭션 전파란 여러 메소드(실행)의 수행을 하나의 트랜잭션(작업)으로 묶기 위한 규칙을 말한다. 즉, 어떠한 작업이 A, B, C 메소드의 실행으로 이루어져 있다면, 이 3가지 메소드를 하나의 트랜잭션으로 묶을 것인지, B 메소드만 별도의 트랜잭션으로 분리할 것인지 등등에 대한 설정을 제공하는 것이다. 
http://static.springsource.org/spring/docs/2.5.x/reference/transaction.html#tx-propagation
(스프링에서는 메소드 단위로 트랜잭션 Propagation 설정을 할 수 있다.)



트랜잭션 매니저는 사용하면서 autocommit 은 true로 할 수 없는가?

이 글의 핵심이자 위 내용의 응용이다. 보통은 한번의 request로 인해 발생하는 일련의 수행들을 하나의 트랜잭션으로 묶는것이 일반적이지만, 그러고 싶지 않은 경우도 분명히 존재한다. 

'어플리케이션에서는 하나의 트랜잭션이지만 실제 DB작업은 트랜잭션을 걸지 않고 처리'하고 싶은 경우도 있다. 예를 들어 로그 데이터와 같은 경우, 일련의 로그를 쌓던 도중 에러가 났다고 해서 모두 롤백이 되어 버리면 로그의 의미가 없어지게 되므로 개별 로그 데이터를 쌓는 작업을 별도의 트랜잭션으로 설정해야 한다.

이런 경우 DataSource를 직접 사용하면 되지만 만약 여러개의 쓰레드가 많은 양의 CRUD 작업을 하게 되면, 한번의 sql 처리에 DataSource로부터 커넥션 Fetch와 Return이 반복되어 발생하며 여러 쓰레드가 동시에 이러한 작업을 하게 되므로, DataSouce가 병목지점이 될 수 있다. 

이러한 현상을 피하기 위해서 하나의 트랜잭션에 대해서 하나의 커넥션만 fetch하도록 해야 하며 이와 동시에 커넥션의 autocommit이 true여야 한다는 요구조건을 만족시켜야 한다.

이런 경우 해당 처리 메소드들을 트랜잭션에 참여시키지 않도록 하면 간단하게 요구사항을 만족시킬 수 있다.

접기

    ......
    <!-- auto name proxy for DataManager Transaction -->

    <bean id="nameMatchAS"class="org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
        <property name="properties">
            <props>
                <prop key="*">PROPAGATION_NOT_SUPPORTED,
                    -RuntimeException, -Exception</prop>
            </props>
        </property>
    </bean>
    ......

접기


이렇게 하면 DB에서 트랜잭션이 발생하지 않으면서 (autocommit = true) TransactionManager에 의해 커넥션을 한번만 fetching 하게 된다.


출처 - http://tinywolf.tistory.com/107


===================================================================================


<?xml version="1.0" encoding="UTF-8"?>

<beans default-autowire="no" default-lazy-init="false"
 default-dependency-check="none"
 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"
 xmlns:jee="http://www.springframework.org/schema/jee"
 xsi:schemaLocation="
 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
 http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.0.xsd">

 <!-- DB 설정파일 위치 지정 -->
 <bean id="dbConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="location"><value>WEB-INF/config/db.properties</value></property>
 </bean>

    <!-- dataSource 정의 -->
    <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>
        <property name="maxActive"><value>${jdbc.maxActive}</value></property>
        <property name="maxIdle"><value>${jdbc.maxIdle}</value></property>
        <property name="maxWait"><value>${jdbc.maxWait}</value></property>
    </bean> 
 
    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> 
     <property name="dataSource" ref="dataSource" />
     <property name="configLocation" value="WEB-INF/config/sqlMapConfig.xml" />
    </bean>     
    
    <bean id="transactionManager"
  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
 </bean>
 
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
     <tx:attributes>
      <tx:method name="execute*" read-only="false" rollback-for="BizException" propagation="REQUIRED"/>
     </tx:attributes>
    </tx:advice>
    
    <aop:config>
     <aop:pointcut id="testServiceProcOperation" expression="execution(* test.logic.proc.*Proc.*(..))" />
     <aop:advisor advice-ref="txAdvice" pointcut-ref="testServiceProcOperation"/>
    </aop:config>
</beans>


출처 - http://bbaeggar.tistory.com/86











Posted by linuxism

댓글을 달아 주세요