스프링은 기본적으로  / WEB-INF/classes 폴더 아래에 두면 log4j.xml파일을 자동으로 읽는다.

스프링이 log4j.xml파일을 못찾을땐,  / WEB-INF/classes 폴더 아래로 log4j.xml파일을  옮겨보자.

'Spring' 카테고리의 다른 글

5장 트랜잭션 서비스 추상화  (0) 2020.01.29
5장 서비스 추상화-I  (0) 2020.01.14
4장 예외  (0) 2020.01.05
3장 템플릿IV - 템플릿/콜백의 응용  (0) 2020.01.04
3장 템플릿 III - 스프링의 JdbcTemplate  (0) 2020.01.04

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

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

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

여기서 중요한 개념 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
픽스쳐(fixture)

테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스쳐(fixture)라고 한다. UserDaoTest에서라면 dao가 대표적인 픽스처라고 볼 수 있다.

1장에서 4장까지 차례대로 만들어 왔던  UserDao는 User오브젝트에 담겨 있는 사용자 정보를 등록,조회,수정, 삭제하는 가장 기초적인 작업만 가능하다. 지금 부터는 간단한 비지니스 로직을 추가해보자.

구현해야할 비지니스 로직은 아래와 같다.

  • 사용자의 레벨은 BASIC,SILVER,GOLD중 하나다.
  • 사용자가 처음 가입하면 BASIC,이후 활동하면서 한단계씩 업그레이드된다.
  • 가입 후 50회 이상 로그인을 하면 BASIC에서 SILVER레벨이 된다.
  • SILVER레벨이면서 30번 이상 추천을 받으면 GOLD레벨이 된다.
  • 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않는다.

비지니스 로직을 알았으니 이제 구현을 시작해보자. 우선, 사용자 레벨을 저장할 필드가 필요하다. 그럼 사용자 레벨을 어떤 타입으로 저장하는게 좋을까? DB에 varchar타입으로 "GOLD","SILVER","BASIC"으로 저장하는 방법도 있겠지만 , DB용량도 많이  차지하고 좋아 보이지 않는다. 그렇다면  범위가 작은 숫자로 관리하면 어떨까? DB용량도 많이 차지않고 가벼워서 좋다. 하지만, 의미없는 숫자를 property로 사용하면 타입이 안전하지 않아 위험할 수 있다. 예를 들어 , GOLD을 1, SILVER를 2, BASIC을 3으로 정해좋고 사용한다고 해보자. 실수로 1,2,3 의 숫자외에  값을 저장할 경우가 발생한다면???  컴파일러가 체크해 주지 못한다는 점이다. 잘못하다가는 레벨이 엉뚱하게 바뀌는 심각한 버그가 발생할 가능성이 잠재하게 된다. 따라서, 이럴경우는 enum을 이용하는것이 좋다. 

1. BASIC,SILVER,GOLD를 enum을 이용하여 정의해보자.

public enum Level {
	BASIC(1),SILVER(2),GOLD(3); //-> 세 개의 이늄 오브젝트 정의 
    
	private final int value;
	
	private Level(int value) { //-> DB에 저장할 값을 넣어줄 생성자를 만들어 준다. 
		this.value = value;
	}
	public int initValue() { //-> 값을 가져오는 메소스 
		return value;
	}	
	public static Level valueOf(int value) //-> 값으로 부터 Level타입 오브젝트를 가져오도록 
    {									   // 	만든 스태틱 메소스 
		switch(value) {
			case 1: return BASIC;
			case 2: return SILVER;
			case 3: return GOLD;	
			default: throw new AssertionError("Unknown value: "+ value);
		}
	}
}

enum을 이용하게 되면 DB에 저장할 int 타입의 값을 가지고 있지만, 겉으로는 Level타입의 오브젝트 이므로 안전하게 사용할 수 있는 장점이 있다. 예를 들어 user1.setLevel(1000) 같은 코드는 컴파일러가 알아서 error로 걸러주게 된다. 

2. User 클래스에 Level 필드를 추가해보자

