반응형

1. 템플릿/콜백 패턴

 - 전략 패턴의 기본구조 + 익명 내부 클래스

 

템플릿
일반적으로 템플릿은 어떤 목적을 위해 미리 만들어둔 틀을 뜻한다. 프로그래밍에서는 어떤 고정된 패턴안에 바꿀 수 있는 부분을 넣어서 사용하는 경우 템플릿이라고 부른다.
콜백
콜백은 메서드가 실행되는 것을 목적으로 다른 오브젝트 메서드에 전달되는 오브젝트를 말한다. 자바에선 메서드 자체를 파라미터로 전달할 수 없기때문에 메서드가 담긴 오브젝트를 전달하며, 이러한 오브젝트를 펑셔널 오브젝트(functional object)라고도 한다.
전략패턴
전략 패턴은 자신의 기능 중 필요에 따라 변경이 필요한 로직을 인터페이스를 통해 외부로 분리시키고, 이를 구현한 구체적인 로직을 필요에 따라 바꿔 사용할 수 있게 하는 디자인 패턴이다.

 

1.1. 예제

public void deleteAll() throws SQLException{
	// 전략패턴 + 익명 내부 클래스 사용 = 템플릿/콜백 패턴
        jdbcContext.workWithStatementStrategy(new StatementStrategy() {
            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                return c.prepareStatement("delete from users");
            }
        });
    }

 - jdbcContext.workWithStatementStrategy 메서드 파라미터의 인자에 인터페이스에 대한 구현 클래스가 아닌 익명 클래스를 넣고 있다. (== 전략 패턴의 기본구조 + 익명 내부 클래스)

 

1.2. 동작원리

템플릿/콜백의 작업 흐름

1) 클라이언트는 템플릿 안에서 실행될 콜백 오브젝트를 만든다.

2) 만든 콜백과 함께 템플릿을 호출한다. (메서드 레벨 DI)

3, 4) 템플릿에서는 사용할 참조정보를 생성한다.

5) 클라이언트로부터 전달받은 콜백을 참조변수와 함께 호출한다.

6) 콜백 함수에서는 Client의 final 변수를 참조한다.

7) 콜백 비지니스 로직을 수행한다.

8) 콜백 리턴 값을 템플릿에 전달한다.

9, 10) 템플릿에서는 나머지 로직을 수행한다. 

11) 최종적으로 템플릿의 결과 값을 클라이언트에게 리턴한다.

 

1.3. 단점

 - 익명 클래스 사용으로 인해 일반적인 코드보다는 상대적으로 코드를 작성하고 읽기가 불편하다. 하지만 익명클래스 코드 부분을 재사용하도록 템플릿화 시킨다면 코드 작성은 훨씬 간단해질 것이다. 

 

1.4. 콜백 분리 및 재사용

1) 메서드 분리

 - 기존 deleteAll 메서드에서 바뀌는 부분은 오직 SQL 부분이다. UserDao에 SQL을 받는 메서드를 만들고, 재사용하도록 구현한다. 이로써 응답 값이 없는 요청은 executeSql 메서드에 SQL만 넣어 호출하면 된다.

public void deleteAll() throws SQLException{
        executeSql("delete from users");
}

...

 public void executeSql(final String query) throws SQLException{
        jdbcContext.workWithStatementStrategy(
                new StatementStrategy() {
                    @Override
                    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                        return c.prepareStatement(query);
                }
        });
}

 

2) 클래스 분리

- UserDao 클래스 뿐 아니라 모든 Dao 클래스에서도 executeSql 메서드를 재사용할 수 있도록 executeSql 메서드를 JdbcContext 클래스로 이동시켜주자. 이로써 UserDao의 구현부에는 익명 클래스를 구현할 필요가 없어졌다.

 

