트랜잭션
From JCFWiKi
Copyright © 2007 Daewoo Information Systems Co., Ltd. |
|
여기서는 JCF에서 트랜잭션을 처리할때 사용되는 기술과 사용하는 방법에 대해서 설명한다. 크게 단일 DB를 사용하는 경우와 다중 DB를 사용하는 경우, 각각에 대해서 트랜잭션 관리 방법을 볼 수 있다. |
목차 |
[편집] JCF에서의 트랜잭션 관리 방법
- 여기서 다루는 트랜잭션은 데이터 베이스에 DML(INSERT, UPDATE, DELETE) 문장을 수행하는 경우 적합하게 커밋하고 롤백하는 데이터 베이스 트랜잭션을 의미합니다. JCF 에서는 스프링 프레임워크의 트랜잭션 관리 기능을 사용하고 있습니다. 스프링 트랜잭션 관리 기능을 사용하면 데이터 베이스를 연동하는 다양한 기술들(JDBC, Stored Procedure, ibatis, hibernate, EJB, ..)에 대해서 일관된 방법으로 트랜잭션을 관리할 수 있습니다. 단지 하나의 DB 연동 기술을 사용할 때뿐만 아니라, 두 가지 이상의 기술을 복합적으로 사용할 때에도 역시 일관된 방법으로 쉽게 트랜잭션을 관리 할 수 있습니다. 게다가 실제 소스 코드에는 트랜잭션을 관리하기 위한 코드가 필요 없다는 것이 큰 장점이 됩니다. XML을 통해 선언적으로 트랜잭션 처리 전략을 결정할 수 있기 때문입니다. 또한 스프링 2.0이상의 버전에서 제공하는 AOP 기능을 사용하면 매우 자유롭게 트랜잭션을 관리 할 수 있습니다.
- 스프링을 사용하는 경우, 일반적인 웹 애플리케이션에서는 다음 그림과 같이 트랜잭션을 관리하게 됩니다.
- 그림에서 트랜잭션은 비즈니스 로직이 시작하는 서비스 클래스로부터 시작합니다. 정확히 말하자면, 하나의 서비스 메소드가 시작할 때, 하나의 트랜잭션이 시작된다고 보면 됩니다(이것은 결정하기 나름입니다만, 일반적인 형태의 경우를 가정합니다). 이렇게 하나의 서비스 메소드가 시작된 후 끝날 때까지 실행되는 다양한 DB DML문들은 스프링에서 하나의 트랜잭션의 바운더리로 묶게 됩니다. 그리고 시작된 서비스 메소드가 무사히 완료되면, 모든 DML 문들을 일괄적으로 커밋하게 됩니다. 만약, 서비스 메소드가 완료되기 전에 예외(Exception)이 발생하게 되면, 해당 트랜잭션에서 수행되었던 모든 DML들을 롤백 처리 합니다.(여기서 예외의 타입이 중요한데, 기타 부분에서 다룹니다.)
[편집] 스프링 프레임워크를 통해 트랜잭션 관리 선언하기
- DB 데이터 소스 자원을 스프링의 관리 자원으로 선언하는 방법과 실제 트랜잭션을 관리하는 방법을 설명합니다.
[편집] 데이터 소스를 스프링에서 관리하기
- 스프링 프레임워크는 스프링에서 관리하는 자바 클래스(빈(Bean) 클래스라고 합니다.)들에 대한 라이프 사이클을 관리하게 됩니다. 그리고 이렇게 스프링에서 관리하는 빈 클래스 자원들에 대해서 JEE에서 제공하는 서비스들을 쉽게 제공할 수 있는 방법을 스프링에서 제공합니다. 따라서 DB와 연동하기 위한 DB 커넥션, 즉 DB 데이터 소스 역시 스프링이 관리 자원이어야만 스프링에서 트랜잭션을 관리할 수 있습니다.(다양한 트랜잭션 선언 방법은 기타 부분에서 다룹니다.)
- DB 데이터 소스를 스프링의 관리 자원으로 선언하기 위한 방법은 다양합니다. 가장 일반적인 방법은 다음과 같습니다.
- 1. 커넥션 풀 데이터 소스 만들기
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName"><value>oracle.jdbc.driver.OracleDriver</value></property> <property name="url"><value>jdbc:oracle:thin:@127.0.0.1:1521:ora</value></property> <property name="username"><value>scott</value></property> <property name="password"><value>tiger</value></property> </bean>
- 이와 같이 선언하면 스프링이 DBCP의 커넥션 풀을 사용하여 자동으로 커넥션들을 만들게 되고, 이러한 커넥션들은 스프링의 관리 자원이 됩니다. <bean/> 태그의 id 속성은 스프링에서 해당 자원의 고유한 아이디를 부여하는데 사용됩니다. 여기서는 데이터 소스를 관리하기 위한 스프링의 아이디가 됩니다. class 속성에서는 데이터소스 자원을 생성하는데 사용되는 실제 클래스를 지정합니다. 내부 <property/> 태그 들에서는 데이터 소스를 생성하는데 필요한 부가적인 정보들을 입력하게 됩니다. 각각의 프라퍼티들은 class 속성에 지정된 클래스의 속성(맴버 변수)으로 존재하고 있습니다.
- 2. 웹 애플리케이션 서버에 생성된 데이터 소스 룩업(Lookup)해서 가져오기
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/sampleDS"/>
- 이와 같은 정의를 통해서 WAS의 데이터소스를 통해 DB 커넥션을 가져올 수 있고, 이러한 커넥션들은 스프링의 관리 자원이 됩니다. jdbc/sampleDS 라는 이름으로 정의된 WAS의 데이터 소스를 룩업하여 스프링에서는 dataSource라는 아이디로 관리를 하게 됩니다. 이와 같이 선언된 dataSource는 다른 클래스에서 참조하여 사용할 수 있게 됩니다.
[편집] 스프링에서 트랜잭션 선언하기
- 웹 애플리케이션에서 사용하는 DB 커넥션이 스프링 관리 자원이 되었다면, 이제 스프링의 선언적인 방법을 통해서 트랜잭션을 관리할 수 있습니다(자바 소스 코드내부에서 프로그래밍 적으로 처리하는 방법도 있으나, 권장하지 않습니다.).
- 스프링에서 선언적으로 처리하기 위한 XML 작성 방법은 다음과 같습니다
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="find*" read-only="true"/> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="serviceOperation" expression="execution(* com.sample..service.*Service.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/> </aop:config>
- 여기서는 <bean/>, <tx:advice/>, <aop:config> 태그를 사용하여 3가지 정의를 하고 있습니다. 트랜잭션을 관리할 트랜잭션 매니저, 트랜잭션을 처리할 전략, 그리고 트랜잭션을 처리해야 하는 대상을 각각 정의하고 있습니다. 각 태그에 대한 설명은 다음과 같습니다.
- 1. 우선 트랜잭션을 관리할 매니터는 <bean/> 태그로 정의했습니다. <bean/> 태그에서는 txManager라는 이름을 가지는 트랜잭션 매니저를 선언하고 있습니다. 실제 클래스는 스프링의 DataSourceTransactionManager라는 클래스인데, 이 클래스는 특정 데이터 소스에 대한 트랜잭션을 관리하는 클래스 입니다. 태그 내부에 <property/> 태그를 사용해서 관리해야 할 데이터 소스를 지정하고 있습니다. <property/> 태그의 name 속성은 트랜잭션 매니저 클래스의 프라퍼티(맴버 변수)명이고, 실제 관리할 대상인 데이터 소스의 이름은 ref 속성에 지정됩니다(이름이 동일하다고 혼동하지 마십시오.). ref 속성에 지정된 값은 이전에 <bean/> 태그나 <jee:jndi-lookup/> 태그를 사용하여 선언된 데이터 소스의 id 값입니다.
- 2. 이제 트랜잭션을 처리할 전략입니다. 스프링에서는 특정 클래스(예, 서비스 클래스)의 메소드가 시작해서 끝나기 것까지를 기준으로 트랜잭션을 관리하기 때문에 전략도 메소드 단위로 지정하게 됩니다. <tx:advice /> 태그에서 이러한 전략을 정의하고 있습니다. 우선 id 속성으로 txAdvice를 부여했으며, 여기서 정의하는 트랜잭션 전략들은 transaction-manager 속성에 정의된 txManager라는 아이디를 가진 빈 클래스가 수행하게 됩니다. <tx:attribute/> 라는 태그 내부에 각 메소드 단위로 전략들을 정의하고 있는데, 첫번째 <tx:method/>에 정의에서 name 속성의 find*는 find로 시작하는 모든 메소드를 지칭하게 되며, read-only 속성이 true로 지정됨에 따라, 해당 메소드 내의 트랜잭션은 read-only로 관리됩니다.
- 두번째 <tx:method/>태그에서는 name속성에 * 값을 지정하여, 이전에 정의한 메소드 이외의 모든 메소드들을 대상으로 하고 있으며, propagation 속성의 값을 required로 지정함으로서 해당 메소드에서 실행되는 DML문들은 하나의 트랜잭션으로 묶이도록 정의하고 있습니다(propagation에 지정되는 다양한 트랜잭션 속성값들과 각각의 의미는 기타 부분에서 다룹니다.).
- 3. 마지막으로 트랜잭션을 처리할 대상들은 <aop:config/>에서 정의하고 있습니다. 내부의 <aop:pointcut/>을 통해 트랜잭션 처리 대상들(메소드들)을 지정하고 있고, <aop:advisor/> 태그를 통해서 트랜잭션 처리 대상에 대해서 적용할 트랜잭션 전략을 지정(매핑)하고 있습니다. <aop:pointcut/>를 살펴 보면 id는 serviceOperation으로 지정했고, 실제 대상들은 expression 속성에 정규 표현식을 통해 간략하게 표현되고 있습니다. 예에서 정의된 내용은 다음과 같습니다.
- * : 모든 형태의 가시성(public, protected, ..)을 가지는 메소드.
- com.sample : com.sample 이라는 패키지 명으로 시작하는 클래스
- ..service : 마지막 패키지명이 service로 끝나는 클래스
- *Service : 클래스 명이 Service로 끝나는 클래스.
- *(..) : 모든 형태의 파라미터를 가지는 모든 메소드
- 의미를 정리를 해보면, “com.sample로 시작하며, service로 끝나는 패키지 명을 가지는 클래스들 중에서 Service라는 이름으로 끝나는 클래스들이 가지는 모든 메소드들”을 지정하는 정규 표현식입니다.
- <aop:advisor/> 에서는 serviceOperation 이라는 id를 가지는 pointcut에 대해서(pointcut-ref) txadvisor라는 아이디(advice-ref)를 가지는 트랜잭션 전략을 적용하겠다고 선언했습니다.
- 이와 같은 선언을 통해서 실제 자바 클래스의 소스 코드상에서 트랜잭션 커밋이나 롤백을 관리하지 않고, 스프링에서 적합하게 트랜잭션을 관리할 수 있게 됩니다.
- 단, 여기서는 다음의 2가지를 가정하고 있습니다.
- 1. 트랜잭션 처리 대상 클래스들(서비스 클래스등..)은 모두 스프링이 관리하는 클래스이어야 한다.
- 단, 여기서는 다음의 2가지를 가정하고 있습니다.
<bean id="codeService" class="com.sample.common.code.service.CodeService"> <property name="codeDao" ref="codeDao"/> </bean>
- 2. DB 커넥션은 반드시 스프링에 정의된 데이터 소스로부터 획득되어야 한다.
<bean id="codeDao" class="com.sample.common.code.dao.CodeDaoImpl"> <property name="dataSource" ref=" dataSource "/> </bean>
[편집] 다중 데이터 소스를 사용하는 경우 트랜잭션 처리하기
[편집] JTA 설정 방법
- 하나의 DB에 대해서 권한별로 데이터 소스를 다르게 사용하거나, 아예 다수의 DB로부터 다양한 데이터 소스를 가져와 사용해야 하는 경우에도 스프링을 통해서 트랜잭션을 일관성 있게 관리할 수 있습니다. 이 경우 JTA를 사용하게 됩니다.
- JTA를 사용하면 다수의 DB 커넥션에서 수행된 DML문들을 하나의 트랜잭션으로 묶어서 일괄적으로 커밋 혹은 롤백(2-phase-commit) 할 수 있습니다. 이를 위해서는 데이터 소스가 JTA를 지원해야 하는데, 별도의 솔루션을 사용하지 않는 경우 일반적으로 웹로직, 웹스피어, JBOSS 등 JTA를 지원하는 WAS의 데이터소스를 사용해야 합니다. 스프링에서는 이러한 데이터 소스를 <jee:lookup/>을 통해서 가져오고, 트랜잭션 매니저를 JTA 트랜잭션 매니저로 변경함으로써 쉽게 2-phase-commit을 활용할 수 있습니다.
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/sampleJtaDS"/> <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
- <jee:jndi/>는 이전과 달라질 것이 없습니다. 다만, jndi-name 속성에 지정된 데이터소스명에 해당하는 WAS의 데이터 소스가 JTA 데이터 소스이면 됩니다. 다음으로 트랜잭션 매니저를 선언하는 부분에서 트랜잭션 매니저 클래스 부분이 달라집니다. id가 txManager인 <bean/> 정의를 보면, 스프링에서 제공하는 JtaTransactionManager를 사용하는 것을 볼 수 있으며, 별도로 데이터 소스를 지정하지 않는 것을 확인할 수 있습니다. 이렇게 되면, txManager가 적용되는 대상들(예, 서비스 클래스의 메소드. 이부분은 <aop:config/>에서 지정됨)에서 사용되는 커넥션들이 모두 트랜잭션 관리 대상이 됩니다.
[편집] 트랜잭션 속성
- REQUIRED - 현재의 트랜잭션을 지원하고, 없을 경우 새로 만듭니다. 이것은 가장 자주 사용되는 옵션입니다.
- SUPPORTS - 현재의 트랜잭션을 지원하고, 없을 경우 트랜잭션을 실행하지 않습니다.
- MANDATORY - 현재의 트랜잭션을 지원하고, 없을 경우 예외를 throw합니다.
- REQUIRES_NEW - 새로운 트랜잭션을 만들고 현재의 트랜잭션이 존재하면 이를 일시 중지합니다.
- NOT_SUPPORTED - 트랜잭션을 실행하지 않다가 트랜잭션이 존재하게 되면 이를 일시 중지합니다.
- NEVER - 트랜잭션을 실행하지 않다가 트랜잭션이 존재하게 되면 예외를 throw합니다.
- NESTED - 현재의 트랜잭션이 존재하면 중첩된 트랜잭션 내에서 실행하고 존재하지 않으면 PROPAGATION_REQUIRED처럼 작동합니다.
[편집] JBoss를 사용한 다중데이터 소스 활용 방법
- 환경 : JBoss 4.2.1.GA 와 오라클 9i
- JBoss 에서 XA 데이터 소스 설정하는 방법(참고 URL : http://wiki.jboss.org/wiki/SetUpAOracleDatasource)
<?xml version="1.0" encoding="UTF-8"?> <datasources> <xa-datasource> <jndi-name>jdbc/commonXA</jndi-name> <track-connection-by-tx/> <isSameRM-override-value>false</isSameRM-override-value> <xa-datasource-class>oracle.jdbc.xa.client.OracleXADataSource</xa-datasource-class> <xa-datasource-property name="URL">jdbc:oracle:thin:@152.149.xx.xx:1521:ora9i</xa-datasource-property> <xa-datasource-property name="User">baw</xa-datasource-property> <xa-datasource-property name="Password">baw</xa-datasource-property> <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter</exception-sorter-class-name> <no-tx-separate-pools/> </xa-datasource> <xa-datasource> <jndi-name>jdbc/tagXA</jndi-name> <track-connection-by-tx/> <isSameRM-override-value>false</isSameRM-override-value> <xa-datasource-class>oracle.jdbc.xa.client.OracleXADataSource</xa-datasource-class> <xa-datasource-property name="URL">jdbc:oracle:thin:@152.149.xx.xx:1521:ora9i</xa-datasource-property> <xa-datasource-property name="User">foo</xa-datasource-property> <xa-datasource-property name="Password">foo</xa-datasource-property> <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter</exception-sorter-class-name> <no-tx-separate-pools/> </xa-datasource> </datasources>
- 스프링 applicationContext XML 파일 설정
- datasource와 ibatis sqlMapClient 및 트랜잭션 설정 부분
- tag모듈 하나만 제외하고 모두 datasource-common을 사용한다. <aop:config/> 에서 commonServiceOperation과 tagServiceOperation을 구분한것은 기능적으로 차이는 없으며, 단지 tag 서비스를 보여주기 위해서 아래와 같이 작성했다. 두개를 합쳐서 serviceOperation으로 작성하는 것이 더욱 자연스러운 처리가 될 것이다.
- 주목할 점은 datasource-common으로 sqlMapClient-common을 생성하고, dataSource-tag로 sqlMapClient-tag를 생성함으로서 실제 tagDao는 별도의 데이터소스(커넥션)을 사용할 것이라는 점이며, 이를 위해서 트랜잭션 매니저를 JtaTransactionManager를 적용했다는 점이다.
- datasource와 ibatis sqlMapClient 및 트랜잭션 설정 부분
<jee:jndi-lookup id="dataSource-common" jndi-name="java:jdbc/commonXA"/> <jee:jndi-lookup id="dataSource-tag" jndi-name="java:jdbc/tagXA"/> <bean id="sqlMapClient-common" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation"> <value>classpath:/config/sqlmap-config.xml</value> </property> <property name="dataSource"> <ref bean="dataSource-common" /> </property> </bean> <bean id="sqlMapClient-tag" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation"> <value>classpath:/config/sqlmap-config.xml</value> </property> <property name="dataSource"> <ref bean="dataSource-tag" /> </property> </bean> <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true" /> <tx:method name="find*" read-only="true" /> <tx:method name="view*" read-only="true" /> <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="*" propagation="REQUIRED" /> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="commonServiceOperation" expression="execution(* edu.blog..service.*Service.*(..)) || execution(* edu.code..service.*Service.*(..)) || execution(* sample..service.*Service.*(..))" /> <aop:pointcut id="tagServiceOperation" expression="execution(* edu.tag..service.*Service.*(..))" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="commonServiceOperation" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="tagServiceOperation" /> </aop:config>
- 애플리케이션 모듈 설정 부분
- 위에서 설정된 sqlMapClient-common과 sqlMapClient-tag로 각각 blogDao와 tagDao를 작성했다. 별도의 데이터소스를 가지게 된 것이다.
- blogService에서는 blogDao 뿐만 아니라 tagService를 가진다. 즉 블로그를 생성할때, 블로그 정보와 태그 정보를 서로 다른 데이터 소스를 가지고(두개의 커넥션을 가지고) 데이터베이스에 Insert 할수 있게 되었다.
- 주석처리 된 blogService에서 처럼 tagService가 아니라 tagDao를 사용해도 트랜잭션은 정상적으로 관리된다. Service를 사용할지 Dao를 사용할지는 모델링 관점에서 결정하면 된다.
- 애플리케이션 모듈 설정 부분
<!-- configuratio for blog --> <bean id="blogService" class="edu.blog.service.BlogService"> <property name="blogDao" ref="blogDao" /> <property name="tagService" ref="tagService"/> </bean> <bean id="blogDao" class="edu.blog.dao.BlogDao"> <property name="sqlMapClient" ref="sqlMapClient-common" /> </bean> <!-- configuratio for tag --> <bean id="tagService" class="edu.tag.service.TagService"> <property name="tagDao" ref="tagDao" /> </bean> <bean id="tagDao" class="edu.tag.dao.TagDao"> <property name="sqlMapClient" ref="sqlMapClient-tag" /> </bean>