public class User {
	String id;
	String name;
	String password;
	Level level;
	int login;
	int recommend;
	
	public User() {
		
	}	
	public User(String id, String name, String password, Level level, int login, int recommend) {
		super();
		this.id = id;
		this.name = name;
		this.password = password;
		this.level = level;
		this.login = login;
		this.recommend = recommend;
	}		
	public void upgradeLevel() {
		Level nextLevel = this.level.nextLevel();
		if(nextLevel ==null) {
			throw new IllegalStateException(this.level +"은 업그레이드가 불가능합니다."); 
		}else {
			this.level = nextLevel;
		}
	}
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public int getLogin() {
		return login;
	}

	public void setLogin(int login) {
		this.login = login;
	}

	public int getRecommend() {
		return recommend;
	}

	public void setRecommend(int recommend) {
		this.recommend = recommend;
	}

	
	public void setLevel(Level level) {
		this.level = level;
	}

	public Level getLevel() {
		return level;
	}
}

3. 필드가 추가 되었으니  UserDaoJdbc도 수정해 보겠다.

public class UserDaoJdbc implements 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"));
			user.setLevel(Level.valueOf(rs.getInt("LEVEL")));
			user.setLogin(rs.getInt("LOGIN"));
			user.setRecommend(rs.getInt("RECOMMEND"));
			return user;
		}
	};
	
	public void setDataSource(DataSource dataSource) {		
		this.jdbcTemplate = new JdbcTemplate(dataSource);	 
	}
	public int deleteAll() {		
		return this.jdbcTemplate.update("delete from users");
	}	
	public int add(final User user) {	
		return this.jdbcTemplate.update("insert into users(id, name, password,level,login,RECOMMEND) values(?,?,?,?,?,?)",
				user.getId(),user.getName(),user.getPassword(),user.getLevel().initValue(),user.getLogin(),user.getRecommend());
	}
	public int update(User user) {
		return this.jdbcTemplate.update("update users set name=?,password=?,level=?,login=?,recommend=? where id=?", 
				user.getName(),user.getPassword(),user.getLevel().initValue(),user.getLogin(),user.getRecommend(),user.getId());
	}
	
	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);
	}
}

4. Domain와 Jdbc를 수정해두었으니, 사용자 레벨을 업그레이드 시킬 실제 비즈니스 로직이 들어갈 Service 클래스를 만들어보자.

public class UserService {
	@Autowired
	private UserDao userDao ;
	
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void upgradeLevels() {
		List<User> Userlist = userDao.getAll();
		for( User user: Userlist){
			Boolean changed =null;
			if(user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
				user.setLevel(Level.SILVER);
				changed=true;
				
			}else if(user.getLevel() == Level.SILVER && user.getRecommend() >=30) {
				user.setLevel(Level.GOLD);
				changed=true;
			}else  if(user.getLevel() == Level.GOLD){
				changed=false;
			}else {
				changed=false;
			}
			
			if(changed == true) {
				userDao.update(user);
			}
		}		
	}
	public void add(User user) {
		// TODO Auto-generated method stub
		if(user.getLevel() == null) user.setLevel(Level.BASIC);
		this.userDao.add(user);
	}
}

이제 비지니스 로직의 구현을 모두 마쳤다. 하지만 코드에 대한 점검이 필요하다.

  • 코드에 중복된 부분은 없는가?
  • 코드가 무엇을 하는것인지 불편하지 않는가?
  • 코드가 자신이 있어야 할 자리에 있는가?
  • 앞으로 변경이 일어난다면 어떤것이 있을수 있고 그변화에 쉽게 대응할 수있게 작성되었는가?

위에 질문사항들을 체크해 나가다 보니, UserService의 upgradeLevels() 메소스에서 몇가지 문제가 발견되었다.

여러기능의 Logic들이 뒤섞여 있어 가독성이 떨어지는것이 가장 큰 문제 였다. 지금부터 각 기능의 로직들을 분리하여 가독성을 높일 수 있도록 코드 리팩토링을 해보겠다.

리팩토링

