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

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

앞에 글에서 전략 패턴으로 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;
			}
		}				
	);	
}

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

아래 코드를 한번보자.  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