3.5) 템플릿과 콜백
템플릿/콜백의 동작원리
- 템플릿/콜백 패턴이란?
- 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부만 자주 바꿔서 사용해야 하는 경우에
적합한 구조이며 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식이다. - 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.
템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이며
콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다. - 여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리
템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다.
템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다. - 콜백 인터페이스의 메소드에는 보통 파라미터가 있으며 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받는다.
- 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부만 자주 바꿔서 사용해야 하는 경우에
- 템플릿/콜백(DI 방식의 전략 패턴 구조)의 작업 흐름을 알아보자.
- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공한다.
만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다. (메소드 레벨에서 일어나는 DI) - 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가
내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다.
콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서
작업을 수행하고 그 결과를 다시 템플릿에 돌려준다. - 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다.
경우에 따라 최종 결과를 클라이언트에게 다시 돌려주기도 한다. - 템플릿에 인스턴스 변수를 만들어두고 사용할 의존 오브젝트를 수정자 메소드로 받아서 사용하는 일반적인 DI와 달리,
템플릿/콜백 방식에서는 템플릿이 사용할 콜백 인터페이스를 구현한 오브젝트를 메소드를 통해 주입해주는 DI 작업이
클라이언트가 템플릿의 기능을 호출하는 것과 동시에 일어나 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는다.
그러므로 템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라 할 수 있다.
- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공한다.