우선, upgradeLevels()의 역할이 무엇인지 분명히 할 필요가 있다. 해당 메소드는 이름 그래도 사용자의 레벨을 업그레이드 하는 역할에만 충실할 수 있도록 변경해야 한다.

사용자가 레벨을 변경할 수 있는가? 있다면 레벨을 변경한다.

위의 기본 기능에만 충실한 로직을 만들어보면 아래와 같다.

public void upgradeLevels() {
		List<User> userList = userDao.getAll();
		for(User user:userList) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}

한결 코드의 가독성이 좋아졌다.

이제 필요한 메소드를 추가로 더 만들어 Service 클래스를 완성해보자.

public class UserService {
	@Autowired
	private UserDao userDao ;
	
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void upgradeLevels() {
		List<User> userList = userDao.getAll();
		for(User user:userList) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}
	private void upgradeLevel(User user) {
		// TODO Auto-generated method stub
		if(user.getLevel() == Level.BASIC) user.setLevel(Level.SILVER);
		else if(user.getLevel() == Level.SILVER) user.setLevel(Level.GOLD);
		userDao.update(user);
	}
	private boolean canUpgradeLevel(User user) {
		// TODO Auto-generated method stub
		Level currentLevel =user.getLevel();
		switch(currentLevel) {
			case BASIC: return (user.getLogin() >=50);
			case SILVER: return (user.getRecommend() >= 30);
			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);
	}
}

하지만, 코드 작성 후, 살펴보니, upgradeLevel(User user) 메소드의 문제점이 추가적으로 발견되었다. 

private void upgradeLevel(User user) {
// TODO Auto-generated method stub
if(user.getLevel() == Level.BASIC) user.setLevel(Level.SILVER);
else if(user.getLevel() == Level.SILVER) user.setLevel(Level.GOLD);
userDao.update(user);

위 메소드는 현재 레벨을 체크 후, 다음 단계로 변경하고 DB에 업데이트 하는 두가지 기능이 섞여있다. 이제 이것도 더 분리해보자. 먼저 레벨의 순서와 다음 단계 레벨이 무엇인지를 결정하는 일은 Level에게 맡기자. 레벨의 순서를 굳이 UserService에 담을 이유가 없다. 아래는 변경한 Level 코드이다. Level enum에 다음 단계 레벨을 저장 할 수 있는 next 필드를 추가했다.

public enum Level {
	GOLD(3,null),SILVER(2,GOLD),BASIC(1,SILVER);
	private final int value;
	private final Level next;
	
	private Level(int value,Level next) {
		this.value = value;
		this.next = next;
	}
	public int initValue() {
		return value;
	}	
	public static Level valueOf(int value)
	{
		switch(value) {
			case 1: return BASIC;
			case 2: return SILVER;
			case 3: return GOLD;	
			default: throw new AssertionError("Unknown value: "+ value);
		}
	}
	public Level nextLevel() {
		return next;
	}
}

이번엔 사용자 정보가 바뀌는 부부을 UserService에서 User로 바꿔보자. User클래스에 아래 코드만 추가해주면 된다.

public void upgradeLevel() {
		Level nextLevel = this.level.nextLevel();
		if(nextLevel ==null) {
			throw new IllegalStateException(this.level +"은 업그레이드가 불가능합니다."); 
		}else {
			this.level = nextLevel;
		}
	}

Level와 User 클래스에 upgradeLevel(User user)에 뒤섞여 있던 로직을 분리하고 나니 upgradeLevel(User user)의 코드가 한결 간결해졌다.

private void upgradeLevel(User user) {
		// TODO Auto-generated method stub
		user.upgradeLevel();	
		userDao.update(user);
	}

지금까지 개선한 코드를 살펴보면, 각 오브젝트와 메소드가 각각 자기 몫의 책임을 맡아 하는 구조로 만들어 졌음을 알 수있다. 객체 지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다. 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이다. 

리팩토링을 완료 하기 전에 한가지만 더 수정해 보도록 하자. 레벨 업그레이드 조건 체크시, 숫자로 체크할 경우 업그레이드 조건이 변경될때 매번 해당 숫자를 일일히 찾아서 수정해야 하는 번거로움이 있다. 이럴경우는 해당 숫자를 상수로 선언해 놓고 사용하는 것이 좋다.

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;
	
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void upgradeLevels() {
		List<User> userList = userDao.getAll();
		for(User user:userList) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}
	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);
	}
}

