Pattern - Template Method
너무 욕심내지 마라. 천천히 그리고 꾸준히 하는게 답이다.
의도
객체의 연산이 알고리즘의 뼈대 만들 정의하고 각 단계에서 수행할 구체적 처리는 서브 클래스 쪽에서 미룹니다. 알고리즘의 구조 자체는 그대로 놔둔 채 알고리즘 각 단계의 처리를 서브 클래스에서 처리할 수 있게 합니다.
동기
Application 클래스와 Draw 클래스를 제공하는 응용 프로그램 프레임워크를 생각해 봅시다. Application 클래스는 파일이나 특정한 외부 형식으로 저장된 문서를 열 수 있고, Document 객체를 파일에서 읽은 문서에 정보를 나타냅니다. 특정한 요구에 따라서 프레임워크 정의한 Application 클래스와 Document 클래스를 상속한 서브 클래스를 정의하여 새로운 응용 프로그램을 구축할 수 있을 것입니다. 예를 들어, 그림 그리기 응용 프로그램은 DrawApplication 클래스와 DrawDocument 클래스를 정의할 수 있을 것이고, 스프레드 시트 응용프로그램은 SpreadsheetApplication 클래스와 SpreadsheetDocument 클래스를 정의할 수 있습니다. 이와 같은 패턴을 구현할 때 사용하는 것이 탬플릿 메서드라고 합니다.
활용성
- 어떤 한 알고리즘을 이루는 부분 중 변하지 않는 부분을 한 번 정의해 놓고 다양해질 수 있는 부분을 서브 클래스에서 정의할 수 있도록 남겨주고자 할 때
- 서브 클래스 사이의 공통적인 행동을 추출하여 하나의 공통 클래스에 몰아둠으로써 코드 중복을 피하고 싶을 때. 먼저, 기존 코드에서 나타나는 차이점을 뽑아 이를 별도의 새로운 연산들로 구분해 놓습니다. 그런 뒤 달라진 코드 부분을 이 새로운 연산을 호출 하는 템플릿 메서드로 대체하는 것입니다.
- 서브 클래스의 확장을 제어할 수 있습니다. 템플릿 메서드가 어떤 특정한 시점에 “훅(hook)” 연산을 호출하도록 정의함으로써, 그 특정 시점에서만 확장되도록 합니다.
항목에 대한 설명
-
AbstractClass
Application
서브 클래스들이 재정의를 통해 구현해야 하는 알고리즘 처리 단계 내의 기본 연산을 정의합니다.
그리고 알고리즘의 뼈대를 정의하는 템플릿 메서드를 구현합니다. 템플릿 메서드는 AbstractClass 에 정의된 연산 또는 다른 객체 뿐만 아니라 기본 연산도 호출합니다. -
ConcreteClass
MyApplication
서브 클래스마다 달라진 알고리즘 처리 단계를 수행하기 위한 기본 연산을 구현합니다.
협력방법
- ConcreteClass는 AbstractClass를 통하여 알고리즘의 변하지 않는 처리 단계를 구현합니다.
결과
- 템플릿 메서드는 “할리우드 원칙”이라는 제어 역전의 구조를 끌어냅니다. “전화하지 마세요. 우리가 연락할게요.” 라는 것입니다. 다시 말해, 부모 클래스는 서브 클래스에 정의된 연산을 호출할 수 있지만 반대 방향의 호출은 아닙니다.
- 구체 연산 : ConcreteClass 나 사용자 클래스에 정의된 연산
- AbstractClass 구체 연산 : 서브 클래스에서 일반적으로 유용한 연산
- 기본 연산 : 추상화된 연산
- 팩토리 메서드
- 훅 연산 : 필요하다면 서브클래스에서 확장할 수 있는 기본 행동을 제공하는 연산. 기본적으로는 아무 내용도 정의하지 않습니다.
템플릿 메서드 패턴에서는 어떤 연산이 훅 연산인지(오버라이드 가능한지) 추상연산인지(꼭 오버라이드 해야하는지)를 지정해 두는 것이 대단히 중요합니다. 훅 연산은 나중에 재정의할 수 있고, 재정의 하지 않을 수도 있는 메서드이고, 추상 연산은 반드시 재정의해야 하는 연산입니다. 추상할 클래스를 효과적으로 재사용하기 위해서, 서브 클래스 작성자는 어떤 연산들이 오버라이드용으로 설계되었는지를 정확하게 이해하고 있어야 합니다.
코드 예제 – 기본 샘플
public class Caller {
public static void main(String[] args) {
DBTemplate dbTemplate = new OracleDB();
List<Map> resutList = (List<Map>)dbTemplate.execute("SELECT * FROM TB_ZZ_USER");
resutList.forEach(value -> {
System.out.println(value.get("USER_ID"));
});
}
}
public abstract class DBTemplate {
public Object selectQuery(String sql){
return execute(sql);
}
public List<Map> execute(String queryStatment){
ResultSet resultSet = null;
List<Map> resultList = null;
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
String[] connectionInfo = getDBInformation();
connection = getConnection(connectionInfo);
preparedStatement = executeStatement(connection, queryStatment);
resultSet = getResultSet(preparedStatement);
resultList = getResultMapRows(resultSet);
}catch (Exception ex){
ex.printStackTrace();
}finally {
try{
if(connection != null ) releaseConnection(connection);
if(preparedStatement != null ) releasePrepareStatement(preparedStatement);
if(resultSet != null ) releaseResultSet(resultSet);
}catch (Exception ex){
ex.printStackTrace();
}
}
return resultList;
}
/**
* ResultSet을 Row마다 Map에 저장후 List에 다시 저장.
* @param rs DB에서 가져온 ResultSet
* @return Listt<map> 형태로 리턴
* @throws Exception Collection
*/
private List<Map> getResultMapRows(ResultSet rs) throws Exception
{
// ResultSet 의 MetaData를 가져온다.
ResultSetMetaData metaData = rs.getMetaData();
// ResultSet 의 Column의 갯수를 가져온다.
int sizeOfColumn = metaData.getColumnCount();
List<Map> list = new ArrayList<Map>();
Map<String, Object> map;
String column;
// rs의 내용을 돌려준다.
while (rs.next())
{
// 내부에서 map을 초기화
map = new HashMap<String, Object>();
// Column의 갯수만큼 회전
for (int indexOfcolumn = 0; indexOfcolumn < sizeOfColumn; indexOfcolumn++)
{
column = metaData.getColumnName(indexOfcolumn + 1);
// map에 값을 입력 map.put(columnName, columnName으로 getString)
map.put(column, rs.getString(column));
}
// list에 저장
list.add(map);
}
return list;
}
private void releaseConnection(Connection connection) throws SQLException {
connection.close();
}
private void releaseResultSet(ResultSet resultSet) throws SQLException {
resultSet.close();
}
private void releasePrepareStatement(PreparedStatement preparedStatement) throws SQLException {
preparedStatement.close();
}
public abstract String[] getDBInformation();
public abstract Connection getConnection(String[] connectionInfo) throws SQLException;
public abstract PreparedStatement executeStatement(Connection connection, String sql) throws SQLException;
public abstract ResultSet getResultSet(PreparedStatement preparedStatement) throws SQLException;
}
public class OracleDB extends DBTemplate {
@Override
public String[] getDBInformation() {
String jdbcDriver = "jdbc:oracle:thin:@***.***.***.***:***/*****";
String dbUser = "******";
String dbPassword = "******";
String[] resultInfo = {jdbcDriver, dbUser, dbPassword};
return resultInfo;
}
@Override
public Connection getConnection(String[] connectionInfo) throws SQLException {
return DriverManager.getConnection(connectionInfo[0], connectionInfo[1], connectionInfo[2]);
}
@Override
public PreparedStatement executeStatement(Connection connection, String sql) throws SQLException {
return connection.prepareStatement(sql);
}
@Override
public ResultSet getResultSet(PreparedStatement preparedStatement) throws SQLException {
return preparedStatement.executeQuery();
}
}