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

파일을 하나 열어서 모든 라인의 숫자를 더한 합을 돌려주는 코드를 만들어 보겠다. 다음과 같이 네 개의 숫자를 담고 있는 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

템플릿이란 코드에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로 부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.  쉽게 생각해서 코드에서 변하는 부분과 변하지 않는 부분을 분리시키자라는 의미이다.

아래 코드를 한번보자.  deleteAll() 함수와 add함수에서 함수별로 반복적으로 나타나는 부분(변화되지 않는 부분) 과 함수별로 다른 부분을 찾을수 있다.

두함수의 변하는 부분을 템플릿 메소트 패턴을 이용하여  따로 빼보자.

하지만 여기서도 문제가 존재한다. 새로운 DAO logic이 추가될때 마다 상속을 통해 새로운 클래스를 만들어한다는 것이다.

 

그래서 이번에는 전략패턴을 적용해 보도록 하자.  전략패을 적용하기 전에 용 변하는 부분을 인페이스로 만들어 보자.

시작전에  Context는 변하지 않는 부분, 전략은 바뀌는 부분  <- 이것을 머리에 넣어두고 아래글을 보면 더 이해가 쉽다.

deleteAll() 컨텍스트를 정리해보면 다음과 같다.

  • DB커넥션 가져오기
  • PreparedStatement를 만들어줄 외부기능 호출하기
  • 전달받은 PreparedStatement 실행하기
  • 예외가 발생하면 이를 다시 메소드 밖으로 던지기
  • 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기

여기서 두번째 PreparedStatement를 만들어줄 외부기능 <- 이부분이 전략패턴에서 전략이라고 볼수 있다.  전략패턴의 구조를 따라 이부분을 인터페이스로 만들어 인터페이스메소드를 통해 PreparedStatement를 생성하는 전략을 호출해주면 된다. 이때 PreparedStatement를 생성할때 Context에서 만든 DB커넥션을 전달해줘야 한다.

전략 interface를 다음과 같이 만들어보자.

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

public interface StatementStrategy {
	public PreparedStatement makeStatement(Connection c) throws SQLException; 
}

전략  interface를 상속받아 실제 전략 클래스를 만들면 다음과 같다.

public class DeleteAllStatement implements StatementStrategy {

	@Override
	public PreparedStatement makeStatement(Connection c) throws SQLException {
		// TODO Auto-generated method stub
		PreparedStatement ps=c.prepareStatement("delete from users");
		return ps;
	}
}

이제 delete전략 클래스를 실제 deleteAll메소드에 적용해 보도록 하자.

public void deleteAll() throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		StatementStrategy strategy= new DeleteAllStatement();
		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) {
					
				}
			}
		}
	}	

그런데 컨텍스트 안에 이미 구체적인 적략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다. 컨텍스트 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는건, 전략패턴에도 OCP(Open Closed Principle)에도 맞지 않다.

DI 적용을 위한 클라이언트/컨텍스트 분리

전략패턴에 따르면 컨텍스트가 어떤 전략을 사용할지는 클라인트가 결정하는게 일반적이다. 클라이언트가 구체적인 전략 하나를 오브젝트로 만들어 컨텍스트에 전달해준다. 그러면 컨텍스트는 전달받은 오브젝트를 사용하게된다. 이를 표현 한것이 아래 그림이다.

결국 이구조에서 전략 오브젝트를 생성하고 컨텍스트로 전달 하는 것은 Object Factoty(client)이며, 이것이 바로 의존관계 주입(DI)에 해당된다. 이제 이패턴을 적용시켜보자. 아래 코드를 다시 살펴보면 try/catch/finally 부분이 context에 해당되며 StatementStrategy strategy= new DeleteAllStatement();  이부분이 전략에 해당된다.

public void deleteAll() throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		StatementStrategy strategy= new DeleteAllStatement();
		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) {
					
				}
			}
		}
	}	

 

이제 context를 함수로 분리해보자. context은 전략을 전달 받아야 하니, 메소드 작성시  파라미터로 전략을 넣어주자.

public void jdbcContextWithStatementStrategy(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) {
					
				}
			}
		}
	}

이제 클라이언트에 해당하는 deleteAll()부분도 수정해보자.  여기서 보면, 전략 객체 strategy를 만들어 Context에 넘겨 주고 있음을 확인할 수 있다. ( 이것이 바로 DI 이다)

public void deleteAll() throws SQLException {		
		StatementStrategy strategy= new DeleteAllStatement(); //1.선택한 전략 클래스의 오브젝트를 생성  
		jdbcContextWithStatementStrategy(strategy);           //2. 선택한 전략을 context에 넣어준다. 		
}	

이번에는 add()함수에서 사용할 AddStatement를 만들어보자.  

전략 Interface StatementStartegy 상속받은 AddStatement 클래스를 하나 만들었다.  그런데 여기서 DeleteAllStatement전략 클래스와 다른점이 있다. 바로 Usesr정보이다. 클라이언트에서 User정보를 넘겨주어야 한다. 그래서 생성자로 User정보를 넘겨받도록 수정하였다. 

