본문 바로가기
프로젝트/자리 있어요?

[Refactoring] 22.08.31. Transaction (트랜잭션) 처리

by 규글 2022. 8. 31.

 트랜잭션은 21년 8월 5일에 처음 소개했고, 6일에 관련 내용을 수업했던 내용이다. 이 프로젝트에서는 트랜잭션에 대한 처리까지 하지 않았었고, 그래서 그에 대한 작업을 해주기 위해서 해당 필기 부분을 찾아보던 중, 자세하게 필기되어있지 않아서 이런저런 검색을 통해 관련 내용을 알아보고 작업을 시작하고자 한다.

 

Transaction (트랜잭션)

 Transaction은 사전적으로 '거래, 매매' 의 의미도 있지만, 하나의 '처리(과정)' 의 의미도 있다. 그래서 DataBase Transaction (데이터베이스 트랜잭션) 이라 함은 데이터베이스에서의 어떤 상호 작용, 동작, 처리 과정의 '단위'라고 할 수 있겠다. 즉, 데이터에 대한 하나의 논리적인 실행 단계를 의미한다고 할 수 있다. 이론적으로 데이터베이스 시스템은 각 transaction에 대해 안전하게 수행되기 위한 다음의 네 가지 성질 보장하며, 각 성질의 첫 글자를 따서 'ACID'라고도 부른다. 실제로는 성능 향상을 위해 이 성질들을 약간은 완화해서 적용하는 것 같다. [각주:1] [각주:2]

  • 원자성 (Atomicity)
    트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 성질이다. 예를 들어 은행 이제 작업에서 이체 자체는 성공과 실패가 구분될 수 있으나, 한쪽에서 돈을 빼내는 작업만 성공하고 반대쪽에 돈을 넣는 작업이 실패하는 것은 안된다. 즉, 중간 단계까지만 수행되고 나머지가 실패하는 일이 없도록, 하나의 작업이 수행되도록 하는 성질이다. (사실 원자도 쿼크로 쪼개지는 것이 사실이긴 하다. 의미만 받아들이면 된다.)

  • 일관성 (Consistency)
    트랙잭션이 성공적으로 수행되면 언제나 일관성 있는 데이터베이스의 상태로 유지하는 성질이다. 예를 들어 모든 계좌에는 잔고가 있어야하는 것이 조건이라면, 그것을 위반하게 되는 트랜잭션은 중단된다.

  • 독립성 (Isolation)
    트랜잭션이 수행될 때, 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 성질이다. 이는 트랜잭션이 독립적인 수행으로 간주되어, 트랜잭션의 밖에서는 그 중간 단계의 데이터를 볼 수 없다는 것을 의미한다고 한다. 이체를 다시 예로 들면, 한쪽에서 다른쪽으로 이체하는 query를 실행하더라도 양쪽에 대하여 그것을 확인할 수 없다는 것이다. 성능 관련으로 이 성질은 가장 유연한 제약 조건이라고 하는데, 이 말은 아직 와닿지 않는다.

    독립성을 유지하기 위해서는 한 유저가 특정 table을 읽고 있다면, 다른 유저는 그 유저의 트랜잭션이 끝나는 것을 기다려야 한다. 하지만 그렇게되면 작업의 동시 수행이 어렵고, 성능도 온전히 발휘하지 못한다고 한다. 그래서 기존에 데이터베이스가 락(lock)에 의존했던 것에서, 그 대안으로 수정되는 모든 데이터를 별도의 복사본으로 관리하는 MVCC(다중 버전 동시성 제어 : Multi-Version Concurrency Control) 방식을 사용한다고 한다. 즉, 한쪽에서 트랜잭션 시작할 때 가지고 있던 복사본을 다른 유저에 제공해서 동시에 작업이 수행될 수 있다고 한다. 이런 면에서 유연성이 크다고 하는 것 같다.

  • 영구성 (Durability)
    성공적으로 수행된 트랜잭션은 영원히 반영되어야 하는 성질이다. 시스템 문제나 데이터베이스의 일관성 체크 등의 작업을 하더라도 유지되어야 한다는 것을 의미한다. 모든 트랜잭션은 로그로 남고, 시스템 장애 발생 전 상태로도 되돌릴 수 있다. 그리고 트랜잭션은 로그에 모든 내용이 저장된 후에만 commit된 상태로 간주될 수 있다고 한다. 다른 말로 '지속성, 영속성'이라고도 하는 것 같다.

 

