앞글에서 서비스 추상화에 대해 살펴보았다. 회원의 레벨을 업그레이드 할때 예외가 발생하게 되면 어떤일이 발생할까?

예외 발생 전 회원만 레벨이 업그레이드 되고, 그 후 회원들은 레벨이 그대로 일것이다. 이러한 현상이 실제 게임상에 일어난다면 회원들의 반발이 클것이라 예상할 수있다. 이러한 문제를 사전에 예방하기 위해 우리는 트랜잭션이라는 개념을 이용하여 처리 할 수 있다.

트랜잭션이란 더 이상 나눌 수 없는 단위 작업이다.

여기서 중요한 개념 2가지를 짚고 넘어가도록 하자.

두가지 이상의 작업이 하나의 트랜잭션이 되려면 모든 작업이 성공적으로 수행 되기 전 문제가 발생 했을시에 앞서 처리한 DB 작업이 모두 취소 되어야 한다. 이것이 바로 트랜잭션 롤백(Transaction Rollback)이다.  그리고 모든 작업이 정상적으로 수행 되었다면 DB에게 작업 완료를 알려야 한다. 이것이 바로 트랜잭션 커밋(Transaction Commit)이다.

JDBC 트랜잭션의 트랜잭션 경계설정

모든 트랜잭션은 시작하는 지점과 끝나는 지점이 있다. 시작하는 방법은 한가지 이지만 끝나는 방법은 두가지이다. 모든 작업을 무효화 하는 롤백과 모든 작업을 확정하는 커밋이다.

트랜잭션에서 경계란 어플리케이션안에서 트랜잭션이 시작되고 끝나는 위치를 말한다.

아래는 트랜잭션을 적용하는 간단한 예제이다.

JDBC의 트랙재션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 트랜잭션의 시작과 종료는 Connection오브젝트를 통해 이뤄지기 때문이다. JDBC에서 트랙잭션을 시작하려면 자동커밋 옵션을 false로 만들어 주면 된다.(setAutoCommit(false)) JDBC의 기본 설정은 DB작업을 수행 직후에 자동으로 커밋이 되도록 되어 있다. 트랜잭션이 한번 시작되면 commit or rollback()메소드가 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다. 작업중에 예외가 발생하면 트랜잭션을 rollback한다.

이렇게 setAutoCommit(false)로 트랜잭션을 시작하고, commit() or rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정(transaction demarcation) 이라고 한다. 트랜잭션의 경계는 하나의 Connection이 만들어지고 닫히는 범위안에 존재한다.

자, 그럼 이제 회원 레벨업그레이드를 위한 메소드 구조를 살펴보자. 아래는  예외 발생시 롤백 처리 하고 문제 없을시 커밋 처리 하기 위한 트랜잭션 경계설정 구조이다.

트랜잭션을 사용하는 전형적인 JDBC코드의 구조이다.  여기서  눈여겨 봐야 할 점은 DB connection을 생성한 후, connection 객체를 DAO 메소드에 넘겨 주어 사용할 수 있도록 해야 한다. 트랜잭션의 경계는 하나의 connection 객체로 정해 지기 때문에 동일한 트랜잭션 처리를 하기 위해서는 같은 connection을 사용해야 한다.

하지만 이렇게 할 경우 DAO메소드를 호출 할때마다  매번 connection객체를 넘겨줘야 하는 불편함이 있다. 이를 위해 스프링은 트랜잭션 동기화 방식을 제공한다.

트랜잭션 동기화 방식 (transaction synchronizaion)

(1) UserService는 Connection을 생성하고 (2) 이를 트랜잭션 동기화 저장소에 저장해 두고 Connection의 setAutoCommit(false)를 호출해 트랜잭션을 시작시킨 후에 DAO의 기능을 이용하기 시작한다. (3) 첫번째 update()메소드가 호출되고, update()메소드 내부에서 이용하는 JdbcTemplate메소드에서는 가장 먼저 (4)트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection오브젝트가 존재하는지 확인한다. (2) upgraLevels() 메소드 시작 부분에서 저장해둔 Connection을 발견하고 이를 가져온다. 가져온 (5) Connection을 이용해 PreparedStatement를 만들어 수정 SQL을 실행한다. (6) 두번째 update()가 실행되면 이때도 마찬가지로 (7) 트랜잭션 동기화 저장소에서 Connection을 가져와 (8)사용한다.(9) 마지막 update()도 (10)같은 트랜잭션을 가진 connection을 가져와 (11)사용한다. 모든 작업이 정상적으로 끝났다면 (12) Connection의 commit()을 호출하여 트랜잭션을 완료시킨다. 마지막으로 (13) 트랜잭션저장소가 더이상 Connection 오브젝트를 저장해두지 않도록 Connection객체를  제거한다. 어느 작업중에 예외 발생시에는 즉시 Connection의 rollback()을 호출하고 트랜잭션을 종료하며 이때에도 트랜잭션저장소가 더이상 Connection 오브젝트를 저장해두지 않도록 Connection객체를 제거한다.

이제 위를 구현한 코드를 살펴보자.

