[토비의 스프링] Week4(3.5~3.7)

12 분 소요

토비의 스프링 3.1 Chapter 3.5 ~ 3.7

Chapater 3.5 템플릿과 콜백

템플릿/콜백 패턴

  • 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식
    • 템플릿: 전략 패턴의 컨텍스트
    • 콜백: 익명 내부 클래스로 만들어지는 오브젝트

3.5.1 템플릿/콜백의 동작원리

  • 고정된 작업 흐름을 가진 코드를 재사용한다는 의미의 템플릿 내부에서 콜백 오브젝트가 호출되면서 동작

템플릿/콜백의 특징

  • 콜백은 보통 단일 메소드 인터페이스를 사용
    • 특정 기능을 위해 한 번 호출되는 경우가 일반적
    • 하나 이상 사용할 수도 있음
  • 콜백의 파라미터
    • 템플릿 내부에서 만들어지는 컨텍스트 정보를 전달받을 때 사용
  • DI 방식의 전략 패턴 구조
    • 클라이언트가 템플릿 메소드를 호출, 콜백 오브젝트를 전달하는 것은 메소드 레벨에서 일어나는 DI
    • T/C 방식에서는 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받음
    • 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조

3.5.2 편리한 콜백의 재활용

  • 익명 내부 클래스 때문에 코드를 작성하고 읽기가 조금 불편

콜백의 분리와 재활용

  public void deleteAll() throws SQLExecption {
    this.jdbcCOntext.workWithStatementStrategy(
      new StatementStrategy() {
        public PreparedStatement makePreparedStatement(Connention c) throws SQLExecption {
          return c.prepareStatement("delete from users");
        }
      }
    )
  }
  • 익명 클래스 내부의 SQL 문장은 계속 변할 수 있다.
    public void deleteAll() throws SQLException {
        executeSql("delete from users");
    }
    
    private void executeSql(final String query) throws SQLException {
        jdbcContext.workWithStatementStrategy(new StatementStrategy() {
            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement(query);
                return ps;
            }
        });
    }
    
  • 변하지 않는 부분을 분리
  • 재활용 가능한 콜백을 담아냄

콜백과 템플릿의 결합

  • executeSql() 메소드는 UserDao 뿐만 아니라 다른 DAO에서 사용될 수 있다
  • 템플릿은 JdbcContext 클래스가 아니라 그 내부의 workWithStatementStrategy 메소드이므로 jdbcContext 클래스로 excuteSql 메소드를 옮기는 것이 좋다
    public void deleteAll() throws SQLException {
        this.jdbcContext.executeSql("delete from users");
    }
    
    public class JdbcContext {
      ...
      public void executeSql(final String query) throws SQLException {
          workWithStatementStrategy(new StatementStrategy() {
              @Override
              public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                  PreparedStatement ps = c.prepareStatement(query);
                  return ps;
              }
          });
      }
      ...
    }
    
  • executeSql을 JdbcContext 내부로 옮기고 접근이 가능하게 public으로 수정
  • UserDao 메소드에서도 jdbcCOntext를 통해 executeSql 메소드를 호출하도록 수정
  • 하나의 목적을 위해 서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들은 한 군데 모여있는 게 유리

3.5.3 템플릿/콜백의 응용

  • 스프링은 템플릿/콜백 패턴을 적극적으로 활용하는 프레임워크
  • 고정 된 작업 흐름을 갖고 있으면서 반복되는 코드가 있다면, 분리할 방법을 생각해보는 습관을 길러보자
  • 템플릿/콜백 패턴의 주요 대상 - try/catch/finally를 사용하는 코드
  • 책의 calcSum() 코드 참고

중복의 제거와 템플릿/콜백 설계

  • 추가 요구사항이 들어오면? 코드 복붙?? X
  • 템플릿/콜백 패턴 적용
    • 템플릿에 담을 반복되는 작업 흐름 정하기
    • 템플릿 to 콜백, 콜백 to 템플릿 각각 전달해야 할 내용이 무엇인지 파악
  • BufferedReader을 전달받아 결과값 돌려주는 콜백 적용 (3-35)
  • 추가 요구사항에 따라 곱셈 기능도 수행하는 콜백 메서드 생성

