# 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