트랜잭션의 단계

 간단한 트랜잭션은 다음과 같은 단계로 수행된다.

  • 트랜잭션의 시작
  • Query 문들의 수행
  • Commit

 만약 이 과정에서 중간에 query 하나가 실패하게 되면 데이터베이스 시스템은 수행되던 트랜잭션에서 실패한 query문에 대해 roll back 한다. 이는 DBMS가 어떻게 사용되고 있고 그 설정에 따라서도 다르지만, 중요한 것은 commit 전에는 언제든지 수동으로라도 roll back 될 수 있다는 점이다.

 이렇게 트랜잭션을 지원하는 데이터베이스를 'Transactional Database' 라고 하며, 현재 대부분의 관계형 데이터베이스 관리 시스템은 트랜잭션을 지원하고 있다.

 

트랜잭션의 목적

 데이터의 안정성을 최고로 다루는 데이터베이스는 그 안정성을 유지하기 위해서 트랜잭션을 다루는 기능을 포함한다. 하나의 트랜잭션은 각각 데이터베이스의 정보를 읽고 쓰는 하나 이상의 독립된 작업 단위로 구성되어 있다. 이렇게 트랜잭션이 수행될 때 중요한 것은 모든 그러한 진행과정들이 데이터베이스에 일관된 상태로 저장되는 것을 보장하는 것이 중요하다. 즉, 트랜잭션은 이를 보장하기 위해 존재하는 것이다.

 물건을 구매하는 과정을 예로 들어보자. 물건을 구매하는 과정은 크게 다음과 같이 생각해볼 수 있다.

  • 구매하고자 하는 품목 선택
  • 해당 품목에 대한 주문
  • 결제
  • 배송
  • 물건 도착

 이 중에 결제 과정에서는 잔고에서 품목의 가격만큼이 물건 판매자에게 전달되어야 한다. 하지만, 그렇게 되었을 때 잔고가 0 미만이 된다면 그것은 문제가 된다. 이것을 query 문으로 작업할 때 잔고 column이 반드시 0 이상이어야 하는 조건이 있다면 sql exception이 발생한다. 그리고 Spring은 sql exception이 발생하면 DataAccessException을 발생시킨다고 한다. (이 DataAccessException은 @Repository annotation이 붙어있는 dao에서 발생한 exception이다.)

 이외에도 품목의 재고가 없는 경우에도 해당 품목의 개수를 줄이는 query문을 수행하는 과정에서 문제가 발생할 수도 있는 노릇이다. 때문에 데이터베이스에 일관된 상태로 저장되는 것을 보장하기 위해서 트랜잭션을 사용하는 것이다.

 

 