public class AddStatement implements StatementStrategy {
	User user;
	public AddStatement(User user) {
		super();
		this.user = user;
	}
	@Override
	public PreparedStatement makeStatement(Connection c) throws SQLException {
		// TODO Auto-generated method stub
		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;
	}
}

 이제 AddStatement 전략 클래스를 Context로 넘겨주도록 add() 함수를 수정 하면 아래와 같다. 전략 AddStatement 의 객체 생성시 user정보를 넘겨주었다. 그리고 strategy를 넘겨주어 PreparedStatement를 받고 있음을 확인할 수 있다.

public void add(User user) throws SQLException {
    // 1.선택한 전략 클래스의 오브젝트를 생성  , user정보 넘겨줌
	StatementStrategy strategy = new AddStatement(user);  
    //2. 선택한 전략을 context에 넣어준다. 
	jdbcContextWithStatementStrategy(strategy);
	
}

그런데 지금까지 해온 방법에는 두가지 문제가 있다.

  1. DAO 메소드마다 새로운 StaementStrategy 구현 클래스를 만들어야 하는점
  2. User와 같이 부가적인 정보가 있을때 전략클래스에 오브젝트를 전달 받는 생성자와 이를 저장할 인스턴스 변수를 생성해야 한다는 점

지금부터 이 두가지 문제를 해결해 보도록 하자.

첫번째 문제의 해결법은 간단하다. 아래와 같이 로컬 클래스를 사용하면 된다. 그러면 매번 전략 클래스을 따로 만들 필요가 없다.  다음 해결법을 시작하기전에 아래 add()함수를 한번 더보자. Addstatement 클래스 안에 user정보를 받는 생성자가 없다. 로컬 클래스를 사용하면 자신이 선언된 곳의 변수를 직접 사용할수 있기 때문이다.단, 로컬클래스에서 외부 변수를 사용할때는 final로 선언해줘야 한다. 그래서 add()함수의 파라미터 앞에 final을 선언했다. (user은 외부변수다.) 

public void deleteAll() throws SQLException {		
	class DeleteAllStatement implements StatementStrategy {
		@Override
		public PreparedStatement makeStatement(Connection c) throws SQLException {
			PreparedStatement ps=c.prepareStatement("delete from users");
			return ps;
		}
	}
	StatementStrategy strategy= new DeleteAllStatement(); // 이부분을 로컬 클래스로 변경
	jdbcContextWithStatementStrategy(strategy);		
}	
public void add(final User user) throws SQLException {
	class AddStatement implements StatementStrategy {
		@Override
		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;
	    }
	}
	StatementStrategy strategy = new AddStatement();  
	jdbcContextWithStatementStrategy(strategy); 
}

여기서 좀더 간단하게 고쳐보자. AddStatement와 DeleteAllStatement 클래스는 해당 메소드에서만 사용되는 클래스이이므로 간결하게 클래스 이름도 제거해보자. 익명 클래스를 이용하면 가능하다. 

익명 내부 클래스는  이름을 갖지 않는 클래스이다. 클래스 선언과 오브젝트 생성이 결합된 형태로 만들어지며, 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 다음과 같은 형태로 만들어 사용한다. 클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다.
 
new 인터페이스 이름( ) { 클래스 본문 }

아래는 익명 클래스로 수정한 코드다. 코드가 한결 간결해졌다.

public void deleteAll() throws SQLException {		
	jdbcContextWithStatementStrategy(
		new StatementStrategy() {
			public PreparedStatement makeStatement(Connection c) throws SQLException {
				PreparedStatement ps=c.prepareStatement("delete from users");
				return ps;
			}
		}				
	);		
}	
public void add(final User user) throws SQLException {	
	jdbcContextWithStatementStrategy(
		 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;
				}
		 }				
	);
}

 

JDBC Context의 분리 

우리가 지금까지 UserDao클래스 내부에서 사용했던 jdbcContextWithStatementStrategy() 메소드는 JDBC의 일반적인 작업 흐름을 담고 있기때문에 다른 DAO에서도 사용할 가능성이 크다. UserDao클래스 밖으로 독립시켜서 모든 DAO가 사용가능 하도록 분리해보자. JdbcContext 클래스를 만들고  jdbcContextWithStatementStrategy() 메소드를workWithStatementStrategy() 메소드로 이름만 변경했다.

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) {
					
				}
			}
		}
	}	
}

 그리고 deleteAll()메소드와 add()메소드에서 DI가 일어나는 부분의 함수 호출 부분을 아래와 같이 변경되었다.  

public class UserDao{
	JdbcContext jdbcContext;	
	public void setDataSource(DataSource dataSource) {		
		this.jdbcContext = new JdbcContext();	 
		this.jdbcContext.setDataSource(dataSource);
		this.dataSource = dataSource;
	}
	public void deleteAll() throws SQLException {		
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makeStatement(Connection c) throws SQLException {
					PreparedStatement ps=c.prepareStatement("delete from users");
					return ps;
				}
			}				
		);	
	}	
	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;
					}
			 }				
		);
	}
}

스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다. 하지만 JdbcContext는 구현방법이 바뀔 가능성이 없다. 따라서 인터페이스로 구현하지 않았다. 그리고 한가지 재미 있는 점은 UserDAO에서 JdbcContext 에 직접 dataSource를 DI 해주고 있다는 점이다. 이 방법은 스프링에서 종종 사용되는 기법이다.

+ Recent posts