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

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

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

여기서 중요한 개념 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

템플릿과 콜백의 기본적인 원리와 동작방식, 만드는 방법을 알아보았으니, 지금 부터는 Spring에서 제공하는 템플릿/콜백 기술을 살펴보자. 스프링은 JDBC를 이용하는 DAO에서 사용할 수는 다양한 템플릿과 콜백을 제공한다. 

스프링이 제공하는 JDBC코드용 기본 템플릿은 JdbcTemplate이다.  앞에서 만든 JdbcContext와 비슷하지만 더 강력한 기능을 제공한다.   아래는 기존에 작성 JdbcContext를 이용하여 작성했던 코드이다. 지금 부터는 아래 코드를 JdbcTemplate을 이용하는 코드로 변경 해보자. 

public class UserDao{
	private DataSource dataSource;
	JdbcContext jdbcContext;
	
	public void setDataSource(DataSource dataSource) {		
		this.jdbcContext = new JdbcContext();	 
		this.jdbcContext.setDataSource(dataSource);
		this.dataSource = dataSource;
	}
	
	//반복되는 부분 분리 
	public void executeSQL(final String strSQL) throws SQLException{
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makeStatement(Connection c) throws SQLException {
					PreparedStatement ps=c.prepareStatement(strSQL);
					return ps;
				}
			}				
		);	
	}
	public void deleteAll() throws SQLException {		
		executeSQL("delete from users");
	}	
	public void add(final User user) throws SQLException {	
		this.jdbcContext.workWithStatementStrategy(
			 new StatementStrategy() {
				 public PreparedStatement makeStatement(Connection c) throws SQLException {
						PreparedStatement ps = c.prepareStatement(
								"insert into users(id, name, password) values(?,?,?)");
							ps.setString(1, user.getId());
							ps.setString(2, user.getName());
							ps.setString(3, user.getPassword());
						return ps;
					}
			 }				
		);
	}
	//아직 템플릿 / 콜백 방식을 적용하지 않은 함수들
	/*public User get(String id) throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		ResultSet rs =null;
		User user = null;
		
		try {		
			c = this.dataSource.getConnection();
			ps = c.prepareStatement("select * from users where id = ?");
			ps.setString(1, id);			
			rs = ps.executeQuery();			
			if (rs.next()) {
				user = new User();
				user.setId(rs.getString("id"));
				user.setName(rs.getString("name"));
				user.setPassword(rs.getString("password"));
			}
		}catch(SQLException e) {
			throw e; 
		}finally{
			if(rs != null) {
				try {
					// try/catch문 사용하지 않았을때 rs.close()시 exception발생시 그다음 코드는 실행이 안된다.
					// ps,c가 close가 안되는 문제가 발생하게 됨.
					rs.close();  
				}catch(SQLException e) {
					
				}
			}
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
						}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
		if (user == null) throw new EmptyResultDataAccessException(1);
		return user;
	}

	

	public int getCount() throws SQLException  {
		Connection c = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		int count =0;
		try {
			c= dataSource.getConnection();	
			ps=c.prepareStatement("select count(*) from users");
			rs=ps.executeQuery();
			rs.next();
			count = rs.getInt(1);
		}catch(SQLException e) {
			throw e;
		}finally {
			if(rs != null) {
				try {
					rs.close();
				}catch(SQLException e) {
					
				}
			}
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
		return count;
	}*/
}

1. JdbcContext를 JdbcTemplate으로 변경했다.

private DataSource dataSource;
JdbcTemplate jdbcContext;

public void setDataSource(DataSource dataSource) {		
	this.jdbcContext = new JdbcTemplate();	 
	this.jdbcContext.setDataSource(dataSource);
	this.dataSource = dataSource;
}

 2. 이제 deleteAll() 함수를 변경해보자. 

public void deleteAll() throws SQLException {		
	this.jdbcTemplate.update("delete from users");
}	

3. add()함수를 변경해보자

public void add(final User user) throws SQLException {	
	this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
			user.getId(),user.getName(),user.getPassword());
}

4. 아직 템플릿/콜백 방식을 적용하지 않았던 메소드에 JdbcTemplate을 적용해보자. 

 getCount( )  함수 수정

기존에 작성된 getCount( ) 함수는 JdbcTemplate의 query( ) 함수와 PreparedStatementCreator콜백과          resultSetExtractor 콜백을 사용하여 수정 할수 있다.  

  • PreparedStatementCreator callback 함수 - statement생성
  • ResultSetExtractor callback 함수 - 쿼리실행후 얻은 Resultset에서 원하는 값 추출 
public int getCount() throws SQLException  {
		return this.jdbcTemplate.query(new PreparedStatementCreator() {			
			@Override
			public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
				// TODO Auto-generated method stub
				return con.prepareStatement("select count(*) from users");
			}
		}, new ResultSetExtractor<Integer>() {
			public Integer extractData(ResultSet rs) throws SQLException,DataAccessException {
				rs.next();
				return rs.getInt(1);
			}
		});
	}

