# Spring - Transaction


1) Transaction 이란?


    - 데이터베이스 관리 시스템에서의 상호작용 단위

    - 데이터베이스 작업 단위

    - 하나 이상의 SQL 문장들로 이루어진 논리적인 작업 단위

    - 데이터베이스 처리 작업이 모두 완료되거나 모두 취소되게 되는 작업의 단위


     --> 실제로 INSERT, UPDATE, DELETE 등 도 하나의 트랜잭션으로 볼 수 있다.  예를 들어, 10개의 데이터를 삭제 하는 경우 3개를 삭제한 후,

          4개째에서 오류가 발생하여 삭제가 안되면, 기존의 삭제 된 3개도 rollback 한다. (또는 3개만 정상 삭제 처리됬다고 사용자에게 알림을 

          주기도 한다)


     --> 여러개의 DML 문을 복합적으로 사용하여 하나의 트랜잭션으로 묶는 경우도 많다. DELETE 후 INSERT 하는 경우가 그 예이다. 



2) Transaction의 특성 'ACID'

  

    - 원자성(Atomicity): 더이상 분리할 수 없는 하나의 작업 단위로, 모두 완료되거나 모두 취소되어야 한다.


    - 일관성(Consistency): 사용되는 데이터는 모두 일관되어야 한다.


    - 고립성(Isolation): 하나의 트랜잭션이 접근하고 있는 데이터는 다른 트랜잭션으로 부터 격리되어야 한다. 

                             트랜잭션 과정 중간의 데이터는 확인 불가.


    - 영구성(Durability): 트랜잭션이 종료되면, 그 결과는 영구적으로 적용되어야 한다.


       --> 실제론, 성능향상을 위해 각 특성을 완화하는 경우도 많다. 



 3) Transaction, 뭐 때문에 쓰는건지?? 목적?


     트랜잭션을 쓰는 이유는 데이터 무결성(integrity) 때문이다. 

     쿼리 하나가 실패하면, 데이터베이스 시스템은 전체 트랜잭션 또는 실패한 쿼리를 롤백한다. 

     은행에서 내계좌에서 돈이 빠져나간 후 '송금' 처리하는 작업을 순차적으로 처리할 때, 중간에 쿼리가 실패하여

     내 계좌에서는 돈이 빠져나갔는데, 송금 처리는 되지 않는 경우가 발생 할 수 있다. 이런 경우 데이터 무결성이 깨지게 된다. 

     이러한 문제를 방지하기 위해서, 즉 데이터 무결성을 지키기 위해 트랜잭션을 사용한다.

     

     --> 실제로 트랜잭션은 보통 아래와 같은 과정을 거쳐 SQL 언어로 데이터베이스 내에서 실행된다.

            * Begin the transaction

            * Execute several queries  (DB 갱신 전)

            * Commit the transaction  (성공적인 트랜잭션 수행 후, DB 갱신)



  4) Transaction 설정


      사실 이 부분을 정리해 두려고, 이 포스팅을 작성하기 시작했다. 프로젝트 때 delete 후 insert 하는 업무 등을 처리하기 위해

      트랜잭션을 걸어줬었다. 실제로 어떻게 설정하는지 아래에 정리해 보았다.


      트랜잭션을 설정하는 방법은 하나만 있는 것이 아니다. Java의 트랜잭션 API (JTA)를 쓸 수도 있고, Spring이 제공하는 트랜잭션

      기능 등을 이용할 수 도 있다. 


      우선 이번 포스팅에서는 Spring에서 제공해주는 트랜잭션 관리 방법을 정리하고자 한다. 



 (4.1) Transaction 관련 스키마 설정

      

<?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: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/context http://www.springframework.org/schema/context/spring-context.xsd">



 (4.2) TransactionManager 설정


         - 우선, datasource 쪽 설정파일에 transactionmanager 설정을 해준다.

         - 이번 프로젝트에서는, resources/spring/context-datasource.xml 이라고 DB 관련 설정을 한 데 모아놓았다. 트랜잭션도 이곳에 설정!

         - class: spring 프레임워크에서 제공하는 DataSourceTransactionManager 클래스 (platformTransactionManager)


<tx:annotation-driven transaction-manager="transactionManagerSample" proxy-target-class="true" />

     <bean id="transactionManagerSample"       

              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

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

     </bean>