편리한 콜백의 재활용
- 템플릿/콜백 방식의 장점은?
- 템플릿에 담긴 코드를 여기저기에 반복적을 사용하는 원시적인 방법에 비해 많은 장점이 있다.
- JdbcContext를 사용하면 기존에 JDBC 기반의 코드를 만들었을 때 발생했던 여러 가지 문제점과 불편함을 제거한다.
- 클라이언트인 DAO의 메소드는 간결해지고 최소한의 데이터 액세스 로직만 갖고 있게 된다.
- 하지만 익명 내부 클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 조금 불편하다.
- 복잡한 익명 내부 클래스의 사용을 최소화할 수 있는 방법을 찾아보자.
- JDBC의 try/catch/finally에 적용했던 방법을 현재 UserDao의 메소드에도 적용하여
만약 분리를 통해 재사용이 가능한 코드를 찾아낼 수 있다면 익명 내부 클래스를 사용한 코드를 간결하게 만들 수도 있다. - StatementStrategy 인터페이스의 makePreparedStatement() 메소드를 구현한 콜백 오브젝트 코드를 살펴보면
고정된 SQL 쿼리 하나를 담아서 PrepraredStatement를 만드는 게 전부이다.
바인딩할 파라미터 없이 미리 만들어진 SQL을 이용해 만들기만 하면 되는 콜백은 적지 않을 것이므로
deleteAll()과 유사한 내용의 콜백 오브젝트가 반복될 가능성이 높다.
그렇다면, 중복될 가능성이 있는 자주 바뀌지 않는 부분을 분리해보자. - 단순 SQL을 필요로 하는 콜백이라면 나머지 코드는 매번 동일할 것이다.
그렇다면 SQL 문장만 파라미터로 받아서 바꿀 수 있게 하고 메소드 내용 전체를 분리해 별도의 메소드로 만들어보자.
executeSql() 메소드를 만들게 되고 바뀌는 부분인 SQL 문장만 파라미터로 받아서 사용하게 되므로
SQL을 담을 파라미터를 final로 선언해서 익명 내부 클래스인 콜백 안에서 직접 사용할 수 있게 하는 것만 주의하면 된다. - 이렇게 하면 재활용 가능한 콜백을 담은 메소드가 만들어지게 되므로
모든 고정된 SQL을 실행하는 DAO 메소드는 deleteAll() 메소드처럼 executeSql()을 호출하는 한 줄이면 끝이므로
복잡한 익명 내부 클래스인 콜백을 직접 만들 필요조차 없어진다.
- JDBC의 try/catch/finally에 적용했던 방법을 현재 UserDao의 메소드에도 적용하여
/**
* JDBC를 이용한 등록과 조회 기능이 있는 UserDao 클래스
*/
public class UserDao {
private DataSource dataSource;
private JdbcContext jdbcContext;
// JdbcContext를 DI 받도록 만든다.
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/* 새로운 사용자를 생성 */
// JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.
public void add(final User user) throws SQLException {
// add() 메소드 내부에 선언된 익명 클래스이다.
this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(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 = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
// SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
ResultSet rs = ps.executeQuery();
// User는 null 상태로 초기화해놓는다.
User user = null;
// id를 조건으로 한 쿼리의 결과가 있으면 User 오브젝트를 만들고 값을 넣어준다.
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
c.close();
// 결과가 없으면 User는 null 상태 그대로일 것이므로 이를 확인해서 예외를 던져준다.
if (user == null) throw new EmptyResultDataAccessException(1);
return user;
}
/* 모든 사용자 삭제하기 */
// 변하지 않는 부분을 분리시킨다.
public void deleteAll() throws SQLException {
executeSql("delete from users");
}
// 변하지 않는 콜백 클래스 정의와 오브젝트 생성 부분을 분리시킨다.
private void executeSql(final String query) throws SQLException {
// deleteAll() 메소드 내부에 선언된 익명 클래스이다.
this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
/* 사용자 테이블의 레코드 개수 읽어오기 */
public int getCount() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
// ResultSet도 다양한 SQLException이 발생할 수 있는 코드이므로 try 블록 안에 둬야 한다.
try {
c = dataSource.getConnection();
ps = c.prepareStatement("select count(*) from users");
rs = ps.executeQuery();
rs.next();
return 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) {
}
}
}
}
}
- 변하는 것과 변하지 않는 것을 분리하고 변하지 않는 건 유연하게 재활용할 수 있게 만든다는 간단한 원리를 계속 적용했을 때
단순하면서도 안전하게 작성 가능한 JDBC 활용 코드가 완성된다. - 재사용 가능한 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다.
- executeSql() 메소드는 UserDao만 사용하기는 아깝다.
- 엄밀히 말해서 템플릿은 JdbcContext 클래스가 아니라 workWithStatementStrategy() 메소드이므로
JdbcContext 클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql() 메소드를 옮긴다고 해도 문제 될 것은 없다.
그러면 이제 모든 DAO 메소드에서 executeSql() 메소드를 사용할 수 있게 된다.
- executeSql() 메소드는 UserDao만 사용하기는 아깝다.
/**
* JDBC 작업 흐름을 분리해서 만든 JdbcContext 클래스
*/
public class JdbcContext {
private DataSource dataSource;
// DataSource 타입 빈을 DI 받을 수 있게 준비해둔다.
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(c);
} 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 query) throws SQLException {
workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
}
/**
* JDBC를 이용한 등록과 조회 기능이 있는 UserDao 클래스
*/
public class UserDao {
private DataSource dataSource;
private JdbcContext jdbcContext;
// JdbcContext를 DI 받도록 만든다.
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/* 새로운 사용자를 생성 */
// JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.
public void add(final User user) throws SQLException {
// add() 메소드 내부에 선언된 익명 클래스이다.
this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(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 = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
// SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
ResultSet rs = ps.executeQuery();
// User는 null 상태로 초기화해놓는다.
User user = null;
// id를 조건으로 한 쿼리의 결과가 있으면 User 오브젝트를 만들고 값을 넣어준다.
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
c.close();
// 결과가 없으면 User는 null 상태 그대로일 것이므로 이를 확인해서 예외를 던져준다.
if (user == null) throw new EmptyResultDataAccessException(1);
return user;
}
/* 모든 사용자 삭제하기 */
// 변하지 않는 부분을 분리시킨다.
public void deleteAll() throws SQLException {
this.jdbcContext.executeSql("delete from users");
}
/* 사용자 테이블의 레코드 개수 읽어오기 */
public int getCount() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
// ResultSet도 다양한 SQLException이 발생할 수 있는 코드이므로 try 블록 안에 둬야 한다.
try {
c = dataSource.getConnection();
ps = c.prepareStatement("select count(*) from users");
rs = ps.executeQuery();
rs.next();
return 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) {
}
}
}
}
}
- 결국 JdbcContext 안에 클라이언트와 템플릿, 콜백이 모두 함께 공존하면서 동작하는 구조가 됐다.
- 일반적으로 성격이 다른 코드들은 가능한 분리하는 편이 낫지만, 이 경우는 하나의 목적을 위해
서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들이기 때문에 한 군데에 모여 있는 게 유리하다. - 구체적인 구현과 내부의 전략 패턴, 코드에 의한 DI, 익명 내부 클래스 등의 기술은 최대한 감춰두고,
외부에는 꼭 필요한 기능을 제공하는 단순한 메소드만 노출해주는 것이다.
- 일반적으로 성격이 다른 코드들은 가능한 분리하는 편이 낫지만, 이 경우는 하나의 목적을 위해
/**
* 콜백 재활용을 적용한 JdbcContext 클래스
*/
public class JdbcContext {
private DataSource dataSource;
// DataSource 타입 빈을 DI 받을 수 있게 준비해둔다.
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
// 템플릿 <<template>>
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(c);
} catch (SQLException e) {
throw e;
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
}
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {
}
}
}
}
// 클라이언트 <<client>>
// 변하지 않는 콜백 클래스 정의와 오브젝트 생성 부분을 분리시킨다.
public void executeSql(final String query) throws SQLException {
// 콜백 <<callback>>
workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
}
템플릿/콜백의 응용
- 스프링의 많은 API나 기능을 살펴보면 템플릿/콜백 패턴을 적용한 경우를 많이 발견할 수 있다.
- 스프링에는 다양한 자바 엔터프라이즈 기술에서 사용할 수 있도록
미리 만들어져 제공되는 수십 가지 템플릿/콜백 클래스와 API가 있다. - 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리하는 방법을 생각해보자.
- 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다.
- 그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면
인터페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI로 의존관계를 관리하게 만든다. - 그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴을 적용해보자.
- 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다.
- try/catch/finally 블록을 사용하는 코드는 가장 전형적인 템플릿/콜백 패턴의 후보이다.
- 파일을 하나 열어서 모든 라인의 숫자를 더한 합을 돌려주는 코드를 만들어보자.
- 파일에 있는 모든 숫자의 곱을 계산하는 기능을 추가해야 한다면?
- 이외에도 앞으로 많은 파일에 담긴 숫자 데이터를 여러 가지 방식으로 처리하는 기능이 계속 추가될 것이다.
- 이 경우 파일을 읽어서 처리하는 기능이 새로 필요할 때마다 앞에서 만든 코드를 복사해서 사용할 것이 아니라,
템플릿/콜백 패턴을 적용해보자. - BufferdReader를 만들어서 넘겨주는 것과 그 외의 모든 번거로운 작업에 대한 작업 흐름은 템플릿에서 진행하고,
준비된 BufferReader를 이용해 작업을 수행하는 부분은 콜백을 호출해서 처리하도록 한다. - 그 외에도 콜백에 각 라인을 읽는 작업 등의 유사한 코드가 있다면 템플릿에 포함되도록 하여 콜백을 더욱 단순하게 만든다.
1
2
3
4
/**
* BufferedReader를 전달받는 콜백 인터페이스
*/
public interface BufferedReaderCallback {
Integer doSomethingWithReader(BufferedReader br) throws IOException;
}
/**
* 라인별 작업을 정의한 콜백 인터페이스
*/
public interface LineCallback {
Integer doSomethingWithLine(String line, Integer value) throws IOException;
}
/**
* Calculator 클래스 코드
*/
public class Calculator {
// 템플릿 <<template>>
// BufferedReaderCallback을 사용하는 템플릿 메소드
public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
// 콜백 오브젝트 호출을 통해 템플릿에서 만든 컨텍스트 정보인 BufferedReader를 전달해주고 콜백의 작업 결과를 받아둔다.
int ret = callback.doSomethingWithReader(br);
return ret;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 템플릿 <<template>>
// LineCallback을 사용하는 템플릿 메소드
public Integer lineReadTemplate(String filepath, LineCallback callback, int initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
Integer res = initVal;
String line = null;
// 파일의 각 라인을 푸르를 돌면서 가져오는 것도 템플릿이 담당한다.
while ((line = br.readLine()) != null) {
// 각 라인의 내용을 가지고 계산하는 작업만 콜백에게 맡긴다.
res += callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback sumCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer sum = 0;
String line = null;
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
return sum;
}
};
return fileReadTemplate(filepath, sumCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback sumCallback = new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, sumCallback, 0);
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback multiplyCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer multiply = 1;
String line = null;
while ((line = br.readLine()) != null) {
multiply *= Integer.valueOf(line);
}
return multiply;
}
};
return fileReadTemplate(filepath, multiplyCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback multiplyCallback = new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, multiplyCallback, 0);
}
}
- 제네릭스를 이용하면 좀 더 강력한 템플릿/콜백 구조를 만들 수 있다.
- 만약 파일의 경우처럼 라인 단위로 처리해서 만드는 결과의 타입을 다양하게 가져가고 싶다면,
자바 언어에 타입 파라미터라는 개념을 도입한 제네릭스를 이용하면 된다. - 제네릭스를 이용하면 다양한 오브젝트 타입을 지원하는 인터페이스나 메소드를 정의할 수 있다.
- 콜백 인터페이스를 <T>로 수정한 후 콜백 메소드의 리턴값과 파라미터의 값의 타입을 제네릭 타입 파라미터 T로 선언한다.
그리고 템플릿 메소드에도 타입 파라미터를 사용해 제네릭 메소드로 만들어준다. - 이렇게 되면 템플릿 메소드는 타입 파라미터 T를 갖는 인터페이스 콜백 타입의 오브젝트와 T 타입의 초기값을 받아서
T 타입의 변수를 정의하고, T 타입 파라미터로 선언된 콜백의 메소드를 호출해서 처리한 후
T 타입의 결과를 리턴하는 메소드가 되게 된다. 그러므로 T 타입의 결과를 만들어내는 범용적인 템플릿/콜백이 되게 된다.
- 만약 파일의 경우처럼 라인 단위로 처리해서 만드는 결과의 타입을 다양하게 가져가고 싶다면,
/**
* 라인별 작업을 정의한 콜백 인터페이스
*/
public interface LineCallback<T> {
// 타입 파라미터를 적용
T doSomethingWithLine(String line, T value);
}
/**
* Calculator 클래스 코드
*/
public class Calculator {
// 템플릿 <<template>>
// BufferedReaderCallback을 사용하는 템플릿 메소드
public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
// 콜백 오브젝트 호출을 통해 템플릿에서 만든 컨텍스트 정보인 BufferedReader를 전달해주고 콜백의 작업 결과를 받아둔다.
int ret = callback.doSomethingWithReader(br);
return ret;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 템플릿 <<template>>
// LineCallback을 사용하는 템플릿 메소드 (타입 파라미터 추가)
public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
T res = initVal;
String line = null;
// 파일의 각 라인을 푸르를 돌면서 가져오는 것도 템플릿이 담당한다.
while ((line = br.readLine()) != null) {
// 각 라인의 내용을 가지고 계산하는 작업만 콜백에게 맡긴다.
res = callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback sumCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer sum = 0;
String line = null;
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
return sum;
}
};
return fileReadTemplate(filepath, sumCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback<Integer> sumCallback = new LineCallback<Integer>() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, sumCallback, 0);
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback multiplyCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer multiply = 1;
String line = null;
while ((line = br.readLine()) != null) {
multiply *= Integer.valueOf(line);
}
return multiply;
}
};
return fileReadTemplate(filepath, multiplyCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback<Integer> multiplyCallback = new LineCallback<Integer>() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, multiplyCallback, 0);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 concatenate() 메소드
public String concatenate(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback<String> concatenateCallback = new LineCallback<String>() {
@Override
public String doSomethingWithLine(String line, String value) {
return value + line;
}
};
return lineReadTemplate(filepath, concatenateCallback, "");
}
}
/**
* 파일을 계산하는 코드의 테스트
*/
public class CalculatorTest {
Calculator calculator;
String numFilepath;
@Before
public void setUp() {
this.calculator = new Calculator();
this.numFilepath = getClass().getResource("numbers.txt").getPath();
}
@Test
public void sumOfNumbers() throws IOException {
assertThat(calculator.calSumWithLine(this.numFilepath), is(10));
}
@Test
public void concatenateStrings() throws IOException {
assertThat(calculator.concatenate(this.numFilepath), is("1234"));
}
}
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 예외 (0) (0) | 2023.09.30 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (6) (0) | 2023.09.29 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (4) (0) | 2023.09.29 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (3) (0) | 2023.09.28 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (2) (0) | 2023.09.28 |
3.5) 템플릿과 콜백
템플릿/콜백의 동작원리
- 템플릿/콜백 패턴이란?
- 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부만 자주 바꿔서 사용해야 하는 경우에
적합한 구조이며 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식이다. - 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.
템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이며
콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다. - 여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리
템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다.
템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다. - 콜백 인터페이스의 메소드에는 보통 파라미터가 있으며 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받는다.
- 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부만 자주 바꿔서 사용해야 하는 경우에
- 템플릿/콜백(DI 방식의 전략 패턴 구조)의 작업 흐름을 알아보자.
- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공한다.
만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다. (메소드 레벨에서 일어나는 DI) - 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가
내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다.
콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서
작업을 수행하고 그 결과를 다시 템플릿에 돌려준다. - 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다.
경우에 따라 최종 결과를 클라이언트에게 다시 돌려주기도 한다. - 템플릿에 인스턴스 변수를 만들어두고 사용할 의존 오브젝트를 수정자 메소드로 받아서 사용하는 일반적인 DI와 달리,
템플릿/콜백 방식에서는 템플릿이 사용할 콜백 인터페이스를 구현한 오브젝트를 메소드를 통해 주입해주는 DI 작업이
클라이언트가 템플릿의 기능을 호출하는 것과 동시에 일어나 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는다.
그러므로 템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라 할 수 있다.
- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공한다.