"select count(*) from users" 처럼 SQL의 실행 결과가 하나의 정수 값이 되는 경우 이용할 수 있는 함수를 JdbcTemplate에서 제공하고 있다. 바로 queryForInt() 함수 이다. 이 함수를 이용하여 위 코드를 다시 수정해보자.  코드가 훨씬 간결해졌다. 

public int getCount() throws SQLException  {
		return this.jdbcTemplate.queryForInt("select count(*) from users");
}
get( ) 메소드 수정 
  • queryForObject( ) 함수 사용
  • RowMapper callback 사용 

위에 두 함수를 사용하여 수정하였다.

	public User get(String id) throws SQLException {		
		return this.jdbcTemplate.queryForObject("select * from users where id=? ", 
				new Object[] {id}, //-> SQL에 바인딩할 파라미터값, 가변인자 대신 배열을 사용 
				new RowMapper<User>() {					
					@Override
					public User mapRow(ResultSet rs, int rowNum)throws SQLException {
						// TODO Auto-generated method stub
						User 	user = new User();
						user.setId(rs.getString("id"));
						user.setName(rs.getString("name"));
						user.setPassword(rs.getString("password"));
						return user;
					}
				});
	}

 

5. 모든 사용자 정보를 가져오는 getAll() 함수를 추가해 보자. 

public List<User> getAll() throws SQLException{
	return this.jdbcTemplate.query("select * from users",
			new RowMapper<User>() {					
				@Override
				public User mapRow(ResultSet rs, int rowNum)throws SQLException {
					// TODO Auto-generated method stub
					User 	user = new User();
					user.setId(rs.getString("id"));
					user.setName(rs.getString("name"));
					user.setPassword(rs.getString("password"));
					return user;
				}
			});
}

위 코드는 JdbcTemplate의 query()  템플릿를 사용했다. 해당 query() 템플릿의 수행 Logic은 아래에 설명해 두었으니, 참고하기 바란다. 

query () 템플릿 parameter

  • 첫번째 : SQL문 
  • 두번째 : 바인딩할 파라미터가 있는 경우 넣고 없으면 생각가능
  • 세번째 : RowMapper콜백이다.

query() 템플릿 수행 순서

 

  1. query() 템플릿은 SQL을 실행해서 얻은 Resultset의 모든 Row을 열어 RowMapper 콜백을 호출한다. 즉, SQL 쿼리 실행 결과 Row 수만큼 호출된다. 
  2. 호출된 RowMapper콜백은 해당 Row를 User로 Mapping해서 돌려주게 된다.
  3.  이렇게 만들어진 User 오브젝트는 템플릿에서 미리 준비한 List<User> 컬렉션에 추가된다.
  4. 그리고 모든작업이 완료되면 템플릿은 List<User>을 리턴힌다. 

6. 재사용 가능한 콜백 분리 

DataSourc인스턴스 변수는 더이상 사용 하지 않으니, 제거하자.

하기 코드에서 보면 알수있듯이 get() , getAll() 함수에서 RowMapper가 중복됨을 확인했다. 중복되는 코드를 다시 분리시켜 보자.

중복되는 익명 내부 클래스를 인스턴스 변수 하나에 받아서 사용하도록 수정했다. 드디어 코드가 완성되었다.!!!!

public class UserDao{
	JdbcTemplate jdbcTemplate;
	private RowMapper<User> userMapper = new RowMapper<User>() {					
		@Override
		public User mapRow(ResultSet rs, int rowNum)throws SQLException {
			// TODO Auto-generated method stub
			User 	user = new User();
			user.setId(rs.getString("id"));
			user.setName(rs.getString("name"));
			user.setPassword(rs.getString("password"));
			return user;
		}
	};
	
	public void setDataSource(DataSource dataSource) {		
		this.jdbcTemplate = new JdbcTemplate();	 
		this.jdbcTemplate.setDataSource(dataSource);
	}
	public void deleteAll() {		
		this.jdbcTemplate.update("delete from users");
	}	
	public void add(final User user) {	
		this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
				user.getId(),user.getName(),user.getPassword());
	}

	public User get(String id) {		
		//가변 인자 사용하고 싶을때 
		return this.jdbcTemplate.queryForObject("select * from users where id=? ", this.userMapper,id);
	}
	public int getCount()  {
		return this.jdbcTemplate.queryForInt("select count(*) from users");
	}
	public List<User> getAll() {
		return this.jdbcTemplate.query("select * from users",this.userMapper);
	}
}

'Spring' 카테고리의 다른 글

4장 예외  (0) 2020.01.05
3장 템플릿IV - 템플릿/콜백의 응용  (0) 2020.01.04
3장 템플릿 II- 템플릿/ 콜백패턴  (0) 2020.01.04
3장 템플릿I - 전략패턴  (0) 2020.01.04
2장 테스트  (0) 2019.12.28

+ Recent posts