
성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 부분으로부터 독립시켜서 활용하는 방법
JDBC에서 Connection과 PreparedStatement는 보통 풀 방식으로 운영된다. 미리 정해진 풀안에 제한된 수의 리소스를 만들어 두고 필요할 때 할당하고 다시 반환하면 풀에 넣는 방식으로 운영된다.
이때 close() 메소드를 통해 사용한 리소스를 반환한다.
PreparedStatement ps = c.prepareStatement("delete from user");
ps.executeUpdate();
ps.close();
c.close();
중간에 예외가 발생하면 메소드 실행을 끝마치지 못하고 메소드를 빠져나가게 된다. 그러면 close()가 실행되지 않고 리소스가 반환되지 않는다. 그러면 반환되지 않는 커넥션이 계속 쌓이고, 커넥션 풀에 여유가 없어져 심각한 오류를 내며 서버가 중단될 수 있다.
try{
} catch (SQLException e){
} finally{
if (ps != null){
try{
ps.close();
}
catch (SQLException e){}
}
}
finally는 중간에 예외가 발생하든 안하든 작성된 코드가 실행된다. finally에서는 반드시 c와 ps가 null이 아닌지 확인후 close() 메소드를 호출해야한다.
동작에는 전혀 문제가 없지만, 디비에 연결하는 매 함수마다 close()하는 함수를 작성을 빼먹는 것과 같은 실수가 일어나면 문제가 발생한다. 당장에는 테스트를 실행해도 문제가 없어 보인다. 최대 DB 커넥션 개수를 넘어설 것이고, 서버에서 리소스가 꽉 찼다는 에러로 서비스가 중단될 것이다.
DAO로직을 수정하면 try/catch/finally 블록 안에서 필요한 부분을 찾아서 수정하고 close를 한번이라도 잊으면 문제가 반복된다.
public class UserDaoDeleteAll extends UserDao {
protected PreparedStatment makeStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
}
}
슈퍼클래스 메소드와 필요에 따라서 상속을 통해 구체적인 PreparedStatement를 바꿔서 사용할 수 있게 만드는 서브클래스로 분리할 수있다.
하지만, 템플릿 메소드 패턴으로의 접근은 제한이 많다. 가장 큰 문제는 DAO 로직 마다 새로운 클래스를 만들어야 한다는 점이다.
인터페이스
public interface StatementStrategy {
PreparedStatment makePreparedStatement(Connection c) throws SQLException;
}
Delete에 대한 구현체
public class DeleteAllStatement implements StatementStrategy{
public PreparedStatement makePreparedStatemnt(Connection c) throws SQLException{
PreparedStatement ps = c.preparedStatement("delete from users");
return ps;
}
}
UserDao 에서 사용
public void deleteAll() throws SQLException{
//생략
try {
c = dataSource.getConnection();
StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.makePreparedStatement(c);
ps.executeUpdate();
}catch(SQLException e){
//생략
}
}
전략 패턴을 사용해서 필요한 구현체를 사용한다. 하지만, 현재로써는 전략 클래스가 코드내에 고정되어 있기 때문에 문제가 있다.
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException{
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(c);
ps.executeUpdate();
} catch(SQLException e) {
throw e;
} finally {
if(ps!=null) {try {ps.close();} catch(SQLException e) {}}
if(c!=null) {try {ps.close();} catch(SQLException e) {}}
}
}
//클라이언트 역할
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); //전략 인스턴스 생성
jdbcContextWithStatementStrategy(st); //컨텍스트에 전략 인스턴스 인자로 호출
}
메소드 추출을 통해 JDBC에 연결하는 역할을 하는 코드를 함수로 분리시킨다.
add()메소드를 만드는 과정을 생각해보자. deleteAll()과 달리 add()에서는 User라는 부가적인 정보가 필요하다.
public class AddStatement implements StatementStrategy {
User user;
public AddStatement(User user) {
this.user = user;
}
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
}
생성자를 통해 정보를 받아올 수 있도록 구현한다.
현재까지의 전략패턴의 문제는 DAO의 메소드마다 새로운 구현 클래스를 만들어야 한다는 것이다. 템플릿 메소드 패턴을 적용했을 때 생긴 문제인 클래스 파일의 개수가 많이 늘어나는 문제가 있다.
클래스 파일이 많아지는 문제에 대한 간단한 해결방법중 하나는 로컬 클래스이다.
public void add(final User user) throws ClassNotFoundException, SQLException{
//내부 클래스로 선언
class AddStatement implements StatementStrategy {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
ps.setString(1, user.getId()); //외부 add메소드의 user 변수에 접근 가능
ps.setString(2, user.getName()); //외부 add메소드의 user 변수에 접근 가능
ps.setString(3, user.getPassword()); //외부 add메소드의 user 변수에 접근 가능
return ps;
}
}
StatementStrategy st = new AddStatement(); //내부 클래스가 외부 메소드 user를 사용할 수 있으므로 생성자로 user를 넘길 필요가 없어짐.
jdbcContextWithStatementStrategy(st);
}
이렇게 메소드 내에 메소드마다 추가해야 했던 클래스 파일을 하나로 줄일 수 있다는 장점이 있으며, 로컬 변수를 가져다 사용할 수있다는 것도 큰 장점이다.
메소드 내에서 한번밖에 사용하지 않을 클래스이기 때문에 굳이 클래스로 작성하지 않고 바로 생성하는 편이 낫다
public void add(final User user) throws ClassNotFoundException, SQLException{
jdbcContextWithStatementStrategy(new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
});
}
public void deleteAll() throws SQLException {
jdbcContextWithStatementStrategy(new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
});
}
jdbcContextWithStatementStrategy() 메소드는 UserDao 뿐아니라 다른 DAO에서도 사용 가능하기 때문에 클래스 밖으로 독립시켜 모든 DAO가 사용할 수있게 한다.
JdbcContext
public class JdbcContext {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
//전략패턴 컨텍스트 역할
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException{
//생략
}
}
UserDao에서 사용
public class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
private JdbcContext jdbcContext;
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
public void add(final User user) throws ClassNotFoundException, SQLException{
//생략
}
}
위에서 UserDao에서 JdbcContext를 의존하고 있는데 인터페이스가 아닌 구현 클래스를 통해 의존 받고 있다. 1장에서 인터페이스를 사이에 두고 적용하는 것이 DI의 조건 중 하나였는데 문제가 있지 않은가??
이 책에서 이야기 하는 것은 인터페이스를 적용해도 상관없지만, 꼭 그럴 필요는 없다는 것이다.
개념에 충실하려면 인터페이스를 사이에 두어서 런타임시에 다이내믹하게 주입하는 것이 맞다. 하지만, 2가지 이유로 구현 클래스를 직접 의존한다고 이야기한다.
첫째, JdbcContext가 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다.
둘째, JdbcContext가 dataSource를 주입받아야하는데, 그러기 위해서는 두 오브젝트 모두 빈으로 등록돼야 하기 때문이다.
하지만 나는 이 두가지 이유보다 이 책 아래 문장에서 이야기하는 것에 더 납득이 간다. UserDao는 항상 JdbcContext와 함께 사용되며 강한 응집도를 갖고 있고 테스트에서도 다른 구현으로 대체해서 사용할 이유가 없기 때문이다.
템플릿 콜백패턴은 전략패턴의 기본구조에 익명 내부 클래스를 활용한 방식이다.
전략패턴의 컨텍스트를 템플릿, 익명 내부 클래스를 콜백이라고 부른다.
콜백은 보통 단일 메소드 인터페이스를 사용한다. 위에서 익명 내부 클래스로 구현한 방식이 바로 템플릿/콜백 패턴이다.

