오상우
오상우

Categories

  • java
  • spring

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 제공 템플릿 클래스를 활용해 코드를 리팩토링 한 것이다.

제네릭을 이용해 콜백의 리턴 타입을 다이내믹하게 정할 수 있다.

템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 신경을 써야 한다.

결론

템플릿/콜백은 실제 스프링에서 몹시 흔하게 사용되는 객체지향 패턴이다. 스프링이 제공하는 템플릿/콜백을 잘 사용할 뿐 아니라 직접 템플릿/콜백을 만들어 쓸 수 있어야 한다.