편리한 콜백의 재활용
- 템플릿/콜백 방식의 장점은?
- 템플릿에 담긴 코드를 여기저기에 반복적을 사용하는 원시적인 방법에 비해 많은 장점이 있다.
- JdbcContext를 사용하면 기존에 JDBC 기반의 코드를 만들었을 때 발생했던 여러 가지 문제점과 불편함을 제거한다.
- 클라이언트인 DAO의 메소드는 간결해지고 최소한의 데이터 액세스 로직만 갖고 있게 된다.
- 하지만 익명 내부 클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 조금 불편하다.
- 복잡한 익명 내부 클래스의 사용을 최소화할 수 있는 방법을 찾아보자.
- JDBC의 try/catch/finally에 적용했던 방법을 현재 UserDao의 메소드에도 적용하여
만약 분리를 통해 재사용이 가능한 코드를 찾아낼 수 있다면 익명 내부 클래스를 사용한 코드를 간결하게 만들 수도 있다. - StatementStrategy 인터페이스의 makePreparedStatement() 메소드를 구현한 콜백 오브젝트 코드를 살펴보면
고정된 SQL 쿼리 하나를 담아서 PrepraredStatement를 만드는 게 전부이다.
바인딩할 파라미터 없이 미리 만들어진 SQL을 이용해 만들기만 하면 되는 콜백은 적지 않을 것이므로
deleteAll()과 유사한 내용의 콜백 오브젝트가 반복될 가능성이 높다.
그렇다면, 중복될 가능성이 있는 자주 바뀌지 않는 부분을 분리해보자. - 단순 SQL을 필요로 하는 콜백이라면 나머지 코드는 매번 동일할 것이다.
그렇다면 SQL 문장만 파라미터로 받아서 바꿀 수 있게 하고 메소드 내용 전체를 분리해 별도의 메소드로 만들어보자.
executeSql() 메소드를 만들게 되고 바뀌는 부분인 SQL 문장만 파라미터로 받아서 사용하게 되므로
SQL을 담을 파라미터를 final로 선언해서 익명 내부 클래스인 콜백 안에서 직접 사용할 수 있게 하는 것만 주의하면 된다. - 이렇게 하면 재활용 가능한 콜백을 담은 메소드가 만들어지게 되므로
모든 고정된 SQL을 실행하는 DAO 메소드는 deleteAll() 메소드처럼 executeSql()을 호출하는 한 줄이면 끝이므로
복잡한 익명 내부 클래스인 콜백을 직접 만들 필요조차 없어진다.
- JDBC의 try/catch/finally에 적용했던 방법을 현재 UserDao의 메소드에도 적용하여
/**
* JDBC를 이용한 등록과 조회 기능이 있는 UserDao 클래스
*/
public class UserDao {
private DataSource dataSource;
private JdbcContext jdbcContext;
// JdbcContext를 DI 받도록 만든다.
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/* 새로운 사용자를 생성 */
// JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.
public void add(final User user) throws SQLException {
// add() 메소드 내부에 선언된 익명 클래스이다.
this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(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 = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
// SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
ResultSet rs = ps.executeQuery();
// User는 null 상태로 초기화해놓는다.
User user = null;
// id를 조건으로 한 쿼리의 결과가 있으면 User 오브젝트를 만들고 값을 넣어준다.
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
c.close();
// 결과가 없으면 User는 null 상태 그대로일 것이므로 이를 확인해서 예외를 던져준다.
if (user == null) throw new EmptyResultDataAccessException(1);
return user;
}
/* 모든 사용자 삭제하기 */
// 변하지 않는 부분을 분리시킨다.
public void deleteAll() throws SQLException {
executeSql("delete from users");
}
// 변하지 않는 콜백 클래스 정의와 오브젝트 생성 부분을 분리시킨다.
private void executeSql(final String query) throws SQLException {
// deleteAll() 메소드 내부에 선언된 익명 클래스이다.
this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
/* 사용자 테이블의 레코드 개수 읽어오기 */
public int getCount() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
// ResultSet도 다양한 SQLException이 발생할 수 있는 코드이므로 try 블록 안에 둬야 한다.
try {
c = dataSource.getConnection();
ps = c.prepareStatement("select count(*) from users");
rs = ps.executeQuery();
rs.next();
return 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) {
}
}
}
}
}
- 변하는 것과 변하지 않는 것을 분리하고 변하지 않는 건 유연하게 재활용할 수 있게 만든다는 간단한 원리를 계속 적용했을 때
단순하면서도 안전하게 작성 가능한 JDBC 활용 코드가 완성된다. - 재사용 가능한 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다.
- executeSql() 메소드는 UserDao만 사용하기는 아깝다.
- 엄밀히 말해서 템플릿은 JdbcContext 클래스가 아니라 workWithStatementStrategy() 메소드이므로
JdbcContext 클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql() 메소드를 옮긴다고 해도 문제 될 것은 없다.
그러면 이제 모든 DAO 메소드에서 executeSql() 메소드를 사용할 수 있게 된다.
- executeSql() 메소드는 UserDao만 사용하기는 아깝다.
/**
* JDBC 작업 흐름을 분리해서 만든 JdbcContext 클래스
*/
public class JdbcContext {
private DataSource dataSource;
// DataSource 타입 빈을 DI 받을 수 있게 준비해둔다.
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(c);
} 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 query) throws SQLException {
workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
}
/**
* JDBC를 이용한 등록과 조회 기능이 있는 UserDao 클래스
*/
public class UserDao {
private DataSource dataSource;
private JdbcContext jdbcContext;
// JdbcContext를 DI 받도록 만든다.
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/* 새로운 사용자를 생성 */
// JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.
public void add(final User user) throws SQLException {
// add() 메소드 내부에 선언된 익명 클래스이다.
this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(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 = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
// SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
ResultSet rs = ps.executeQuery();
// User는 null 상태로 초기화해놓는다.
User user = null;
// id를 조건으로 한 쿼리의 결과가 있으면 User 오브젝트를 만들고 값을 넣어준다.
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
c.close();
// 결과가 없으면 User는 null 상태 그대로일 것이므로 이를 확인해서 예외를 던져준다.
if (user == null) throw new EmptyResultDataAccessException(1);
return user;
}
/* 모든 사용자 삭제하기 */
// 변하지 않는 부분을 분리시킨다.
public void deleteAll() throws SQLException {
this.jdbcContext.executeSql("delete from users");
}
/* 사용자 테이블의 레코드 개수 읽어오기 */
public int getCount() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
// ResultSet도 다양한 SQLException이 발생할 수 있는 코드이므로 try 블록 안에 둬야 한다.
try {
c = dataSource.getConnection();
ps = c.prepareStatement("select count(*) from users");
rs = ps.executeQuery();
rs.next();
return 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) {
}
}
}
}
}
- 결국 JdbcContext 안에 클라이언트와 템플릿, 콜백이 모두 함께 공존하면서 동작하는 구조가 됐다.
- 일반적으로 성격이 다른 코드들은 가능한 분리하는 편이 낫지만, 이 경우는 하나의 목적을 위해
서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들이기 때문에 한 군데에 모여 있는 게 유리하다. - 구체적인 구현과 내부의 전략 패턴, 코드에 의한 DI, 익명 내부 클래스 등의 기술은 최대한 감춰두고,
외부에는 꼭 필요한 기능을 제공하는 단순한 메소드만 노출해주는 것이다.
- 일반적으로 성격이 다른 코드들은 가능한 분리하는 편이 낫지만, 이 경우는 하나의 목적을 위해
/**
* 콜백 재활용을 적용한 JdbcContext 클래스
*/
public class JdbcContext {
private DataSource dataSource;
// DataSource 타입 빈을 DI 받을 수 있게 준비해둔다.
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
// 템플릿 <<template>>
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(c);
} catch (SQLException e) {
throw e;
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
}
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {
}
}
}
}
// 클라이언트 <<client>>
// 변하지 않는 콜백 클래스 정의와 오브젝트 생성 부분을 분리시킨다.
public void executeSql(final String query) throws SQLException {
// 콜백 <<callback>>
workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
}
템플릿/콜백의 응용
- 스프링의 많은 API나 기능을 살펴보면 템플릿/콜백 패턴을 적용한 경우를 많이 발견할 수 있다.
- 스프링에는 다양한 자바 엔터프라이즈 기술에서 사용할 수 있도록
미리 만들어져 제공되는 수십 가지 템플릿/콜백 클래스와 API가 있다. - 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리하는 방법을 생각해보자.
- 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다.
- 그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면
인터페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI로 의존관계를 관리하게 만든다. - 그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴을 적용해보자.
- 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다.
- try/catch/finally 블록을 사용하는 코드는 가장 전형적인 템플릿/콜백 패턴의 후보이다.
- 파일을 하나 열어서 모든 라인의 숫자를 더한 합을 돌려주는 코드를 만들어보자.
- 파일에 있는 모든 숫자의 곱을 계산하는 기능을 추가해야 한다면?
- 이외에도 앞으로 많은 파일에 담긴 숫자 데이터를 여러 가지 방식으로 처리하는 기능이 계속 추가될 것이다.
- 이 경우 파일을 읽어서 처리하는 기능이 새로 필요할 때마다 앞에서 만든 코드를 복사해서 사용할 것이 아니라,
템플릿/콜백 패턴을 적용해보자. - BufferdReader를 만들어서 넘겨주는 것과 그 외의 모든 번거로운 작업에 대한 작업 흐름은 템플릿에서 진행하고,
준비된 BufferReader를 이용해 작업을 수행하는 부분은 콜백을 호출해서 처리하도록 한다. - 그 외에도 콜백에 각 라인을 읽는 작업 등의 유사한 코드가 있다면 템플릿에 포함되도록 하여 콜백을 더욱 단순하게 만든다.
1
2
3
4
/**
* BufferedReader를 전달받는 콜백 인터페이스
*/
public interface BufferedReaderCallback {
Integer doSomethingWithReader(BufferedReader br) throws IOException;
}
/**
* 라인별 작업을 정의한 콜백 인터페이스
*/
public interface LineCallback {
Integer doSomethingWithLine(String line, Integer value) throws IOException;
}
/**
* Calculator 클래스 코드
*/
public class Calculator {
// 템플릿 <<template>>
// BufferedReaderCallback을 사용하는 템플릿 메소드
public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
// 콜백 오브젝트 호출을 통해 템플릿에서 만든 컨텍스트 정보인 BufferedReader를 전달해주고 콜백의 작업 결과를 받아둔다.
int ret = callback.doSomethingWithReader(br);
return ret;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 템플릿 <<template>>
// LineCallback을 사용하는 템플릿 메소드
public Integer lineReadTemplate(String filepath, LineCallback callback, int initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
Integer res = initVal;
String line = null;
// 파일의 각 라인을 푸르를 돌면서 가져오는 것도 템플릿이 담당한다.
while ((line = br.readLine()) != null) {
// 각 라인의 내용을 가지고 계산하는 작업만 콜백에게 맡긴다.
res += callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback sumCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer sum = 0;
String line = null;
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
return sum;
}
};
return fileReadTemplate(filepath, sumCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback sumCallback = new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, sumCallback, 0);
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback multiplyCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer multiply = 1;
String line = null;
while ((line = br.readLine()) != null) {
multiply *= Integer.valueOf(line);
}
return multiply;
}
};
return fileReadTemplate(filepath, multiplyCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback multiplyCallback = new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, multiplyCallback, 0);
}
}
- 제네릭스를 이용하면 좀 더 강력한 템플릿/콜백 구조를 만들 수 있다.
- 만약 파일의 경우처럼 라인 단위로 처리해서 만드는 결과의 타입을 다양하게 가져가고 싶다면,
자바 언어에 타입 파라미터라는 개념을 도입한 제네릭스를 이용하면 된다. - 제네릭스를 이용하면 다양한 오브젝트 타입을 지원하는 인터페이스나 메소드를 정의할 수 있다.
- 콜백 인터페이스를 <T>로 수정한 후 콜백 메소드의 리턴값과 파라미터의 값의 타입을 제네릭 타입 파라미터 T로 선언한다.
그리고 템플릿 메소드에도 타입 파라미터를 사용해 제네릭 메소드로 만들어준다. - 이렇게 되면 템플릿 메소드는 타입 파라미터 T를 갖는 인터페이스 콜백 타입의 오브젝트와 T 타입의 초기값을 받아서
T 타입의 변수를 정의하고, T 타입 파라미터로 선언된 콜백의 메소드를 호출해서 처리한 후
T 타입의 결과를 리턴하는 메소드가 되게 된다. 그러므로 T 타입의 결과를 만들어내는 범용적인 템플릿/콜백이 되게 된다.
- 만약 파일의 경우처럼 라인 단위로 처리해서 만드는 결과의 타입을 다양하게 가져가고 싶다면,
/**
* 라인별 작업을 정의한 콜백 인터페이스
*/
public interface LineCallback<T> {
// 타입 파라미터를 적용
T doSomethingWithLine(String line, T value);
}
/**
* Calculator 클래스 코드
*/
public class Calculator {
// 템플릿 <<template>>
// BufferedReaderCallback을 사용하는 템플릿 메소드
public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
// 콜백 오브젝트 호출을 통해 템플릿에서 만든 컨텍스트 정보인 BufferedReader를 전달해주고 콜백의 작업 결과를 받아둔다.
int ret = callback.doSomethingWithReader(br);
return ret;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 템플릿 <<template>>
// LineCallback을 사용하는 템플릿 메소드 (타입 파라미터 추가)
public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filepath));
T res = initVal;
String line = null;
// 파일의 각 라인을 푸르를 돌면서 가져오는 것도 템플릿이 담당한다.
while ((line = br.readLine()) != null) {
// 각 라인의 내용을 가지고 계산하는 작업만 콜백에게 맡긴다.
res = callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback sumCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer sum = 0;
String line = null;
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
return sum;
}
};
return fileReadTemplate(filepath, sumCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calSum() 메소드
public Integer calSumWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback<Integer> sumCallback = new LineCallback<Integer>() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, sumCallback, 0);
}
// 클라이언트 <<client>>
// fileReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithReader(String filepath) throws IOException {
// 콜백 <<callback>>
BufferedReaderCallback multiplyCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer multiply = 1;
String line = null;
while ((line = br.readLine()) != null) {
multiply *= Integer.valueOf(line);
}
return multiply;
}
};
return fileReadTemplate(filepath, multiplyCallback);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 calMultiply() 메소드
public Integer calMultiplyWithLine(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback<Integer> multiplyCallback = new LineCallback<Integer>() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, multiplyCallback, 0);
}
// 클라이언트 <<client>>
// lineReadTemplate 템플릿/콜백을 적용한 concatenate() 메소드
public String concatenate(String filepath) throws IOException {
// 콜백 <<callback>>
LineCallback<String> concatenateCallback = new LineCallback<String>() {
@Override
public String doSomethingWithLine(String line, String value) {
return value + line;
}
};
return lineReadTemplate(filepath, concatenateCallback, "");
}
}
/**
* 파일을 계산하는 코드의 테스트
*/
public class CalculatorTest {
Calculator calculator;
String numFilepath;
@Before
public void setUp() {
this.calculator = new Calculator();
this.numFilepath = getClass().getResource("numbers.txt").getPath();
}
@Test
public void sumOfNumbers() throws IOException {
assertThat(calculator.calSumWithLine(this.numFilepath), is(10));
}
@Test
public void concatenateStrings() throws IOException {
assertThat(calculator.concatenate(this.numFilepath), is("1234"));
}
}
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 예외 (0) (0) | 2023.09.30 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (6) (0) | 2023.09.29 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (4) (0) | 2023.09.29 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (3) (0) | 2023.09.28 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (2) (0) | 2023.09.28 |