서비스 추상화 공부 정리는 여기까지 하겠다. 다음 글에서 트랜잭션 서비스 추상화에 대해 정리해보도록 하자.

'Spring' 카테고리의 다른 글

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

4장에서는 애플리케이션에서 사용할 수 있는 바람직한 예외처리 방법에 대해 소개 되어 있다. 이 장에서는 많은 내용이 있지 않으므로 간략하게 정리만 하고 넘어가도록하겠다. 

■ 예외를 잡아서 아무런 조취를 취하지 않거나 의미 없는 throws 선언을 해서는 안된다.
■ 예외는 복구 하거나 예외처리 오브젝트로 의도적으로 전달하거나 적절한 예외로 전환해야 한다.
■ 좀 더 의미 있는 예외로 변경하거나, 불필요한 catch/throws를  피하기 위해 러니타임 예외로 포장하는 두 가      지 방법의 예외 전환이 있다.
■ 복구 할 수 없는 예외는 가능한 한 빨리 런타임 예외로 전환하는 것이 바람직하다.
■ 애플리케이션의 로직을 담기 위한 예외는 체크예외로 만들어라JDBC의 SQLException은 대부분 복구할 수 없      는 예외이므로 런타임 예외로 포장해야 한다.
■ SQLException의 에러코드는 DB에 종속되기 때문에 DB에 독립적인 예외로 전환된 필요가 있다.
■ 스프링은 DataAccessException을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 계층을 제공한다. ■ DAO를 테이터 엑세스 기술에서 독립시키려면 인터페이스 도입과 런타임 예외 전환, 기술에 독립적인 추상화     된 예외로 전환이 필요하다. 

     아래 코드는 위 내용을 글로만 읽으니 ,이해가 어려워 코드에 직접 적용해봤다. 아래 코드를 보면 좀더 이해가 쉬울       듯하다. 주석으로 추가 설명을 해뒀으니 참고하자. 

	/*
	 * 4장 예외 공부하면서 적용해본 코드... 
	 * 무의미한 throws SQLException은 좋지 않다. 
	 * 차라리 unchecked Exception으로 포장 해서 던지는 것이 낫다. 
	 * 그리고 구체적으로 꼭 처리해야할  Exception인 경우는 예외 전환을 해주는 것이 좋다. 
	 * -> 이경우 메소드 옆에 throws을 꼭 해줘라. 그래야만 이 함수를 이용하는 다른 프로그래머가 구체적인 예외를 알수있다.
	 * -> 이런 에러도 RuntimeException을 상속 받도록 해서 임의로 unchecked Exception으로 만들어 둬라. 
	 *    그래야만 무의한 throws Exception 형식을 막을 수 있다. 
	 *    RuntimeException이라 하더라도 필요에 의해 try/catch throws를 사용하여 처리 할 수 있기 때문에 RuntimeException으로 처리 하는게 옳다. 
	 * */
	public void add(final User user) throws DuplicateUserIdException {	
		try {
			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;
						}
				 }				
			);
		}catch(SQLException e) {
			if(e.getErrorCode() == 1505) {
				throw new DuplicateUserIdException(e);
			}else {
				throw new RuntimeException(e);
			}
		}
	}
  •  

템플릿/콜백 예제를 하나 만들어보자. 

파일을 하나 열어서 모든 라인의 숫자를 더한 합을 돌려주는 코드를 만들어 보겠다. 다음과 같이 네 개의 숫자를 담고 있는 numbers.txt파일을 하나 준비한다.

1
2
3
4

이제 해당 파일의 경로를 주면 모든 숫자의 합을 돌려 주는 함수와 곱을 돌려주는 함수를 만들어 보자. 