템플릿이 사용할 콜백 인터페이스를 구현한 오브젝트를 메소드를 통해 주입해주어서 사용한다.
기존 코드
public void add(final User user) throws ClassNotFoundException, SQLException{
jdbcContextWithStatementStrategy(new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
});
}
public void deleteAll() throws SQLException {
jdbcContextWithStatementStrategy(new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
});
}
이렇게 작성한 템플릿/콜백의 한가지 아쉬운점은 익명 내부클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 조금 불편하다
따라서 익명 내부클래스에서 변하지 않는 모든 부분을 메소드 추출을 통해 빼낸다.
public void deleteAll() throws SQLException {
executeSql("delete from users");
}
private void executeSql(final String query) throws SQLException {
this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
이후 이 메소드는 UserDao의 관심사 보다는 JdbcContext의 관심사와 더 맞기 때문에 클래스를 옮긴다.
public class JdbcContext{
public void executeSql(final String query) throws SQLException {
workWithStatementStrategy(new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
}
스프링은 JdbcTemplate를 템플릿/콜백 패턴을 통해 제공한다.
이를 학습함과 동시에 UserDao를 JdbcTemplate를 사용하는 코드로 변경한다.
public void deleteAll() throws SQLException {
//콜백을 익명 클래스로 직접 만들어서 만들어 전달
this.jdbcTemplate.update(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement("delete from users");
}
});
}
위와 같이 콜백 인터페이스를 구현하여 넘기는 방법도 제공하고,
public void deleteAll() throws SQLException{
this.jdbcTemplate.update("delete from users");
}
우리가 메소드를 추출한 것과 같이 SQL문장만 전달해서 내장 콜백을 사용하는 기능도 제공한다.
add() 메소드는 값을 바인딩하는 인자들도 넘겨줘야 한다.
public void add(User user) throws ClassNotFoundException, SQLException{
this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
user.getId(), user.getName(), user.getPassword());
}
이전 장에서 만든 getCount()를 jdbcTemplate가 제공하는 query() 메소드를 사용하도록 바꿔본다.
public int getCount() throws SQLException{
return this.jdbcTemplate.query(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement("select count(*) from users");
}
}, new ResultSetExtractor<Integer>() {
public Integer extractData(ResultSet rs)
throws SQLException, DataAccessException {
rs.next();
return rs.getInt(1);
}
});
}
query()는 템플릿 메소드로 인자를 콜백함수 2개 받도록 되어 있다. PreparedStatementCreator 콜백의 실행 결과가 템플릿에 전달되고, ResultSetExtractor콜백은 템플릿이 제공하는 ResultSet을 이용해 원하는 값을 템플릿에 전달하고 최종적으로 query()의 리턴값으로 돌려주게 된다.
위의 코드를 재사용하려면 ResultSetExtractor 콜백을 템플릿 안으로 옮겨 재활용할 수 있다. JdbcTemplate는 queryForInt() 메소드가 이런 기능을 내장하고 있다.
public List<User> getAll(){
return this.jdbcTemplate.query("select * from users order by id",
new RowMapper() {
public Object 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;
}
});
}
query() 템플릿은 SQL 결과 ResultSet의 모든 로우를 열람하며 로우마다 RowMapper 콜백을 호출한다. 또한 queryForObject()는 결과가 없을 때 Exception을 던지지만 query()는 결과가 없으면 크기가 0인 List를 던진다.
재사용이 가능한 RowMapper를 이전에 했던 방식대로 메소드를 추출한다.
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) throws ClassNotFoundException, SQLException{
return this.jdbcTemplate.queryForObject("select * from users where id = ?",
new Object[] {id},this.userMapper);
}
public List<User> getAll(){
return this.jdbcTemplate.query("select * from users order by id",this.userMapper);
}