✅ 템플릿 콜백 패턴이란 무엇인가?
템플릿 콜백 패턴은 템플릿 메소드 패턴과 콜백 패턴을 결합한 디자인 패턴이다. 이 패턴은 알고리즘의 기본 구조를 템플릿 메소드로 정의하고, 일부 동작을 콜백 메소드로 위임하여 구체적인 구현을 서브클래스에서 처리한다. 이를 통해 알고리즘의 확장성과 재사용성을 높일 수 있다. 지금까지 템플릿/콜백 패턴을 적용한 것이다.
🔹템플릿 메소드 패턴이란?
템플릿 메소드는 부모 클래스에서 알고리즘의 골격(구조)을 정의하고, 자식 클래스에서 부모 클래스를 상속 받아 구현할 때 알고리즘의 전체 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계의 메소드를 오버라이드(재정의)할 수 있도록 하는 행동 디자인 패턴이다. 앞에 1장에서 UserDao의 getConnection()
메소드를 NUserDao
와 DUserDao
에서 각각 구현한 것 또한 일종의 템플릿 메소드 패턴을 사용한 것이다.
🔹콜백 패턴이란?
프로그래밍에서 콜백(callback) 또는 콜애프터 함수(call-after function)는 다른 코드의 인수로써 넘겨주는 실행 가능한 코드를 말한다. 자바에서는 메소드 자체를 인수로 전달할 수 없기 때문에 메소드가 실행되는 것을 목적으로 메소드를 오브젝트 안에 담아서 다른 오브젝트로 전달하여 코드를 실행하는 것이다.
파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행 시키기 위해, 특정 메소드를 담은 오브젝트를 전달하는 것이다. 그래서 펑셔널 오브젝트(functional object)라고도 한다. 우선은 간단하게 특정 로직을 가진 인터페이스를 구현한 클래스를, 인터페이스 타입으로 클라이언트에서 실행한 메소드의 인수로 넘겨주어, 인수로 받은 구현 클래스를 통해 해당 메소드를 실행할 수 있다는 것을 알고 넘어가자.
- Java 언어에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다. 코드만 넘길 수 없다.
- Java 8 이전에는 보통 하나의 메소드를 가진 인터페이스를 구현하고, 주로 익명 내부 클래스를 사용했다.
- Java 8 이후에는 주로 람다를 사용한다.
👉 템플릿 메소드 패턴과 콜백 패턴을 이해했다면 템플릿/콜백 패턴에 대해서는 훨씬 쉽게 이해할 수 있다.
템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다. 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면 된다. 현재는 람다식으로 많이 구현한다.
콜백 메소드에는 보통 파라미터가 있는데 이는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달 받을 때 사용한다. 우리가 만든 코드에서는 JdbcContext
에서 템플릿 메소드인 workWithStatementStrategy()
메소드 내에서 생성한 Connection
오브젝트를 콜백 메소드인 makePreparedStatement()
를 실행할 때 파라미터로 넘겨준다. PreparedStatement
를 생성하기 위해서는 Connection
객체가 필요하기 때문이다.
- UserDao
public void add(final User user) throws ClassNotFoundException, SQLException{ //== 템플릿 메소드 ==// jdbcContext.workWithStatementStrategy(new StatementStrategy() { //== 콜백 객체, 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스 ==// public PreparedStatement makePreparedStatement(Connection con) throws SQLException { String sql = "INSERT INTO users(id, name, password) values(?,?,?)"; PreparedStatement pst = con.prepareStatement(sql); pst.setString(1, user.getId()); pst.setString(2, user.getName()); pst.setString(3, user.getPassword()); return pst; }//익명 내부 클래스의 메소드 끝 }//익명 내부 클래스의 끝 );//jdbcContext.workWithStatementStrategy() 메소드의 끝 }//외부 클래스의 끝
- JdbcContext
//== 템플릿 메소드 ==// //인터페이스 타입으로 익명 내부 클래스를 구현한 객체를 매개변수로 받는다. public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException { Connection con = null; PreparedStatement pst = null; try { //참조 정보 생성 con = dataSource.getConnection(); //콜백 메소드 호출하고 파라미터로 참조 정보 전달, 결과값을 pst에 대입 pst = stmt.makePreparedStatement(con); pst.executeUpdate(); } catch (SQLException e) { throw e; } finally { if (pst != null) { try {pst.close(); } catch (SQLException e) {} } if (con != null) { try {con.close(); } catch (SQLException e) {} } } }
—UserDao.add(), 템플릿/콜백의 작업 흐름
- 클라이언트(
UserDao
의add()
메소드)는 템플릿 메소드(workWithStatementStrategy()
)를 실행시키고 콜백 오브젝트(new
StatementStraegy(){}
)를 생성한다.
- 콜백 오브젝트를 템플릿 메소드의 파라미터로 전달하고 템플릿 메소드를 호출한다.
- 템플릿 메소드는 정해진 흐름에 따라 작업(Workflow)을 진행한다.
- 내부에서 참조 정보(
Conncetion
객체)를 생성한다.
- 콜백 오브젝트의 메소드(
makePreparedStatement()
)를 호출한다. 파라미터로 참조 정보(Conncetion 객체)를 전달 받는다.
- 클라이언트의 final 변수(
add()
메소드의 파라미터인final
User
user
)를 참조한다.
- 콜백 오브젝트의 메소드(
makePreparedStatement()
)를 수행한다.
- 콜백의 작업 결과(
PreparedStatement
객체)를 다시 템플릿에게 돌려준다.
- 템플릿 메소드는 콜백이 돌려준 작업 결과를 가지고 다시 작업을 진행한다.
- 경우에 따라 최종 결과를 클라이언트에게 다시 돌려주기도 한다.
(1), (2) 과정에서 클라이언트가 템플릿 메소드를 호출하면서 콜백 오브젝트를 전달하는 것은 메소드 레벨에서 일어나는 DI다. 일반적인 DI라면 템플릿에 인스턴스 변수를 만들고 수정자 메소드로 객체를 받아서 사용할 것이다.
- 템플릿/콜백 패턴의 특징
- 템플릿/콜백 방식에서는 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는다.
- 콜백 오브젝트는 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조할 수 있다. 따라서 클라이언트와 콜백은 강하게 결합된다.
✅ 콜백의 분리와 재활용
복잡한 익명 내부 클래스의 코드를 간단하게 하려면 어떻게 해야 할까? StatementStrategy
인터페이스의 makePreparedStatement()
메소드를 구현한 콜백 오브젝트의 코드를 살펴보자. add()
보다 코드가 간단한 deleteAll()
로 살펴본다.
- UserDao, deleteAll()
public void deleteAll() throws SQLException { this.jdbcContext.workWithStatementStrategy(new StatementStrategy() { //== 콜백 오브젝트의 코드 ==// @Override public PreparedStatement makePreparedStatement(Connection con) throws SQLException { String sql = "DELETE FROM users"; return con.prepareStatement(sql); } }); }
고정된 SQL 쿼리 하나를 만들고
PreparedStatement
객체를 생성하는 것이 전부이다. SQL을 만들고 간단하게PreparedStatement
객체만 생성하는 콜백이 반복될 가능성이 높다. 따라서 SQL 문장만 클라이언트에게 받아서 콜백 객체를 생성할 수 있지 않을까? 메소드를 분리해서deleteAll()
을 구현한 코드는 다음과 같다.public void deleteAll() throws SQLException { String sql = "DELETE FROM users"; executeSql(sql); } private void executeSql(final String query) throws SQLException { this.jdbcContext.workWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makePreparedStatement(Connection con) throws SQLException { return con.prepareStatement(query); } }); }
이제 DAO의 deletAll()
메소드는 단지 쿼리를 생성하고, 템플릿/콜백 메소드인 executeSql() 메소드만 호출하면 된다. DAO의 기능에 해당하는 deleteAll()
메소드가 직접 콜백 메소드를 구현하지 않아도 되는 것이다.
deletAll()
과 같이 고정된 SQL을 실행하는 DAO의 다른 메소드들도 executeSql()
메소드만 실행하면 된다. 굳이 여러 곳에서 콜백 객체를 생성하지 않아도 되는 것이다.
✅ 콜백과 템플릿의 결합
executeSql()
메소드는 UserDao
만 사용하기는 아깝다. 이렇게 재사용 가능한 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮길 수도 있다. 엄밀히 말해서 템플릿은 JdbcContext
클래스가 아니라 workWithStatementStrategy()
메소드이므로 JdbcContext
클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql()
메소드를 옮기는 것도 가능하다.
- JdbcContext
//...생략 //UserDao의 executeSql() 메소드 이동 public void executeSql(final String query) throws SQLException { workWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makePreparedStatement(Connection con) throws SQLException { return con.prepareStatement(query); } }); }
다른 클래스에서 해당 executeSql() 메소드에 접근할 수 있게 하기 위해서 접근 제어자를 private → public으로 변경했다.
- UserDao
//...생략 public void deleteAll() throws SQLException { String sql = "DELETE FROM users"; this.jdbcContext.executeSql(sql); }
JdbcContext 인스턴스의 executeSql()
메소드를 사용하여 쿼리를 수행할 수 있게 되었다.
이렇게 하나의 목적을 위해 서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들은 기존과 다르게 한 군데 모여 있는 게 유리하다. 구체적인 구현과 내부의 전략 패턴, 코드에 의한 DI, 익명 내부 클래스 등의 기술은 최대한 감춰두고, 외부에는 꼭 필요한 기능을 제공하는 단순한 메소드(executesSql()
)만 노출해주는 것이다.
👉 변하는 것과 변하지 않는 것을 분리하고 변하지 않는 건 유연하게 재활용할 수 있게 만드는 것, 이런 게 객체지향 언어와 설계를 사용하는 매력이 아닐까!
📖토비 스프링 3.1 -p240~247
🚩jhcode33의 toby-spring-study.git으로 이동하기
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T