package springbook.learningtest.template;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
//중복제거 
//-> 반복되지 않는 부분 추출 

public class Calculator {
	BufferedReader br = null;
	String num="";
	Integer result=0;

	public Integer calcSum(String filePath)throws IOException{		
		try {
			br=new BufferedReader(new FileReader(filePath));
			while((num = br.readLine()) != null) {
				result += Integer.valueOf(num);
			}
			return result;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println(e.getMessage());
			throw e;
		}finally {
			if(br != null) {
				try {
					br.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					System.out.println(e1.getMessage()) ;
				}
			}
		}		
	}
	public Integer calcMulti(String filePath) throws IOException{		
		try {
			br=new BufferedReader(new FileReader(filePath));
			while((num = br.readLine()) != null) {
				result *= Integer.valueOf(num);
			}
			return result;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println(e.getMessage());
			throw e;
		}finally {
			if(br != null) {
				try {
					br.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					System.out.println(e1.getMessage()) ;
				}
			}
		}		
	}
}

이제 아래의 순서로 진행해보자


  1. 중복의 제거와 템플릿/콜백설계
  2. 템플릿/콜백의 재설계
  3. 제네릭스를 이용한 콜백 인터페이스 

1. 중복의 제거와 템플릿/콜백설계

템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 중요하다. 지금부터 하나씩 찾아가며 진행해보자.

calcSum()함수와 calcMulti함수에서 바뀌는 부분만 따로 빼보자.

아래 코드는 바뀌는 부분을 따로 메소드 doSomethingWithReader()로 뺐다. 

public Integer doSomethingWithReader(BufferedReader br) throws NumberFormatException, IOException {
	Integer result=0;
	String num="";
	while((num = br.readLine()) != null) {
		result += Integer.valueOf(num);
	}		
	return result;
}
public Integer calcSum(String filePath)throws IOException{		
	try {
		br=new BufferedReader(new FileReader(filePath));
		result=doSomethingWithReader(br);
		return result;
	} catch (IOException e) {
		// TODO Auto-generated catch block
		System.out.println(e.getMessage());
		throw e;
	}finally {
		if(br != null) {
			try {
				br.close();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				System.out.println(e1.getMessage()) ;
			}
		}
	}		
}

하지만 해당 method는 calcSum()메소드에서만 사용이 가능하므로 다른 메소드에서 재사용이 불가능했다. 그래서 doSomthingwithReader() 함수를 Interface 클래스의 메소드로 만들어서 재사용이 가능하도록 만들었다. 

public interface BufferedReaderCallback {
	public Integer doSomethingWithReader(BufferedReader br)throws IOException;
}

 이번엔 변하지 않는 부분을 Template(Context와 같은 개념으로 보면된다)함수로 빼보자 

public Integer fileReaderTemplate(String filePath,BufferedReaderCallback callback) throws IOException {
	try {
		br=new BufferedReader(new FileReader(filePath));
		Integer result=callback.doSomethingWithReader(br);
		return result;
	} catch (IOException e) {
		// TODO Auto-generated catch block
		System.out.println(e.getMessage());
		throw e;
	}finally {
		if(br != null) {
			try {
				br.close();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				System.out.println(e1.getMessage()) ;
			}
		}
	}		
}
	

Template함수를 만들었다면 이제 calcSum() 메소드와 calcMulti()메소드도  Template에 전략 객체를 만들어 넘겨주도록 수정이 필요하다. 전략객체를 만드는 부분은 익명내부클래스를 사용해서 구현하였다. 