tx:annotation-driven

 어노테이션에 기반한 트랜잭션의 동작을 활성화한다

 @Transcational이 붙은 bean 만 찾음 (applicationContext에서)

   --> 만약 webApplicationContext에 tx:annotaion-driven을 설정해놓으면 서비스가 아니라 controller

         에서만 @Transcational이 붙은 bean을 찾는다

transaction-manager

 default 값: transactionManager

 default 값 'transactionManager' 가 아닌 다른 이름을 사용하는 경우에만 설정 

 proxy-target-class

 proxy 모드에만 적용된다.
 @Transactional 어노테이션이 붙은 클래스에 어떤 타입의 트랜잭션 프록시를 생성할 것인지 제어한다.    proxy-target-class 속성을 true로 설정했다면 클래스기반의 프록시가 생성된다.
  --> 트랜잭션은 원래 interface에다가 걸게끔 나왔는데, controller 클래스에 걸려는 사용자들이 늘어나

       면서 기본 클래스에도 transaction을 걸 수 있도록 설정할 수 있게 되었다. 

 proxy-target-class 속성을 생략하거나 false로 설정하면 표준 JDK 인터페이스 기반의 프록시가 생성된다


 DataSourceTransactionManager 클래스

 역할

  - JDBC 3.0을 통해 트랜잭션 지원
  - 특정 datasource 로 부터 현재 쓰레드까지 JDBC connection 을 바인딩 한다 

 주요 메서드

  - setDataSource, getDataSource

  - doBegin

  - doCommit

  - doRollback



 (4.3) @Transactional 어노테이션 설정


        - 인터페이스 정의, 인터페이스의 메서드, 클래스 정의, 클래스의 퍼블릭 메서드 앞에 @Transactional 어노테이션을 설정 할 수 있다.

        - 트랜잭션을 적용하려는 메서드 등에 설정. 특정 메서드에서만 트랜잭션을 사용하는 경우, 인터페이스 혹은 클래스 전체에 거는 것보다

          특정 메서드에만 걸어주는 것이 성능면에서 효율적이다.


/**

* 데이터 일괄 삭제 후 insert 서비스

* @param CommandMap commandMap, String jasonData

* @return Map<String, Object> resultMap

* @throws Exception

*/

@SuppressWarnings({ "unchecked", "rawtypes" })

@RequestMapping(value = "/ajaxUpdateList", method = RequestMethod.POST)

@Transactional

public @ResponseBody Map<String, Object> updateFpFunctionList(

@RequestParam(value = "rowList", required = true) String jasonData, CommandMap commandMap)

throws Exception {

...... 생략

        

// 기존 데이터 일괄 삭제

mapDao.delete("groupId.artifactId.sample_delete_all", commandMap.translateMap());

   

// 업데이트 데이터, 한 row 씩 INSERT 처리

for (Object object : array) {

mapDao.update("groupId.artifactId.sample_insert", (Map) object);

}

       ..... 

}

return resultMap;

}



 @Transactional 속성

 propagation

 

  

 isolation

 

 

 readOnly 

 boolean

 읽기/쓰기 트랜잭션? or 읽기 전용 트랜잭션?  

 성능을 최적화하기 위해 사용할 수도 있고 특정 트랜잭션 작업 안에서 쓰기 작업이 일어나는  것을 의도적으로 방지하기 위해 사용하기도 함. 

 timeout

 int 

 트랜잭션 타임 아웃

 정해진 시간 내에 메소드 수행이 완료되지 않을 경우 rollback함

 default 값: -1 ( = timeout 수행하지 않음)

 rollbackFor

 

 정의된 exception에 대해서는 rollback을 수행함 

 noRollbackFor

 

 정의된 exception에 대해서는 rollback을 수행하지 않음