템플릿/콜백의 재설계

  • 곱셈 콜백 메서드와 덧셈 콜백 메서드는 공통되는 부분이 존재
  • 계산한 값을 넘겨주는 콜백 인터페이스 생성
  • 그 콜백을 사용하는 템플릿 생성
  • 템플릿을 사용하도록 곱셈과 덧셈 메소드를 수정 (결과값을 return 해주는 방식)

제네릭스를 이용한 콜백 인터페이스

  • 결과 타입을 Integer뿐만 아니라 다양한 타입으로 받고 싶다면?
    public <T> T lineReadTemplate(String numbersFilePath, LineCallback<T> callback, T initValue) throws IOException {
    BufferedReader br = null;
    try {
      br = new BufferedReader(new FileReader(numbersFilePath));
      T resultValue = initValue;
      String line = null;
      while ((line = br.readLine()) != null) {
        resultValue = callback.doSomethingWithLine(line, resultValue);
      }
      return resultValue;
    } catch (IOException e) {
      System.out.println(e.getMessage());
      throw e;
    } finally {
      if (br != null) {
        try {
        br.close();
        } catch (IOException e) {
          System.out.println(e.getMessage());
        }
      }
    }
    }
    
  • 타입 파라미터 T를 갖는 인터페이스 LineCallback
  • T 타입의 초기값 initValue를 받아서 T 타입 변수 resultValue를 정의
  • T 타입으로 선언된 LineCallBack 메소드 호출해서 처리하고 T 타입의 결과를 리턴하는 메소드

3.6 스프링의 JdbcTemplate

  • 스프링은 JDBC를 이용하는 DAO에서 사용가능한 다양한 템플릿과 콜백을 제공
  • JdbcTemplate: JdbcContext와 유사하지만 훨씬 강력하고 편리한 기능 제공

3.6.1 update()

  • 3.5장에서 만들었던 executeSql()과 비슷
    public void deleteAll() {
    this.jdbcTemplate.update("delete from users");
    }
    
  • 치환자를 가진 SQL로 PreparedStatement를 만들고 함께 제공하는 파라미터를 순서대로 바인딩해주는 기능
    this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)", 
    user.getId(), user.getName(), user.getPassword());
    
  • 파라미터를 바인딩

3.6.2 queryForInt()

  • 기존의 getCount()는 쿼리를 실행하고 ResultSet을 통해 결과 값을 가져오는 방식 ```java public int getCount() { return this.jdbcTemplate.query(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection con) throws SQLException { return con.prepareStatement(“select count(*) from users”); } }, new ResultSetExtractor() { @Override public Integer extractData(ResultSet rs) throws SQLException, DataAccessException { rs.next(); return rs.getInt(1); } }); }
- PreparedStatementCreator 콜백:  템플릿으로부터 Connection을 받고 PreparedStatement를 돌려줌
- ResultSetExtractor 콜백: 템플릿으로부터 ResultSet을 받고 거기서 추출한 결과를 돌려줌
- 2번째 콜백의 리턴 값은 템플릿 메소드 결과로 다시 리턴
- queryForInt()는 스프링 3.2.2 이후로 더 이상 사용하지 않음
- queryForObject()로 대신
```java
  public int getCount() throws SQLException {
    return this.jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
  }

3.6.3 queryFoObject()

  • 기존의 get() 메소드는 SQL에 바인딩이 필요한 치환자가 필요하고 ResultSet을 User 오브젝트로 변환한다
    public User get(String id) {
          return this.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;
              }
          });
      }
    
  • ResultSetExtractor -> RowMapper
  • 첫번째 파라미터는 PreparedStatement를 만들기 위한 SQL, 두번째는 여기 바인딩할 값들
  • ResultSetExtractor는 그냥 최종 결과 리턴까지 알아서 한번에, but RowMapper는 ResultSet의 로우 하나 매핑에 사용 되어서 여러번 호출될 수 있다

