3장 템플릿
- 다시 보는 초난감 DAO
- 예외처리 기능을 갖춘 DAO
- db 리소스를 close 해야하니 try/catch/finally 로 해결
- 예외처리 기능을 갖춘 DAO
- 변하는 것과 변하지 않는 것
- JDBC try/catch/finally 코드의 문제점
- 모든 DAO코드에 넣어야 하기 때문에 불편함
- 리팩토링을 해보자.
-
분리와 재사용을 위한 디자인 패턴 적용
Connection c = null; PreparedStatement ps = null; try{ c = datasource.getConnection(); ps = c.prepareStatement("delete from users"); ps.executeUpdate(); } catch(Exception e){ throw e; } finally{ ... }
- ps = c.prepareStatement(“delete from users”); 만 제외하고 코드가 반복됨
-
방법 1 : 메소드를 추출. 하지만 재사용하는 코드를 메소드로 추출하는게 아니고 변경되는 부분이 메소드로 추출하였기 때문에 잘못된 리팩토링
... try{ ... ps = makeStatement(c); ... private PreparedStatement makeStatement(Connection c) thorws SQLException { PreparedStatement ps; ps = c.prepareStatement("delete from users"); return ps; }
-
방법 2 : 템플릿 메소드 패턴의 적용. try/catch/finally부분은 템플릿 형식이고, dao부분만 변하므로 상속을 통한 템플릿 메소드 패턴 적용. 하지만, DAO코드마다 클래스를 생성해야 하며, 상속을 통하여 그 관계가 정의되기 때문에 좋지 않은 방법
public class UserDaoDeleteAll extends UserDao { protected PreparedStatement makeStatement(Connection c) throw SQLException { ... } }
-
방법 3 : 전략 패턴의 적용. DAO 코드 부분을 전략으로 사용하여 인터페이스를 구현 후 해당 내용을 구현하여 사용. 하지만 결국 호출할 때는 그 구현체가 고정되기 때문에 뭔가 이상함.
// 전략 인터페이스 public interface StatementStrategy { PreparedStatement makePreparedStatement(Connection c) throw SQLException; } // 전략 인터페이스 구현체 public class DeleteAllStatement implements StatementStrategy { public PreparedStatement makePreparedStatement(Connection c) throw SQLException{ .... } } public void deleteAll() throw SQLException { ... try{ ... StatementStrategy strategy = new DeleteAllStatement(); // 전략 구현체 ps = strategy.makePreparedStatement(c); ...
-
방법 4 : DI 적용을 위한 클라이언트/컨텍스트 분리. 방법3을 좀더 개선해서 구현체를 클라이언트가 DI하도록 변경
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throw SQLException { // 전략을 DI함 ... ps = stmt.makePreparedStatement(c) ... } // 클라이언트 코드 public void deleteAll() throws SQLException { // 자신이 DI할 전략을 클라이언트가 정함 StatementStrategy st = new DeleteAllStatement(); jdbcContextWithStatementStrategy(st); }
- JDBC try/catch/finally 코드의 문제점
- JDBC 전략 패턴의 최적화
- 전략 클래스의 추가 정보
- 만약 전략에 사용할 메소드에 추가정보가 필요할 경우는 어떻게 해야할까?
-
즉 아래와 같이 특정 구현체에서 User라는 정보가 필요할 경우,
public class AddStatement implements StatementStrategy { public PreparedStatement makePreparedStatement(Connection c) throw SQLException{ ... // user를 받아 처리해야하지만, 이미 정해진 전략 메소드에서는 받을수 없음 ps.setString(1, user.getId()); ... } }
-
전략 메소드에서 받을 수 없으므로, 따로 인스턴스 변수를 사용하여 클라이언트에서 전략 구현체를 생성할 때 전달 해줘야함.
public class AddStatement implements StatementStrategy { User user; public AddStatement(User user) { this.user = user; } ... } // 클라이언트 public void add(User user) throws SQLException{ StatementStrategy st= new AddStatement(user); jdbcContextWithStatementStrategy(st); }
- 전략과 클라이언트의 동거
- 사실 전략 패턴도 템플릿 메소드 패턴처럼 DAO코드마다 클래스를 여러개 만들어야함.
- 또한 위처럼 추가 정보가 필요할 경우, 추가 작업이 필요함
- 이 두 가지 문제를 해결할 수 있는 방법을 생각해보자.
-
방법 1 : 로컬 클래스. 로컬 클래스는 메소드 안에 클래스를 선언하는 방식임. 클라이언트 코드내의 메소드에서 사용할 전략클래스를 직접 선언하여 사용. 함께 있어서 한 눈에 코드 파악이 가능. 또한 사실 같은 레벨에 있기때문에 변수를 서로 공유할 수 있어 User 인스턴스 변수는 필요없이 직접 바로 사용 가능. (단 final로 변경)
// 클라이언트 public void add(User user) throws SQLException{ // 로컬 클래스 선언 class AddStatement implements StatementStrategy { User user; public AddStatement(User user) { this.user = user; } ... } StatementStrategy st= new AddStatement(user); jdbcContextWithStatementStrategy(st); }
-
방법 2 : 익명 내부 클래스. 선언과 동시에 오브젝트를 사용하는 익명 내부 클래스로 개선가능.
// 클라이언트 public void add(final User user) throws SQLException{ jdbcContextWithStatementStrategy( new StatementStrategy() { ... } ); }
- 전략 클래스의 추가 정보
- 컨텍스트와 DI
- JdbcContext의 분리
- jdbcContextWithStatementStrategy()는 UserDao 뿐만 아니라 다른 Dao에서 사용 가능하므로 클래스를 분리함
-
JdbcContext를 만들고, DataSource가 필요하므로 빈 주입
public class JdbcContext { private DataSource dataSource; // DataSource 빈 DI public void setDataSource(DataSource dataSource){ this.dataSource = dataSource; } public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException{ // jdbcContextWithStatementStrategy 동일 ... }
-
UserDao는 JdbcContext를 DI 주입
public class UserDao{ ... private JdbcContext jdbcContext; // JdbcContext 빈 DI public void setJdbcContext(JdbcContext jdbcContext){ this.jdbcContext = jdbcContext; } // 사용 방식 public void add(final User user) throw ... this.jdbcContext.workWithStatementStrategy( new StatementStrategy() {...} ); }
- JdbcContext의 특별한 DI
- JdbcContext를 인터페이스가 아닌 구현체를 사용해서 DI를 하였는데, 이러면 DI의 의미가 있을까? 인터페이스를 구현해도 되지만, 꼭 그럴 필요는 없다. 그렇다면 그 기준은 무엇일까? JdbcContext와 UserDao는 의미상으로 강한 결합을 하고 있으므로, 구현체를 사용해도 괜찮다.
- 스프링 빈으로 DI : JdbcContext를 빈으로 만든 이유는 무엇일까? 먼저 빈으로 만들면 싱글톤으로 관리가 된다. JdbcContext는 싱글톤으로 적절하므로 빈으로 관리하면 좋다. 또한 JdbcContext는 DataSource라는 빈을 주입받아야 하는데, 주입을 받으려면 자신도 빈이 되어야 한다.
- 코드를 이용하는 수동 DI : JdbcContext를 빈으로 등록하지 않고 사용할 수도 있다. UserDao에서 JdbcContext를 생성해서 사용하고, UserDao에서 DataSorce 빈을 주입받아 JdbcContext를 생성할 때 넘겨주면 된다. 이 방법의 장점은 어색하게(UserDao와 강한 결합이므로) JdbcContext를 빈으로 만들지 않아도 DI를 적용할 수 있다는 점이다.
- 둘다 장단점이 있으므로 적절하게 사용한다.
- JdbcContext의 분리
- 템플릿과 콜백
- 전략 패턴과 익명 내부 클래스를 활용한 방식을 스프링에서는 템플릿/콜백 패턴이라고 한다.
- 템플릿/콜백의 동작원리
- 콜백은 보통 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스
- 클라이언트는 콜백을 만들고, 템플릿에 전달하여 메소드 호출 → 템플릿은 작업을 진행하고 콜백함수를 호출하여 추가 작업을 수행 → 결과를 클라이언트에게 전달 2. 편리한 콜백의 재활용
- 익명 내부 클래스를 계속 작성하기가 불편할 수 있으니 콜백을 분리하여 재활용을 해보자.
-
바인딩할 파라미터 없는 쿼리문은 변하는 부분은 쿼리문 밖에 없으니 쿼리문을 제외하고 메소드로 추출
// 변경전 UserDao public void deleteAll() throw ... this.jdbcContext.workWithStatementStrategy( new StatementStrategy() { public .. makePreparedStatement(Connection c) ..{ return c.prepareStatement("delete from users"); } ); }
// 변경후 UserDao public void deleteAll() ...{ execute("delete from users"); } private void execute(final String query) ..{ this.jdbcContext.workWithStatementStrategy( new StatementStrategy() { public .. makePreparedStatement(Connection c) ..{ return c.prepareStatement(query); // 이 부분에만 따로 쿼리 추출 } ); }
-
위처럼 execute를 추출해야 하는게 Dao마다 있어야 하니, 차라리 이 부분을 템플릿으로 옮겨 버리자. 즉 콜백과 템플릿을 결합한다.
public class JdbcContext{ ... // JdbcContext로 이동, 모두가 사용할 수 있도록 public으로 변경 public void execute(final String query) ..{ workWithStatementStrategy( new StatementStrategy() { public .. makePreparedStatement(Connection c) ..{ return c.prepareStatement(query); } ); }
- 템플릿/콜백의 응용
- 제네릭스를 이용한 콜백 인터페이스
- 전략 패턴과 익명 내부 클래스를 활용한 방식을 스프링에서는 템플릿/콜백 패턴이라고 한다.
- 스프링의 JdbcTemplate
-
스프링에서 JDBC를 이용하기 쉽게 만든 템플릿/콜백 방식의 JdbcTemplate
// 위에서 만든 JdbcContext를 JdbcTemplate로 변경 public class UserDao { private DataSource dataSource; private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.dataSource = dataSourece; } ...
- update()
- update() 는 JdbcTemplate에서 제공하는 콜백/템플릿 방식
-
콜백을 넘겨주는 방식은 PreparedStatementCreator를 구현
public void deleteAll(){ this.jdbcTemplate.update(new PreparedStatementCreator(){ public PreparedStatement createPreparedStatement(Connection con) ...{ return con.prepareStatement("delete from users"); }}); }
-
쿼리문만을 넘겨줘도 콜백함수로 변환해서 호출해준다.
// 쿼리문만 전달 public void deleteAll(){ this.jdbcTemplate.update("delete from users"); } // 인자가 필요할 경우, 치환자를 사용하면 된다. public void add(User user){ this.jdbcTemplate.update("insert into users(id, name) values(?, ?)", user.getId(), user.getName()); }
- queryForInt()
-
결과 값을 가져오는 콜백 방식은 ResultSetExtractor을 구현 하면 됨.
public int getCount(){ return this.jdbcTemplate.query( ... // PreparedStatementCreator 콜백 생성 // ResultSetExtractor 콜백 생성 , new ResultSetExtractor<Integer>(){ public Integer extractData(ResultSet rs) ... { rs.next(); return rs.getInt(1); }}); }
-
쿼리문만 넘겨줘도 Integer 타입의 결과를 가져오는 콜백 함수 호출
// 위 콜백방식이 자주 사용되니, Spring에서 제공하는 콜백 함수 public int getConut(){ return this.jdbcTemplate.queryForInt("select count(*) from users"); }
-
- queryForObject()
- 단순한 값이 아닌 오브젝트를 가져 오기 위해서는 queryForObject() 템플릿 메소드를 사용해야 한다.
-
콜백으로는 RowMapper 구현
public User get(String id){ return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[] {id}, new RowMapper<User>(){ public User mapRow(ResultSet rs, int rowNum) ..{ User user = new User(); user.setId(rs.getString("id")); ... return user; }}); }
- query()
-
여러 개의 로우가 결과로 나올 경우 query 템플릿 메소드를 사용한다.
public List<User> getAll(){ return this.jdbcTemplate.query("select * from users order by id", new RowMapper<User>(){ public User mapRow(ResultSet rs, int rowNum) ..{ User user = new User(); user.setId(rs.getString("id")); ... return user; }}); }
-
데이터가 없을 경우, empty List를 리턴한다.
-
- 재사용 가능한 콜백의 분리
- get()과 getAll()에서 RowMapper는 중복되므로 빼내자.
-
RowMapper는 상태정보가 없으므로, 하나만 만들어서 사용
public class UserDao{ private RowMapper<User> userMapper = new RowMapper<User>(){ public User mapRow(ResultSet rs, int rowNum) ..{ User user = new User(); user.setId(rs.getString("id")); ... return user; }};
- UserDao는 이제 완벽하나, 좀 더 개선할 점은 다음과 같다.
- userMapper가 변하지 않으니, UserMapper를 빈으로 만들고 DI를 하면 어떨까? 이렇게 하면 테이블 관련 정보들은 UserMapper에서만 관리하면 되고, UserDao는 변경하지 않아도 됨.
- Dao 메소드에서 사용하는 쿼리문을 다른곳에서 관리하면 어떨까? 이러면 UserDao는 관련 없이, 쿼리문만 독립적으로 수정 가능함.
-