( ** propagation 과 isolation 개념은 아직 정확히 이해가 안되어 작성하지 못했다.. 



 (4.4) Transaction 이 잘 걸렸나 테스트


       - 위와 같이 트랜잭션 설정을 해놓았으니, 이제 정말 잘 설정된 것이 맞는 지 테스트를 해 볼 차례다.

         테스트는 jUnit 테스트로 진행해보았다.

       

@WebAppConfiguration

@RunWith(SpringJUnit4ClassRunner.class)

@ActiveProfiles("dev")

@ContextConfiguration(locations={"classpath*:/spring/context-*.xml", "classpath*:/config/dispatcher-servlet.xml"})

public class TestTransaction {


    @Autowired

private TransactionTest TransactionTest;

    

    @Autowired

private GenericDao<Map<String, Object>> mapDao;

@SuppressWarnings({ "unchecked", "rawtypes" })

@Test

public void test() throws Exception {

Map map = new HashMap();

map.put("YEAR", "2017");

map.put("userId", "testID");

map.put("name", "tester");

//삭제 후 테스트

mapDao.delete("groupId.artifactId.sample_delete", map);

//중복키 INSERT 테스트

try{

TransactionTest.testDuplecateInsert(map); --> testDuplecateInsert 클래스에서는 같은 데이터(map)를 insert 시도

                                                                                        --> 예외 발생(SQLServerException)

}

catch(Exception e){

//예외 발생 시 INSERT 여부 파악

List<Map<String, Object>> list = mapDao.list("groupId.artifactId.sample_select", map);

if(list.size() == 0 ) {

   System.out.println( "rollback success!" );

}else{

   System.out.println( "rollback fail!" );

}

}

}


위의 class를 test 클래스로 만들고 공통모듈 'aop'쪽에 TestTransaction.java 라는 클래스를 만들어 실제 testDuplecateInsert 메서드를 수행했다.

위 클래스에서 마우스 우측 > run as > junit Test 를 클릭하면 junit test 가 수행된다. (junit 테스트 관련 정리는 다음 포스팅에~!!)


그, 결과 삭제 후 insert 도중 exception이 발생하여 예외가 발생하기전에 insert 수행 처리 되었던 row까지 전부 rollback 되었다. 

트랜잭션 rollback 성공!!


<-- 발생 Exception: SQLServerException -->

com.microsoft.sqlserver.jdbc.SQLServerException: PRIMARY KEY 제약 조건 'TB_SAMPLE'을(를) 위반했습니다. 개체 'dbo.TB_SAMPLE'에 중복 키를 삽입할 수 없습니다. 중복 키 값은 (2017, testID, tester)입니다.

at com.microsoft.sqlserver.jdbc.SQLServerException.makeFromDatabaseError(SQLServerException.java:196)

at com.microsoft.sqlserver.jdbc.SQLServerStatement.getNextResult(SQLServerStatement.java:1454)

at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.doExecutePreparedStatement(SQLServerPreparedStatement.java:388)

at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement$PrepStmtExecCmd.doExecute(SQLServerPreparedStatement.java:338)

at com.microsoft.sqlserver.jdbc.TDSCommand.execute(IOBuffer.java:4026)

at com.microsoft.sqlserver.jdbc.SQLServerConnection.executeCommand(SQLServerConnection.java:1416)

at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeCommand(SQLServerStatement.java:185)

at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeStatement(SQLServerStatement.java:160)

at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.execute(SQLServerPreparedStatement.java:320)

at org.apache.commons.dbcp.DelegatingPreparedStatement.execute(DelegatingPreparedStatement.java:172)

at org.apache.commons.dbcp.DelegatingPreparedStatement.execute(DelegatingPreparedStatement.java:172)

at net.sf.log4jdbc.PreparedStatementSpy.execute(PreparedStatementSpy.java:418) 


     ....... 생략

 rollback success!



트랜잭션을 무작정 걸기 전에, 왜 트랜잭션을 쓰는지, 어디에 걸어야 성능측면에서 가장 효율적일지 등의 고민을 해야할 것 같다. 

설정 후 테스트도 충분히 해보고~!


__________________________________________________________________________________________________________________________________________________________

** 본 포스팅에 대해 수정해야할 부분이나 추가 의견 등이 있으신 분들은 댓글 달아주세요. 언제나 환영입니다 :) 

** 본 포스팅은 아래의 reference 들을 참고하여 내용을 덧붙인 글입니다. 혹시, 문제가 되는 경우 알려주시면 조치하도록 하겠습니다.

** 본 포스팅을 reference 자료로 참고하실 분들은 출처를 꼭 밝혀주시기 바랍니다.


  - https://ko.wikipedia.org/wiki

  - https://spring.io/guides/gs/managing-transactions/

  - https://blog.outsider.ne.kr/869





'Spring' 카테고리의 다른 글

[에러] no matching editors or conversion strategy found  (0) 2017.04.20
(작성중) dispatcher-servlet.xml 설정  (0) 2017.04.16
spring - profile  (0) 2017.03.19
Spring - BeanNameViewResolver  (0) 2017.01.29

+ Recent posts