package springbook.learningtest.template;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Calculator {
	BufferedReader br = null;	
	
	public Integer fileReaderTemplate(String filePath,BufferedReaderCallback callback) throws IOException {
		try {
			br=new BufferedReader(new FileReader(filePath));
			Integer result=callback.doSomethingWithReader(br);
			return result;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println(e.getMessage());
			throw e;
		}finally {
			if(br != null) {
				try {
					br.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					System.out.println(e1.getMessage()) ;
				}
			}
		}		
	}
	public Integer calcSum(final String filePath)throws IOException{
		return fileReaderTemplate(filePath, 
					new BufferedReaderCallback(){
						@Override
						public Integer doSomethingWithReader(BufferedReader br) throws IOException{
							String num=null;
							Integer result=0;
							while((num = br.readLine()) != null) {
								result += Integer.valueOf(num);
							}		
							return result;
						}
					}
				);	
	}
	public Integer calcMulti(String filePath) throws IOException{		
		return fileReaderTemplate(filePath, 
					new BufferedReaderCallback(){
						@Override
						public Integer doSomethingWithReader(BufferedReader br) throws IOException{
							String num=null;
							Integer result=0;
							while((num = br.readLine()) != null) {
								result *= Integer.valueOf(num);
							}		
							return result;
						}
					}
				);	
	}
}

2. 템플릿/콜백의 재설계

중복 제거를 하면서 코드가 간략해졌다. 하지만 아직 중복 되는 부분이 존재한다.  중복되는 부분을 다시 한번 찾아서 템플릿/콜백 재설계를 해보자.

지금 부터 calcMulit()함수와 calcSum() 함수에서 callback을 생성 해주는 부분을 비교해보자.

//calcSum()
public Integer doSomethingWithReader(BufferedReader br) throws IOException{
	String num=null;
	Integer result=0;
	while((num = br.readLine()) != null) {
		result += Integer.valueOf(num);
	}		
	return result;
}
//calcMulti()
public Integer doSomethingWithReader(BufferedReader br) throws IOException{
	String num=null;
	Integer result=1;
	while((num = br.readLine()) != null) {
		result *= Integer.valueOf(num);
	}		
	return result;
}

반복되는 부분이 보이는가? 조금만 살펴보면 두코드가 상당 부분이 반복되고 있다는 것을 알수 있다. 아래에 반복되는 부분을 표시해 두었다. 

템플릿과 콜백을 찾을때는 변하는 코드의 경계를 찾고 그경계를 사이에 두고 주고받는 일정한 정보가 있는지 확인 하면된다고 했다.

위코드에서 바뀌는 부분(변하는 코드의 경계)은 사실 다섯번째 줄이다. (이외에 세번째 줄도 바뀌기는 하나, 이값은 Template에 파라미터로 넘겨줄것이다.)

result +=Integer.valueOf(num);

result *=Integer.valueOf(num);

다섯번째줄에 전달 되는 정보는 선언한 변수 값인 num 값이다. 

변하는 코드의 경계와 전달 되는 정보를 찾았으니, 콜백 인터페이스(전략에 해당된다.)를 다시 만들어보자. 

public interface LineCallback {
	public Integer doSomethingWithLine(Integer result,String num) ;
}

 Template(Context)  와 calcSum(), calcMulti() 메소드를 LineCallback 객체(전략)을 생성하여 Template(Context)에 넘겨주도록 수정해보자.  아래 코드를 보면 filereaderTemplate() 메소드에 파라미터로 initVal를 넘겨주고 있다. 이 파라미터는 result의 초기값을 설정해준다.  

public class Calculator {
	BufferedReader br = null;	
	
	public Integer lineReaderTemplate(String filePath,LineCallback callback,int initVal) throws IOException {
		try {
			br=new BufferedReader(new FileReader(filePath));
			String num=null;
			Integer result=initVal;
			while((num = br.readLine()) != null) {
				result=callback.doSomethingWithLine(result, num);
			}		
			return result;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println(e.getMessage());
			throw e;
		}finally {
			if(br != null) {
				try {
					br.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					System.out.println(e1.getMessage()) ;
				}
			}
		}		
	}
	public Integer calcSum(final String filePath)throws IOException{
		return lineReaderTemplate(filePath, 
    				new LineCallback() {
                       public Integer doSomethingWithLine(Integer result,Integer num) {
                          return result++Integer.parseInt(num);
                       }},0);	
	}
	public Integer calcMulti(String filePath) throws IOException{		
		return lineReaderTemplate(filePath, 
        			new LineCallback() {
                       public Integer doSomethingWithLine(Integer result,Integer num) {
                         return result*+Integer.parseInt(num);
                       }},1);		
	}
}