public void upgradeLevels() throws Exception {
		TransactionSynchronizationManager.initSynchronization();  //트랜잭션 동기화 관리자를 이용해 동기화 작업을 초기화 한다. 
		Connection c = DataSourceUtils.getConnection(dataSource); //DB 커넥션을 생성하고 트랜잭션을 시작한다. 이후의 DAO작업은 모두 여기서 시작한 트랜잭션 안에서 실행된다. 
		c.setAutoCommit(false);
		
		try {									   
			List<User> users = userDao.getAll();
			for (User user : users) {
				if (canUpgradeLevel(user)) {
					upgradeLevel(user);
				}
			}
			c.commit();  
		} catch (Exception e) {    
			c.rollback(); //예외 발생시 rollback
			throw e;
		} finally {
			DataSourceUtils.releaseConnection(c, dataSource);	// DB 커넥션을 닫는다. 
			TransactionSynchronizationManager.unbindResource(this.dataSource);  // 동기화 작업종료및 정리 
			TransactionSynchronizationManager.clearSynchronization();  // 동기화 작업종료및 정리 
		}
	}
스프링의 트랜잭션 서비스 추상화  

스프링은 기술에 독립적은 트랜잭션을 사용 할 수 있는 기술을 제공한다. JMS, JDBC등 다양한 기술의 트랜잭션을 추상화하여 제공 함으로써 기술에 독립적은 트랜잭션 경계설정이 가능해졌다. 아래 그림은 스프링의 트랜잭션 서비스 추상화 계층을 그림으로 나타낸것이다.

위 그림에서 알 수 있듯이 우리가 지금까지 사용한 JDBC를 이용하기 위해서는 추상 Interface PlatformTransactionManager을 구현한 DataSourceTransactionManager를 사용한다. 지금부터 이를 사용하여 UserService의 트랜잭션 경계설정을 해보도록 하자. 아래 코드를 참고하기 바란다.

public void upgradeLevels() throws Exception {
		//사용할 DB(JDBC)의 DataSource를 생성자에 넣어 트랜잭션 추상 오브젝트 생성
		PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); 
		//TransactionDefinition( abstract Interface) <- DefalutTransactionDefinition
		TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition() );  //트랜잭션 시작 
		 
		try {									   
			List<User> users = userDao.getAll();
			for (User user : users) {
				if (canUpgradeLevel(user)) {
					upgradeLevel(user);
				}
			}
			transactionManager.commit(status);  
		} catch (Exception e) {    
			transactionManager.rollback(status); //예외 발생시 rollback
			throw e;
		} finally {
			
		}
	}

그럼 여기서 하나 질문을 해보자 . JDBC가 아니라 JTA를 이용한다고 한다면 어떻게 하면 될까?

PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); 

-> PlatformTransactionManager transactionManager = new JTATransactionManager();

이렇게 바꿔주기만 하면 된다. 우리가 지금까지 배워왔던   스프링의 DI를 이용해서 다시 코드를 변경해보자. UserService와 Context  File을 아래와 같이 수정하였다.

public class UserService implements UserLevelUpgradePolicy{
	@Autowired
	private UserDao userDao ;
	public static final int MIN_LOGCOUNT_FOR_SILVER=50;
	public static final int MIN_RECOMMEND_FOR_GOLD=30;
	@Autowired
	private PlatformTransactionManager transactionManager;
	
	public void setTransactionManager(PlatformTransactionManager transactionManager) {
		this.transactionManager = transactionManager;
	}
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void upgradeLevels() throws Exception {
		//사용할 DB(JDBC)의 DataSource를 생성자에 넣어 트랜잭션 추상 오브젝트 생성
		TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition() );  //트랜잭션 시작 
		try {									   
			List<User> users = userDao.getAll();
			for (User user : users) {
				if (canUpgradeLevel(user)) {
					upgradeLevel(user);
				}
			}
			transactionManager.commit(status);  
		} catch (Exception e) {    
			transactionManager.rollback(status); //예외 발생시 rollback
			throw e;
		} finally {
			
		}
	}
	public void upgradeLevel(User user) {
		// TODO Auto-generated method stub
		user.upgradeLevel();	
		userDao.update(user);
	}
	public boolean canUpgradeLevel(User user) {
		// TODO Auto-generated method stub
		Level currentLevel =user.getLevel();
		switch(currentLevel) {
			case BASIC: return (user.getLogin() >= MIN_LOGCOUNT_FOR_SILVER);
			case SILVER: return (user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD);
			case GOLD: return false;
			default: throw new IllegalArgumentException("Unknowned Level : " + currentLevel);
		}
	}
	public void add(User user) {
		// TODO Auto-generated method stub
		if(user.getLevel() == null) user.setLevel(Level.BASIC);
		this.userDao.add(user);
	}
}
     Contex설정 파일에는 아래내용을 추가해 주면 된다.
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userService" class="springbook.user.dao.UserService">
  <property name="userDao" ref="userDao"/>
  <property name="transactionManager" ref="transactionManager"/>
</bean>

'Spring' 카테고리의 다른 글

log4J.xml 파일을 못찾을때 해결법  (0) 2020.03.14
5장 서비스 추상화-I  (0) 2020.01.14
4장 예외  (0) 2020.01.05
3장 템플릿IV - 템플릿/콜백의 응용  (0) 2020.01.04
3장 템플릿 III - 스프링의 JdbcTemplate  (0) 2020.01.04

+ Recent posts