3. 템플릿
객체 지향 프로그래밍 원칙 중 OCP, 확장에는 열려 있고 변경에는 닫혀 있으려면 코드에서 서로 다른 역할을 하는 부분을 확실하게 구분해 주는 것이 중요하다.
템플릿이란 이렇게 성질이 다른 코드들 중에 변경이 잘 일어나지 않고 패턴으로 정리되는 부분을 자유롭게 변경되는 부분으로 부터 독립시키는 기법이다.
템플릿을 적용하는 방법은 상속을 사용하는 템플릿 메소드 패턴과 인터페이스를 사용하는 전략 패턴이 있다. 우리는 더 나은 방법인 전략 패턴을 사용할 것이다.
전략 패턴으로 템플릿 기법 구현하기
public class UserDao {
private DataSource connectionMaker;
public UserDao() {
}
public void add(final User user) throws SQLException {
jdbcContextWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("INSERT INTO user (id, name, password) VALUES (?, ?, ?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
});
}
private void jdbcContextWithStatementStrategy(StatementStrategy statementStrategy) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = connectionMaker.getConnection();
ps = statementStrategy.makeStatement(c);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
throw e;
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
위 코드에서 jdbcContextWithStatementStrategy 메소드는 공통된 템플릿이라고 할 수 있는 로직이고, 실제로 바뀌는 부분은 StatementStrategy 인터페이스를 구현한 실제 전략 클래스들로 클라이언트 코드(add 메소드)에서 주입받고 있음을 알 수 있다.
즉 위 코드는 전략 패턴을 DI를 활용해서 구현한 예제이다. DI란 의존성의 결정과 주입을 결정하는 주체를 외부로 빼내는 것이다.
public class Calculator {
public Integer calcSum(String path) throws IOException {
LineReadCallback lineReadCallback = new LineReadCallback<Integer>() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(path, 0, lineReadCallback);
}
public Integer calcMult(String path) {
LineReadCallback lineReadCallback = new LineReadCallback<Integer>() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value * Integer.valueOf(line);
}
};
return lineReadTemplate(path, 1, lineReadCallback);
}
private <T> T lineReadTemplate(String path, T initialValue, LineReadCallback<T> lineReadCallback) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
T value = initialValue;
String line = null;
while ((line = br.readLine()) != null) {
value = lineReadCallback.doSomethingWithLine(line, value);
}
return value;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
위 코드 역시 템플릿/콜백 패턴을 구현한 다른 예제로, 에러 처리와 값 계산을 line마다 누적시키는 공통 로직을 템플릿 메소드로 빼내고, 콜백 로직은 공통의 인터페이스를 구현한 익명 클래스로 정의하여 각 클라이언트 메소드에서 콜백을 생성하여 템플릿 메소드에 주입하여 구현하고 있다.
JDBC 제공 템플릿 활용
public class UserDao {
private JdbcTemplate jdbcTemplate;
public UserDao() {
}
public void add(final User user) throws SQLException {
jdbcTemplate.update("INSERT INTO user (id, name, password) VALUES (?, ?, ?)", user.getId(), user.getName(), user.getPassword());
}
public User get(String id) throws SQLException {
return jdbcTemplate.queryForObject("SELECT * FROM user WHERE id = ?", new Object[]{id}, new RowMapper<User>() {
@Override
public User mapRow(ResultSet resultSet, int i) throws SQLException {
User user = new User();
user.setId(resultSet.getString("id"));
user.setName(resultSet.getString("name"));
user.setPassword(resultSet.getString("password"));
return user;
}
});
}
public void deleteAll() throws SQLException {
jdbcTemplate.update("DELETE FROM user");
}
public List<User> getAll() {
return jdbcTemplate.query("SELECT * FROM user ORDER BY id", new RowMapper<User>() {
@Override
public User mapRow(ResultSet resultSet, int i) throws SQLException {
User user = new User();
user.setId(resultSet.getString("id"));
user.setName(resultSet.getString("name"));
user.setPassword(resultSet.getString("password"));
return user;
}
});
}
public int getCount() throws SQLException {
return jdbcTemplate.query("SELECT COUNT(*) FROM user ", new ResultSetExtractor<Integer>() {
@Override
public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException {
resultSet.next();
return resultSet.getInt(1);
}
});
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
위 코드는 JdbcTemplate 이라고 하는 JDBC 제공 템플릿 클래스를 활용해 코드를 리팩토링 한 것이다.
제네릭을 이용해 콜백의 리턴 타입을 다이내믹하게 정할 수 있다.
템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 신경을 써야 한다.
결론
템플릿/콜백은 실제 스프링에서 몹시 흔하게 사용되는 객체지향 패턴이다. 스프링이 제공하는 템플릿/콜백을 잘 사용할 뿐 아니라 직접 템플릿/콜백을 만들어 쓸 수 있어야 한다.