과거의 수업 내용

	/*
	 *  - Spring 트랜잭션 설정 방법
	 *  1. pom.xml 에 spring-tx dependency 추가
	 *  2. servlet-context.xml 에 transaction 설정추가
	 *  3. 트랜잭션을 관리할 서비스의 메소드에 @Transactional 어노테이션 붙이기 
	 *  
	 *  - 프로그래머의 의도 하에서 트랜잭션에 영향을 주는 Exception 을 발생 시키는 방법
	 *  
	 *  DataAccessException 클래스를 상속받은 클래스를 정의하고 
	 *  
	 *  예)  class  MyException extends DataAccessExecption{ }
	 *       throw new MyException("예외 메세지"); 
	 *       
	 *  예외를 발생시킬 조건이라면 위와 같이 예외를 발생시켜서
	 *  트랜잭션이 관리 되도록 한다. 
	 */

 과거 국비 수업에서 수업 실습 파일에 필기해둔 내용이다. 트랜잭션을 사용하기 위해서는 위와 같은 단계를 거치면 된다고 한다.

 

		<!-- 트렌잭션 처리를 위한 라이브러리 -->
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-tx</artifactId>
		    <version>4.0.0.RELEASE</version>
		</dependency>

 우선 프로젝트의 pom.xml file에 spring-tx dependency를 추가한다.

 

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- 
      JNDI 데이터 소스 객체 얻어오는 설정   
      Servers/context.xml 에 설정된 oracle 접속정보 가 있어야 된다. 
       
      <Resource name="jdbc/myoracle" auth="Container"
              type="javax.sql.DataSource" driverClassName="oracle.jdbc.OracleDriver"
              url="jdbc:oracle:thin:@127.0.0.1:1521:xe"
              username="scott" password="tiger" maxTotal="20" maxIdle="10"
              maxWaitMillis="-1"/>          
   	-->
	<beans:bean id="dataSource" 
	   	class="org.springframework.jndi.JndiObjectFactoryBean">
	   	<beans:property name="jndiName" value="java:comp/env/jdbc/myoracle"/>
	</beans:bean>
	<!-- 
		위는 아래의 코드와 같다
		dataSource = new JndiObjectFactoryBean();
		dataSource.setJndiName("java:comp/env/jdbc/myoracle");
	-->
	
	
	<!-- SqlSessionFactory 객체 -->
	<beans:bean id="sessionFactory" 
		class="org.mybatis.spring.SqlSessionFactoryBean">
		<beans:property name="dataSource" ref="dataSource"/>
		<beans:property name="configLocation"
		value="classpath:com/gura/spring05/mybatis/Configuration.xml"/>
	</beans:bean>
	<!-- 
		sessionFactory=new SqlSessionFactoryBean();
		sessionFactory.setDataSource(dataSource);
		sessionFactory.setConfigLocation("classpath:com/gura/xxx");
	-->
	
	
	<!-- 
		SqlSession 인터페이스를 구현한 
		SqlSessionTemplate(SqlSession) 객체 
		Dao 가 의존하는 객체 
	-->
	<beans:bean class="org.mybatis.spring.SqlSessionTemplate">
	  	<beans:constructor-arg name="sqlSessionFactory" 
	     	ref="sessionFactory"/>
	</beans:bean>
	
	<!-- Spring Transaction Manager 설정 -->
   	<beans:bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      	<beans:property name="dataSource" ref="dataSource"/>
   	</beans:bean>
   
   	<!-- @Transactional 어노테이션으로  트렌젝션을 관리 할수 있도록 설정-->
   	<tx:annotation-driven transaction-manager="txManager"/>

 그리고 프로젝트의 src > webapp > WEB-INF > spring > appServlet > 하위에 있는 servlet-context.xml file에 트랜잭션 설정을 추가한다. 붙여넣은 코드는 길지만, 바로 위에 보이는 Spring Transaction Manager 설정 부분을 말한다. dataSource를 필요로 하고 있어서, 이전 내용까지도 긁어왔다.

 

 이렇게 설정을 마치고 나서 트랜잭션으로 설정할 service의 method에 @Transactional annotation을 붙여주면 된다.

 

 사실 Mapper의 query문 동작을 위한 @Repository annotation을 붙인 Dao class에서는 SqlSession 객체를 autowired 해서 사용하고 있다. 해당 객체에는 commit( ) method와 rollback( ) method가 있는데, 이 과정을 Spring framework의 @Transactional annotation을 붙이면 알아서 해주는 것이다.

 

 

현재의 작업 프로젝트

 저것은 수업 내용이고, 현재 refactoring을 하고 있는 작업 프로젝트로 돌아오겠다. 이곳에는 기존 실습 내용을 복사 붙여넣기 한 것이 이유인지 해당 내용이 전부 존재하고 있었다. 그래서 바로 @Transactional annotation을 붙이기로 했다.

 

 가장 먼저 UsersService에는 여러 service method들이 있지만, 그 중에서도 유저의 계정 비밀번호를 수정하는 updateUserPwd 와 유저의 계정을 삭제하는 deleteUser 에서 두 개 이상의 query문을 사용하고 있다. 그래서 해당 service logic에 @Transactional annotation을 작성해주었다.

 

 그렇다면 sql exception에서는 문제가 없었는데, service logic의 다른 부분에서 exception이 발생해서 logic이 멈춘다든지 하는 경우에는 어떻게 되는 것일까? 다음 게시글에서 이에 대해 다뤄보려고 한다.

댓글