3.6.4 query()

  • 기존의 getAll() 메소드는 테이블의 모든 로우를 가져와 List 타입으로 돌려준다
    public List<User> getAll() {
    return this.jdbcTemplate.query("select * from users order by id", 
      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;
        }
      })
    }
    
  • query()는 SQL을 실행해서 얻은 ResultSet의 모든 Row를 열람하면서 Row마다 RowMapper을 호출
  • RowMapper는 현재 Row의 내용을 User 타입 오브젝트에 매핑해서 돌려준다
  • 만들어진 User 오브젝트는 템플릿이 미리 준비한 List 컬렉션에 추가됨
  • 테스트 보완
    • query()는 결과가 없을 경우 크기가 0인 List 오브젝트 반환
    • 스프링에서 주어진 query()를 사용해도 내가 getAll() 메소드에서 바꿔서 구현했을수도 있기 때문에 검증해야함

3.6.5 재사용 가능한 콜백의 분리

  • 이제 UserDao가 코드 분량도 많이 줄고 각 메소드 기능 파악도 쉬워졌다
  • 아직 안 끝났다

DI를 위한 코드 정리

  • 필요없는 DataSource 인스턴스 변수 제거 ```java private JdbcTemplate jdbcTemplate;

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

- DataSource 오브젝트는 JdbcTemplate을 만든 후에는 사용X, 저장해두지 않아도 된다

### 중복 제거
- get()과 getAll()의 RowMapper이 중복된다
- 지금이야 두번에 불과하지만 row를 User오브젝트로 가져오는 작업은 앞으로 수없이 진행될 것
- 현재 콜백을 메소드에 분리해서 중복 제거하고 재사용하자
```java
private RowMapper<User> userMapper = 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;
  }
};

템플릿/콜백 패턴과 UserDao

  • UserDao에는 User 정보를 DB에서 조작하는 방법에 대한 핵심 코드를 갖고 있다
  • JdbcTemplate에는 JDBC API를 사용하는 방식, 예외처리, 리소스 반납, DB ㅕㄴ결 가져오는 방법에 관한 책임과 관심이 담겨 있다
  • UserDao와 jdbcTemplate는 템플릿/콜백 구현에 대한 강한 결합을 갖고 있다
  • 더 개선할 점?
    • userMapper가 인스턴스 변수로 설저, 한번 만들어지면 변경X => 아예 UserDao 빈의 DI용 프로퍼티로 만들어버리면?
    • DAO 내 SQL문장을 코드가 아니라 외부 리소스에 담고 사용하기?

3.7 정리

  • JDBC와 같은 예외가 발생할 가능성이 있으며 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야 한다.
  • 일정한 작업 흐름이 반복되면서 그중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다. 바뀌지 않는 부분은 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다.
  • 같은 애플리케이션 안에서 여러 가지 종류의 전략을 다이내믹하게 구성하고 사용해야 한다면 컨텍스트를 이용하는 클라이언트 메소드에서 직접 전략을 정의하고 제공하게 만든다.
  • 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간결해지고 메소드의 정보를 직접 사용할 수 있어 편리하다.
  • 컨텍스트가 하나 이상의 클라이언트 오브젝트에 사용된다면 클래스를 분리해서 공유하도록 만든다.
  • 컨텍스트는 별도의 빈으로 등록해서 DI 받거나 클라이언트 클래스에서 직접 생성해서 사용한다. 클래스 내부에서 컨텍스트를 사용할 때 컨텍스트가 의존하는 외부의 오브젝트가 있다면 코드를 이용해서 직접 DI 해줄 수 있다.
  • 단일 전략 메소드를 갖는 전략 패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이 라고 한다.
  • 콜백의 코드에도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.
  • 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다.
  • 스프링은 JDBC 코드 작성을 위해 JdbcTemplate을 기반으로 하는 다양한 템플릿과 콜백을 제공한다.
  • 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러 번 호출할 수도 있다.
  • 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 둬야 한다.

댓글남기기