[토비의 스프링] Week4(3.5~3.7)
토비의 스프링 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을 기반으로 하는 다양한 템플릿과 콜백을 제공한다.
- 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러 번 호출할 수도 있다.
- 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 둬야 한다.
댓글남기기