1.5. executeSql 메서드를 새로운 클래스로 분리하지 않은 이유

 - 일반적으로 기능을 분리할 때에는 클래스로 분리하는 것이 재사용성이 좋다. executeSql도 클래스를 따로 생성할 수 있었지만, JdbcContext 클래스로 이동시킨 이유는 응집도 때문이다. JdbcContext는 DB와의 Connection을 맺고 쿼리를 실행시키는 책임이 있다. executeSql 메서드도 마찬가지로 이 책임을 위해 존재하므로 JdbcContext와 긴밀한 관계를 맺고있다고 볼 수 있다. 이렇게 동일한 목적을 가진 긴밀한 코드들은 한 군데 모여있는게 좋다.

 


2. 템플릿/콜백 패턴 예제 실습

2.1. 계산기 예제 코드 작성

 - 텍스트 파일에서 숫자를 읽어와 계산 기능을 처리하는 코드를 작성한다.

public class Calculator {
    public int calcSum(String path) throws IOException {

        BufferedReader br = new BufferedReader(new FileReader(path));
        Integer sum = 0;

        String line = null;

        while((line = br.readLine()) != null){
            sum += Integer.valueOf(line);
        }

        br.close();
        return sum;
    }
}

 

2.2. 코드 분석

 - 현재는 더하기 메서드만 구현하였으나, 곱하기, 나누기 메서드도 생성된다 가정하고 중복 또는 바뀌는 부분을 찾아본다.

 * 중복되는 부분

 1) path에 대한 BufferedReader가 생성 부분

 2) close 되는 부분 - try, catch, finally 포함 예정

 

 * 바뀌는 부분

 1) 읽은 문자열에 대해 계산하는 부분

 

2.3. 코드 리팩토링

 - 중복되는 부분을 메서드화 한다. 다른 클래스에서 재사용하지 않기 때문에 클래스로 분리하지 않았다.

 - 바뀌는 부분에 대해서는 템플릿/콜백 패턴을 적용하여 익명 클래스를 구현하도록 하였다.

 - 곱하기를 처리하는 calcMultiplay 메서드도 구현하였다.

public class Calculator {
    public int calcSum(String path) throws IOException {

        return fileReaderTemplate(path, new BufferedReaderCallback() {
            @Override
            public Integer doSomeThingWithReader(BufferedReader br) throws IOException {
                Integer sum = 0;
                String line;

                while((line = br.readLine())!= null){
                    sum += Integer.valueOf(line);
                }
                return sum;
            }
        });
    }

    public int calcMultiply(String path) throws IOException{

        return fileReaderTemplate(path, new BufferedReaderCallback() {
            @Override
            public Integer doSomeThingWithReader(BufferedReader br) throws IOException {
                Integer multiply = 1;
                String line;

                while((line = br.readLine())!= null){
                    multiply *= Integer.valueOf(line);
                }
                return multiply;
            }
        });
    }

    public Integer fileReaderTemplate(String filePath, BufferedReaderCallback callback) throws IOException {
        BufferedReader br = null;

        try{
            br = new BufferedReader(new FileReader(filePath));
            int ret = callback.doSomeThingWithReader(br);
            return ret;
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } finally {
            if(br != null){
                try{br.close();}catch(Exception e){e.printStackTrace();}
            }
        }
    }
}

...

public interface BufferedReaderCallback {

    Integer doSomeThingWithReader(BufferedReader br) throws IOException;
}

 

2.4. 2차 코드 분석

 - 구현된 메서드들에 대해 2차적으로 중복 또는 바뀌는 부분을 찾아본다.

 * 중복되는 부분

 1) line을 읽는 부분

 2) 계산된 결과를 리턴하는 부분

 

 * 바뀌는 부분

 1) 읽은 line에 대해 연산을 수행하는 부분

 2) 계산 시작 값 (더하기 = 0, 곱하기 = 1)

 

2.5. 2차 코드 리팩토링

 - 마찬가지로 중복되는 부분을 메서드로 생성한다. 기존 fileReaderTemplate을 lineReadTemplate으로 수정하여 구현했다.

 - LineCallback 인터페이스는 읽은 line에 대한 값과 파라미터 값을 더하거나, 곱하기 위해 생성했다.

 - initVal는 초기 값으로 더하기일 경우 0을, 곱하기일 경우 1을 넣어준다.

 