처음 템플릿/ 콜백 설계 했을때는 파일의 각 라인을 루프로 돌면서 가져오는 부분도 전략에 포함 시켰으나, 템플릿/콜백 재설계를 하면서 이부분도 Template(Context)로 빼버렸다. 각 라인의 내용을 가지고 계산하는 작업만 전략으로 빼도록 재설계 했다.   

3. 제네릭스를 이용한 콜백 인터페이스 

지금까지 사용한 LineCallback와 lineReadTemplate()은 Integer로 고정되어 있다. 만약 파일의 라인단위를 처리해서 다양한 결과의 타입을 가져가고 싶다면 어떻게 하면 될까? 자바에서는 제네릭스를 이용하면 된다.

한 예로 파일의 각 라인을 읽어서 하나의 스트링으로 돌려주는 기능이 추가된다고 해보자. 이번에는 템플릿이 리턴하는 타입이 스트링이어야 한다. 콜백의 작업 결과도 스트링이어야 한다. 그럼 지금부터 제네릭스를 이용할수 있도록 수정해보자. 

제네릭스를 이용하여 전략 Interface를 변경했다.

public interface LineCallback<T> {
	public T doSomethingWithLine(T result,T num) ;

}

LineCallback와 lineReadTemplate()도 변경해보자.

public class Calculator {
	BufferedReader br = null;	
	
	public <T> T lineReaderTemplate(String filePath,LineCallback<T> callback,T initVal) throws IOException {
		try {
			br=new BufferedReader(new FileReader(filePath));
			String num=null;
			T result=initVal;
			while((num = br.readLine()) != null) {
				result=callback.doSomethingWithLine(result, num);
			}		
			return result;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println(e.getMessage());
			throw e;
		}finally {
			if(br != null) {
				try {
					br.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					System.out.println(e1.getMessage()) ;
				}
			}
		}		
	}
	public Integer calcSum(final String filePath)throws IOException{					
		return lineReaderTemplate(filePath, new LineCallback<Integer>() {
									@Override
									public Integer doSomethingWithLine(Integer result, String num) {
										// TODO Auto-generated method stub
										return result + Integer.parseInt(num);
									}},0);	
	}
	public Integer calcMulti(String filePath) throws IOException{		
		return lineReaderTemplate(filePath, new LineCallback<Integer>() {
										@Override
										public Integer doSomethingWithLine(Integer result, String num) {
											// TODO Auto-generated method stub
											return result * Integer.parseInt(num);
										}},1);		
	}
}

  이제 마지막으로 파일의 각 라인을 읽어서 하나의 스트링으로 돌려주는 기능을 새로 추가해보자. 

public String concentrate(String filePath) throws IOException{
		return lineReaderTemplate(filePath, new LineCallback<String>() {
			@Override
			public String doSomethingWithLine(String result, String value) {
				// TODO Auto-generated method stub
				return result + value;
			}},"");		
}

3장 템플릿 내용을 다 적용해 볼 수 있는 코드를 만들어 보았다. 이 코드를  직접 만들어 본다면 3장 템플릿의 주요 핵심 개념인 템플릿/콜백을 정확히 이해 할 수 있을 것이다. 

'Spring' 카테고리의 다른 글

