3. 템플릿

템플릿

1 다시보는 초난감 DAO

공유리소스 반환에 대한 문제 -> 예외 발생시점에서 리소스 반환이 안되는 경우 DB풀이 차기 때문에 문제가 발생할 수 있다. -> try/catch/finally 사용이 필수 -> 1.7이후 AutoClose 인터페이스 경우 : try~with~catch 구문을 활용하는게 이득 -> 리소스 반환은 역순으로 close()

2 변하는 것과 변하지 않는 것

변하는 부분과 변하지 않는 부분을 분리 (기능단위 메서드 분리) -> 템플릿메서드 패턴 활용 / 전략 패턴

  • 전략패턴

    public interface StatementStrategy {
      PreparedStatement makePreparedStatement(Connection connection) throws SQLException;
    }
  • 서브클래스1 - DeleteAllStatement

    public class DeleteAllStatement implements StatementStrategy {
    
      @Override
      public PreparedStatement makePreparedStatement(Connection connection) throws SQLException {
        PreparedStatement ps = connection.prepareStatement("delete from test");
        return ps;
      }
    }
  • 서브클래스2 - AddStatement

    public class AddStatement implements StatementStrategy {
    
      private User user;
    
      public AddStatement(User user) {
        this.user = user;
      }
    
      @Override
      public PreparedStatement makePreparedStatement(Connection connection) throws SQLException {
        PreparedStatement ps = connection.prepareStatement("insert into test values (?,?,?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());
        return ps;
      }
    }
  • 마이크로 DI 스프링컨테이너가 DI하는 것이 아니라 코드내에서 DI

3 JDBC 전략 패턴의 최적화

  • 익맹내부 클래스 사용 / 로컬 클래스 사용

    클래스파일을 줄일 수 있다

  public void test_3_19(){

    // 익명클래스 사용
    jdbcContextWithStatementStrategy(new StatementStrategy() {
      @Override
      public PreparedStatement makePreparedStatement(Connection connection) throws SQLException {
        PreparedStatement ps = connection.prepareStatement("select * from test ");
        return ps;
      }
    });

    // 람다 (함수형인터페이스)
    jdbcContextWithStatementStrategy(connection -> connection.prepareStatement("select * from test"));
  }

4 컨텍스트와 DI

JdbcContext 분리

  • JdbcContext

    public class JdbcContext {
    
    private DataSource dataSource;
    
    public JdbcContext(DataSource dataSource) { // DI
      this.dataSource = dataSource;
    }
    
    public DataSource getDataSource() {
      return dataSource;
    }
    
    // 전략패턴
    public void workWithStatementStrategy(StatementStrategy strategy) {
      try (Connection connection = dataSource.getConnection();
           PreparedStatement ps = strategy.makePreparedStatement(connection)) {
        ps.executeUpdate();
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
    }
  • .xml 설정

      <bean id="datasource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
          <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
          <property name="url" value="jdbc:mysql://localhost:3306/test"/>
          <property name="username" value="root"/>
          <property name="password" value="root"/>
      </bean>
    
      <bean id="jdbcContext" class="chap03_템플릿.JdbcContext">
          <constructor-arg ref="datasource"/>
      </bean>

    5 템플릿과 콜백

    템플릿 제공 -> 콜백으로 처리후 반환 된 데이터 재처리

  // 분리 전
  private void executeSql(String sql){
    this.jdbcContext.workWithStatementStrategy(connection -> connection.prepareStatement("delete from test"));
  }


  // 분리 후 (변경되는 부분 -> sql 만 분리)
  public void deleteAll() throws SQLException{
    executeSql("delete from test");  
  }

  // 메서드 분리 -> 변하지 않음
  private void executeSql(String sql){
    this.jdbcContext.workWithStatementStrategy(connection -> connection.prepareStatement(sql));
  }
  • 템플릿/콜백 응용

    public class Calculator {
    public Number calcSum(Path path) {
      try(BufferedReader br = Files.newBufferedReader(path)) {
        OptionalInt reduce = br.lines()
            .flatMapToInt(s -> IntStream.of(Integer.parseInt(s)))
            .reduce((left, right) -> left + right);
        return reduce.getAsInt();
      } catch (IOException e) {
        e.printStackTrace();
      }
      return 0;
    }
    }
    
    @Test
    public void test_3_30() throws URISyntaxException {
      Calculator calculator = new Calculator();
      Path path = Paths.get("numbers.txt");
      Number number = calculator.calcSum(path);
      System.out.println("number = " + number);
    }
  public Number fileReadTemplate(Path path, Function<BufferedReader, OptionalInt> function) {
    try (BufferedReader br = Files.newBufferedReader(path)) {
      return function.apply(br).getAsInt();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return 0;
  }

  public Number calcSum2(Path path, IntBinaryOperator op) {
    return fileReadTemplate(path, br -> br.lines()
        .flatMapToInt(s -> IntStream.of(Integer.parseInt(s)))
        .reduce(op));
  }

  @Test
  public void test_3_35() {
    Calculator calculator = new Calculator();
    Path path = Paths.get("numbers.txt");
    Number number = calculator.calcSum2(path, (left, right) -> left * right);
    System.out.println("number = " + number);
  }
  • 제네릭스를 이용한 방법

    // 사용은 책 참고
    public interface LineCallBack<T> {
    T doSomething(String line, T value);
    }

6 스프링의 JdbcTemplate

위에서 만든 JdbcContext 클래스 대신 사용

public class UserDao {
  private DataSource dataSource;
  private JdbcTemplate JdbcTemplate;
  ...

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

  // JdbcTemplate 사용 -> JdbcContext와 동일 구조
  public void deleteAll2() {
    jdbcTemplate.update(connection -> connection.prepareStatement("delete from test"));
  }

  // sql문만 넘김 -> 내장 콜백 사용
  public void deleteAll3() {
    jdbcTemplate.update("delete from test");
  }

  // add 기존  -> AddStatement 전략 클래스
  @Override
  public PreparedStatement makePreparedStatement(Connection connection) throws SQLException {
    PreparedStatement ps = connection.prepareStatement("insert into test values (?,?,?)");
    ps.setString(1, user.getId());
    ps.setString(2, user.getName());
    ps.setString(3, user.getPassword());
    return ps;
  }

  // add
  public void add2(User user) {
    jdbcTemplate.update("inset into test values(?,?,?)",
            user.getId(), user.getName(), user.getPassword());
  }
}

  // Add 테스트 (JdbcTemplate)
  @Test
  public void test_3_47() {
    userDao.add2(new User("1", "so", "1234"));
  }

  // Delete 테스트 (JdbcTemplate)
  @Test
  public void test_3_46() {
    userDao.deleteAll2();
  }
  • queryForInt -> deprecated

  // 내장 콜백 x
  public int getCount() {
    return jdbcTemplate.query(
        connection -> connection.prepareStatement("select count(*) from test"), // 첫번째 파라미터 : PreparedStatementCreator
        resultSet -> {                                                          // 두번째 파라미터 : ResultSetExtractor
          if (resultSet.next()) {
            return resultSet.getInt(1);
          }
          return 0;
        });
  }
  // 디프리케이트 돼서 queryForInt -> queryForObject로 수정
  // 내장콜백 사용
  public int getCount2() {
    return jdbcTemplate.queryForObject("select count(*) from test", Integer.class);
  }
  • queryForObject

    RowMapper

  // 1.8이전 기준
  public User get2(String id) {
    return jdbcTemplate.queryForObject("select * from test 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;
          }
        });
  }

  // 1.8이상
  public User get22(String id) {
    return jdbcTemplate.queryForObject("select * from test where id = ?",
        new Object[]{id},
        (resultSet, i) -> {
          User user = new User();
          user.setId(resultSet.getString("id"));
          user.setName(resultSet.getString("name"));
          user.setPassword(resultSet.getString("password"));
          return user;
        }
    );
  }
  • getAll

    query() 같은 경우 데이터가 없으면 빈값을 리턴해줌

    queryForObject() 같은 경우는 데이터가 없는 경우 예외발생

  public List<User> getAll() {
    return jdbcTemplate.query("select * from test order by id",
        (resultSet, i) -> {
          User user = new User();
          user.setId(resultSet.getString("id"));
          user.setName(resultSet.getString("name"));
          user.setPassword(resultSet.getString("password"));
          return user;
        });
  }
  • 리팩토링 (중복되는 부분 제거)

    ```java // 중복되는 부분 메서드 추출 private RowMapper getRowMapper() { return (ResultSet rs, int rowNum) -> { User user = new User(); user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); return user; }; }

    public List getAllRefactoring() { return jdbcTemplate.query("select * from test order by id", getRowMapper() // private 사용 ); }

    @Test public void test_3_56() { System.out.println(userDao.getAllRefactoring()); // 리팩토링 후 맞는지 확인 Assert.assertThat(userDao.getAll().toString(), CoreMatchers.is(userDao.getAllRefactoring().toString())); }

```

Last updated