public interface LineCallback {
    Integer doSomethingWithLing(String line, Integer value);
}

...
public class Calculator {
    public int calcSum(String path) throws IOException {

        return lineReadTemplate(path, new LineCallback() {
            @Override
            public Integer doSomethingWithLing(String line, Integer value) {
                return value + Integer.valueOf(line);
            }
        }, 0);
    }

    public int calcMultiply(String path) throws IOException{

        return lineReadTemplate(path, new LineCallback() {
            @Override
            public Integer doSomethingWithLing(String line, Integer value) {
                return value * Integer.valueOf(line);
            }
        }, 1);
    }
    
    public Integer lineReadTemplate(String filePath, LineCallback callback, int initVal) throws IOException {
        BufferedReader br = null;
        try{
            br = new BufferedReader(new FileReader(filePath));
            int res = initVal;
            String line;

            while((line = br.readLine()) != null){
                res = callback.doSomethingWithLing(line, res);
            }
            return res;
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } finally {
            if(br != null){
                try{br.close();}catch(Exception e){e.printStackTrace();}
            }
        }
    }
}

 

 - 이로써 로우 레벨의 파일 처리 코드가 템플릿으로 분리되고, 연산 관련 메서드들은 데이터를 가져와 계산한다는 기능에 충실한 코드만을 갖게 되었다.

 


3. 스프링의 JdbcTemplate

 - 예제를 통해 자바 코드를 통한 DB 처리 로직을 개발했으나, 스프링에서도 JDBC를 이용하는 DAO에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다. 그 중 하나가 JdbcTemplate이다. JdbcContext에서 JdbcTemplate을 사용하는 코드로 DAO를 변경해보자. JdbcTemplate은 생성자 파라미터로 DataSource를 주입해주면 된다.

 

3.1. UserDao 리펙토링

public class UserDao {

    private JdbcTemplate jdbcTemplate;

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource){
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.dataSource = dataSource;
    }
    
    ...
}

 

3.2. UserDao 메서드 리팩토링

 - 기존 코드는 UserDao에서 jdbcContext를 주입받고, executeSql을 실행하고 있다. executeSql은 PreparedStatement를 생성하는 익명 클래스를 구현한 후 workWithStatementStrategy 메서드에 전달하고, 여기서 DB 연결 및 executeUpdate를 통한 DB 쿼리 처리가 실행된다.

public void deleteAll() throws SQLException{
     jdbcContext.executeSql("delete from users");
}

...

public void executeSql(final String query) throws SQLException{
        workWithStatementStrategy(
                new StatementStrategy() {
                    @Override
                    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                        return c.prepareStatement(query);
                    }
                }
        );
    }
    
 public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try{
            c = dataSource.getConnection();

            ps = stmt.makePreparedStatement(c);

            ps.executeUpdate();

            // execute : 수행 결과를 Boolean 타입으로 반환
            // executeQuery : select 구문을 처리할 때 사용하며, 수행 결과를 ResultSet 타입으로 반환
            // executeUpdate : INSERT, UPDATE, DELETE 구문을 처리할 때 사용하며, 반영된 레코드 수를 int 타입으로 반환.
        }catch(SQLException e){
            e.printStackTrace();
            throw e;
        }finally {
            if(ps != null){ try{ ps.close();} catch (Exception e){e.printStackTrace();} }
            if(c != null){ try{ c.close(); }catch(Exception e){ e.printStackTrace();} }
        }
    }

 

하지만 JdbcTemplate을 사용하면 아래와 같이 단 한줄로 작업이 끝난다. SQL 쿼리만 넘기면 된다.

public void deleteAll() throws SQLException{
        this.jdbcTemplate.update("delete from users");
    }

 

이게 가능한 이유는 JdbcTemplate에서도 우리가 지금껏 했던 작업과 같이 템플릿화 되있기 때문이다. 내부 코드 일부를 보면 알 수 있듯이 여기서도 메서드/콜백 패턴이 사용됨을 알 수 있다.

 

