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
Object.keys(객체명).length 

프로젝트를 진행하다가 JSON객체의 길이를 구해야 할때가 있었다. array 길이를 구할때 length를 사용했던것만  생각하고 계속해서 객체명.length를 했더니오류가 났다. 해결법은 Object.keys(객체명). length 였다. 

JSON 설명에 대한 링크는 아래에 걸어두었다. 

2020/01/09 - [Java script] - JSON이란..

'Java script' 카테고리의 다른 글

JSON이란..  (0) 2020.01.09

데이터 전달의 한 방법으로 많이 사용되고 있는 JSON에 대해서 공부해보자.

1. JSON의 개념

JSON(JavaScript Object Notation)은 속성-값 쌍 또는 "키-값 쌍"으로 이루어진 데이터 오브젝트를 전달하기 위해 인간이 읽을 수 있는 텍스트를 사용하는 개방형 표준 포맷이다. 

2. JSON의 자료형과 문법 기본 자료형 

기본자료형

 수(Number)  

  •  정수   

     8진수 ,16진수 표현하는 방법은 지원하지 않는다.

74
1974
750
-114
-273
  •  실수(고정 소수점)
3.14
-2.718
  • 실수(부동 소수점)

문자열(String)

항상 큰 따옴표(")로 묶어야 하며, 그 안에는 유니코드 문자들이 나열된다. 유니코드 중 역슬래시(\)와 큰따옴표(")는      바로 사용할 수 없다. 역슬래시는 제어문자를 표현하기 위해 사용되며 다음과 같은 의미를 지닌다.

\b 백스페이스
\f 폼 피드
\n 개행
\r 캐리지 리턴
\t 탭
\" 따옴표
\/ 슬래시\
\ 역슬래시
\uHHHH 16진수 네자리로되어 있는 유니코드 문자
"1234"
"Love"
"O-matic"
"한글"
"\"JSON\""

 

배열(Array)

배열은 대괄호[ ]로 나타낸다. 배열의 각 요소는 기본 자료형이거나 배열, 객체이다. 각 요소들은 쉼표(,)로 구별된다.      각 요소가 나타나는 순서에 의미가 있다.

1   [10, {"v": 20}, [30, "마흔"]]

객체(Object)

객체는 이름/값 쌍의 집합으로, 중괄호{ }를 사용한다. 이름은 문자열이기 때문에 반드시 따옴표를 하며,값은 기본 자료형이다. 각 쌍들은 쉼표(,)로 구별된다. 각 쌍이 나오는 순서는 의미가 없다.

 {"name2": 50, "name3": "값3", "name1": true}
예제

다음은 한 사람에 관한 정보를 갖는 JSON 객체이다. 키-값 쌍(이름:값)의 패턴으로 표현된다.

 {
    "이름": "홍길동",
    "나이": 25,
    "성별": "여",
    "주소": "서울특별시 양천구 목동",
    "특기": ["농구", "도술"],
    "가족관계": {"#": 2, "아버지": "홍판서", "어머니": "춘섬"},
    "회사": "경기 수원시 팔달구 우만동"
 }

'Java script' 카테고리의 다른 글

Java Script에서 JSON객체의 length 길이 구하기  (0) 2020.01.09

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

가변인자 사용법

키워드 ...을 사용한다. 

void print(String ...str){
	for(String a:str){
    	System.out.println(a);
    }
}

void test(){
    print();
    print("a","b","c);
    print(new String[2]);
}

빈 인자값이나 같은 타입에 배열도 인자값으로 줄수 있다. 코드를 보면 알수 있지만 가변인자는 내부적으로 배열을 생성해서 사용한다. 그래서 가변인자를 난발해서는 안된다. 그리고 가변인자외에도 다른 매개변수가 더 있다면 가변인자는 항상 마지막에 선언해야 한다. 

 

 

'JAVA' 카테고리의 다른 글

익명내부클래스  (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