[토비의 스프링] 3장 템플릿

1장에서 초난감 dao 에서 커넥션 생성에관한 책임분리를 DI와 IOC에 대해 배우면서 진행했고 이번장에서는 탬플릿/콜백 패턴을 배우면서 쿼리만드는것을 제외한 모든 책임을  분리하고 코드 중복을 없애는 리팩토링 과정을 보여준다.

 

0. 리소스 반환을 위해 try catch 범벅

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
      c = dataSource.getConnection();
      // 이 라인 빼고 반복되는 부분이다
      ps = c.prepareStatement("DELETE FROM users");
      ps.executeUpdate();
    } catch (SQLException e){
      throw e;
    } finally {
      if (ps != null) {
        try {
          ps.close();
        } catch (SQLException e) {}
      }
      if (c != null) {
        try {
          c.close();
        } catch (SQLException e) {}
      }
    }
  }

 

1. 템플릿 메서드 패턴으로 반복되는 부분 재사용

쿼리 만드는 부분만 상속을 이용해서 확장하고 나머지 부분은 재활용 하게 디자인패턴을 적용한다.

일정한 흐름을 만들어놓고 특정부분만 자식클래스에서 구현해서 사용하게 하는것을 탬플릿메서드 패턴이라 한다.

abstract public class UserDao {
  ...

  abstract protected PreparedStatement makePreparedStatement(Connection c) throws SQLException;
  
  public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
      c = dataSource.getConnection();
      ps = makePreparedStatement(c);
      ps.executeUpdate();
    } catch (SQLException e){
      throw e;
    } finally {
      if (ps != null) {
        try {
          ps.close();
        } catch (SQLException e) {}
      }
      if (c != null) {
        try {
          c.close();
        } catch (SQLException e) {}
      }
    }
  }
}

class UserDeleteAllDao extends UserDao {
  @Override
  protected PreparedStatement makePreparedStatement(Connection c) throws SQLException {
    return c.prepareStatement("delete from users");
  }
}

 

바뀌는 부분만 상속을 통해 확장했지만 단점이 있다.

  1. 기능 확장시마다 서브클래스를 만들어어한다
    1. userDeleteAllDao, UserDeleteOneDao, UserFindOneDao ...
  2. 컴파일 타임에 확장구조가 결정되어 버린데
    1. 여태까지 봤던 DI를 이용한 런타임의 유연한 확장들은 할 수 없다.
    2. 하지만 명확하게 파악가능하기 때문에 이것은 좋은점도 있다.

 

2. 전략 패턴을 사용

안바뀌는 부분(Context) 에서 바뀌는 부분(Strategy) 를 분리하고 그것을 인터페이스로 만들어 확장할수 있게하는 디자인 패턴이 전략패턴이다. 

interface StatementStrategy {
  PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

class DeleteAllStatement implements StatementStrategy {
  public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
    return c.prepareStatement("delete from users");
  }
}

public class UserDao {
  private DataSource dataSource;
  private StatementStrategy statementStrategy;

  private 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 {
          c.close();
        } catch (SQLException e) {}
      }
    }
  }

  public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy(new DeleteAllStatement());
  }
  
  // 분리함으로서 쉽게 context 재사용 가능
  public void deleteOne(String id) throws SQLException {
    jdbcContextWithStatementStrategy(new DeleteOneStatement(id));
  }
}

 

코드 재사용관점에서 리팩터링할때 1단계에서 상속을 이용한 한것과 다르게 이번엔 전략패턴을 사용함으로서 바뀌는부분을 DI 받을 수 있게 되고 안바뀌는 부분을 분리할수있게 된다. 타입에 계층을 만드는것이 아니라면 상속보단 합성이 낫다.

 

3. 익명 클래스 사용해서 클래스 파일 줄이기

 런타임에 유연하게 확장가능한 장점은 얻었고 함수로 코드를 잘 분리했지만 클래스파일이 많아지는 문제는 여전하다. 이것은 익명클래스로 해결 가능하다.

public class UserDao {
  private DataSource dataSource;
  private StatementStrategy statementStrategy;

  private void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
    ...
  }

  public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy(new StatementStrategy() {
      @Override
      public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
      }
    });
  }

  public void add(User user) throws SQLException {
    jdbcContextWithStatementStrategy(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.id);
        ps.setString(2, user.name);
        ps.setString(3, user.password);
        return ps;
      }
    });
  }
}

 

어차피 UserDao만 사용하고 재사용하지 않는 클래스이니 이렇게 파일수를 줄이는게 좋을것 같다.

 

4. Context를 클래스로 분리하기

UserDao 한개의 클래스에 jdbc 관련 컨텍스트와 쿼리만드는 코드가 같이 있다. 두가지의 책임을 가지고 있다 볼수있는데. 하나의 책임만 가지게 클래스 분리할수있다. 게다가 jdbc 관련 코드는 다른 Dao에서도 재사용 가능해 보이기때문에 일거양득이다.

interface StatementStrategy {
  PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

class JdbcContext {
  private DataSource dataSource;

  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);
      ps.executeUpdate();
    } 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 class UserDao {
  private JdbcContext jdbcContext;
  // 쿼리만 만든다!
  public void deleteAll() throws SQLException {
    jdbcContext.workWithStatementStrategy(new StatementStrategy() {
      @Override
      public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
      }
    });
  }
}

// jdbcContext 재사용
public class OtherDao {
  private JdbcContext jdbcContext;

  public void deleteAll() throws SQLException {
  	// 옛날 책이라 람다 슬쩍 넣어봄
    jdbcContext.workWithStatementStrategy((Connection c) -> c.prepareStatement("delete from others"));
  }
}

 

이렇게 전략패턴을 잘 적용했다. 여기서 context를 template 그리고 strategy를 callback 에 대응해서 템플릿/콜백 패턴으로도 볼수있다.

 

5. 템플릿/콜백 패턴 적용해서 중복 좀더 제거

이전보단 조금 가볍게 패턴을 이용해서 PreparedStatement 만드는 부분의 중복을 좀더 개선할수 있다.

쿼리 문자열이 콜백이고 PreparedStatement 생성부분은 템플릿으로 보고 진행하면 된다.

class JdbcContext {
  private DataSource dataSource;

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

  public void executeSql(final String query) throws SQLException {
    workWithStatementStrategy((Connection c) -> c.prepareStatement(query));
  }

   void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
    ...
  }
}

public class UserDao {
  private JdbcContext jdbcContext;

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

 

아직 add 같은것에는 적용하지 못하지만 JdbcContext에 제네릭을 더하는 식으로 더 넓게 쓸수있게 구현할 수 있다.

 

6. 잘 만들어둔 JdbcTemplate 적용

add 같은것들도 JdbcTemplate으로 잘 만들어져있다 Context , Template 딱딱 들어맞는게 참 재밌다.

public class UserDao {
  private JdbcTemplate jdbcTemplate;

  public void deleteAll() throws SQLException {
    jdbcTemplate.update("delete from users");
  }
  
  public void add(User user) throws SQLException {
    jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)", user.id, user.name, user.password);
  }
  
  // queryForInt, queryForObject, getAll 등등...
  // ResultSet과 객체 매핑을 위한 RowMapper
}

 

 

마무리

디자인패턴을 적용하려고 좋은 코드 얻는 연습을 많이 해야겠다.

다른 자주 쓰는 디자인 패턴도 공부를 해봐야겠다.