public int update(final String sql) throws DataAccessException {
		Assert.notNull(sql, "SQL must not be null");
		if (logger.isDebugEnabled()) {
			logger.debug("Executing SQL update [" + sql + "]");
		}
       		// 메서드 콜백 패턴 사용
		class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
			public Integer doInStatement(Statement stmt) throws SQLException {
				int rows = stmt.executeUpdate(sql);
				if (logger.isDebugEnabled()) {
					logger.debug("SQL update affected " + rows + " rows");
				}
				return rows;
			}
			public String getSql() {
				return sql;
			}
		}
        
        	// 추가 비지니스 로직 처리
		return execute(new UpdateStatementCallback());
	}

 

나머지 add, getCount, get 메서드 또한 jdbcTemplate을 사용한 방식으로 리팩토링 한다.

public void add(final User user)  {
	jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
    		user.getId(), user.getName(),user.getPassword());
}
public int getCount(){
	return jdbcTemplate.queryForInt("select count(*) from users");
}
public User get(String id){
        return jdbcTemplate.queryForObject("select * from users where id = ?", new Object[]{id},
                new RowMapper<User>() {
                    @Override
                    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                        User user = new User();
                        user.setId(rs.getString("id"));
                        user.setName(rs.getString("name"));
                        user.setPassword(rs.getString("password"));
                        return user;
                    }
                });
    }
public List<User> getAll() {
        return jdbcTemplate.query("select * from users order by id",
                new RowMapper<User>() {
                    @Override
                    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                        User user = new User();
                        user.setId(rs.getString("id"));
                        user.setName(rs.getString("name"));
                        user.setPassword(rs.getString("password"));
                        return user;
                    }
                });
    }

 

3.3. 중복 코드 제거

 - RowMapper 오브젝트를 생성하는 부분이 중복되어있다. 사용되는 RowMapper를 보면 상태정보가 없다. 즉, 먼저 생성해놓고 재사용해도 무관하다는 뜻이다.

 

public class UserDao {

	...
    
	private RowMapper<User> userMapper = new RowMapper<User>() {

        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
            return user;
        }
    };
    
	...
    
    public User get(String id){
        return jdbcTemplate.queryForObject("select * from users where id = ?", new Object[]{id},
                userMapper);
    }
    
    public List<User> getAll() {
        return jdbcTemplate.query("select * from users order by id",
                userMapper);
    }
}

4. 회고

 자바를 통해 템플릿/콜백 패턴을 적용해보고, JdbcTemplate을 통해 스프링이 제공하는 템플릿/콜백이 적용된 클래스도 사용해보며, 스프링에서 제공하는 클래스를 단순하게 여기고 사용했지만, 아주 정교하게 템플릿화된 클래스들이라는 것을 몸소 느낄 수 있었다.

 필자가 작성하는 코드 중 스프링 프레임워크만을 사용해 구현한 부분도 있었으나, 비지니스 로직을 구현하는 부분도 많았다. 여러번 수정하여 나름의 리펙토링을 거쳤다고 생각했지만, 이제보니 OOP 개념은 전혀 생각하지 않은 메서드 분리 정도의 리펙토링이었을 뿐이었다.

 템플릿/콜백은 스프링이 OOP에 얼마나 가치를 두고 있는지를 잘 보여준다고 한다. 스프링이 제공하는 이러한 클래스와 메서드들을 상황에 맞게 잘 사용하는 것을 물론이고, 직접 코드를 구현할 때에도 여러 디자인 패턴들을 적용하여 활용할 수 있는 것이 자바 개발자의 기본 소양이라고 생각된다.

 추가적으로, 템플릿/콜백 패턴을 도출해내는 과정이 잘 이해되지 않아서 다시한번 정리해보았다. 이 과정에서 어려움을 겪는 분에게 좋은 참고가 되었으면 좋겠다.

https://tlatmsrud.tistory.com/101

 

반응형

+ Recent posts