5장 서비스 추상화-I  (0) 2020.01.14
4장 예외  (0) 2020.01.05
3장 템플릿 III - 스프링의 JdbcTemplate  (0) 2020.01.04
3장 템플릿 II- 템플릿/ 콜백패턴  (0) 2020.01.04
3장 템플릿I - 전략패턴  (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

앞에 글에서 전략 패턴으로 UserDao를 변경해 봤다.  전략패턴의 기본구조에 익명 내부 클래스를 활용한 방식이었다. 이런방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다. 전략 패턴의 컨텍스트를 템틀릿, 전략을 콜백이라고 부른다. 템를릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙여진 이름이다. 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트이다. 

템플릿
템플릿(template)은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 학생들이 도형을 그릴때 사용하는 도형자 또는 모양자가 바로 템플릿이다. 프로그래밍에서는 고정된 틀 안에 바꿀수 있는 부분을 넣어서 사용하는 경우에 템플릿이라고 부른다. JSP는 HTML이라는 고정된 부분에 EL과 스크립릿이라는 변하는 부분을 넣은 일종의 템플릿 파일이다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분을 서브클래스의 메소드에 두는 구조로 이루어진다. 

콜백
콜백(callback)은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행 시키키 위해 사용한다. 자바에선 메소드 자체를 파라미터로 전달할 방법은 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 Functional Object라고 한다. 

템플릿/콜백의 동작원리

1.템플릿/콜백 특징


  • 단일 메소드 인터페이스를 사용
  • 콜백은 하나의 메소드를 가진 인퍼페이스를 구현한 익명 내부 클래스 만들어짐
  • 콜백 인터페이스의 메소드는 보통 파라미터가 있음

여러개의 메소드를 가진 인터페이스를 사용 사용할 수 있는 전랙패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한번 호출되는 경우가 일반적이기 때문이다.콜백은 일반적으로 하나의 메소드를 가진 인퍼페이스를 구현한 익명 내부 클래스로 만들어진다. 콜백 인터페이스의 메소드는 보통 파라미터가 있다. 템플릿의 작업 흐름중에 만들어지는 컨테스트 정보를 전달 받을때 사용된다. ( UserDao를 전략 패턴으로 변경 했을때 전략클래스의 메소드를 작성시  Context로 부터 connection을 파라미터로 전달 받았었다. )템플릿/콜백 방식은 전략 패턴과 DI장점을 익명 내부 클래스 사용 전략과 결합한 독특한 확용법이다. 

아래 그림은 템플릿/콜백의 작업흐름을 보여준다.  

2. 콜백의 분리와 재활용 

이번에는 복잡한 익명 내부클래스 사용을 최소화 할 수 있는 방법을 찾아보자. 

위 그림에서 처럼 변하는 부분은 변하지 않는 부분에서 분리 시켜 보자. 아래 반영한 코드를 작성해 두었다. 

//반복되는 부분 분리 
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");
}	

그런데 executeSQL() 메소드는 다른 DAO에서도 사용할수 있으므로 템플릿 클래스 안으로 옮겨보자. 

package springbook.user.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import javax.sql.DataSource;

public class JdbcContext {
	private DataSource dataSource;
	
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void workWithStatementStrategy(StatementStrategy strategy) throws SQLException{
		Connection c = null;
		PreparedStatement ps = null;
		try {
			c = dataSource.getConnection();	
			ps=strategy.makeStatement(c); // <-- 이부분이 바뀌는 부분이다.. "전략" 적용하는 부분 
			ps.executeUpdate();
			
		}catch(SQLException e) {
			throw e;
		}finally {
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
	}	

	//반복되는 부분 분리 
	public void executeSQL(final String strSQL) throws SQLException{
		workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makeStatement(Connection c) throws SQLException {
					PreparedStatement ps=c.prepareStatement(strSQL);
					return ps;
				}
			}				
		);	
	}
}

 deleteAll()함수도 수정했다.

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

콜백 재활용을 적용한 JdbcContext를 아래 그림으로 나타낼수 있다. 

이번에는 좀더 복잡한 add()메소드에도 적용해보자. add()메소드에는 PreparedStatement에 바인딩될 파라미터 내용이 추가 되어야 한다. 그래서 자바5에서 부터 제공하는 가변인자를 활용했다. 

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

아래는 JdbcContext의 executeSQL에 가변인자를 추가한 코드다. 

public void executeSQL(final String strSQL, final String ...args) throws SQLException{		
	workWithStatementStrategy(
		new StatementStrategy() {
			public PreparedStatement makeStatement(Connection c) throws SQLException {
				int i=1;
				PreparedStatement ps=c.prepareStatement(strSQL);
				for(String a: args) {
					ps.setString(i++, a);
				}
				return ps;
			}
		}				
	);	
}

+ Recent posts