3장 템플릿

  1. 다시 보는 초난감 DAO
    1. 예외처리 기능을 갖춘 DAO
      • db 리소스를 close 해야하니 try/catch/finally 로 해결
  2. 변하는 것과 변하지 않는 것
    1. JDBC try/catch/finally 코드의 문제점
      • 모든 DAO코드에 넣어야 하기 때문에 불편함
      • 리팩토링을 해보자.
    2. 분리와 재사용을 위한 디자인 패턴 적용

       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);
          }
        
        
  3. JDBC 전략 패턴의 최적화
    1. 전략 클래스의 추가 정보
      • 만약 전략에 사용할 메소드에 추가정보가 필요할 경우는 어떻게 해야할까?
      • 즉 아래와 같이 특정 구현체에서 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);
          }
        
    2. 전략과 클라이언트의 동거
      • 사실 전략 패턴도 템플릿 메소드 패턴처럼 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() {
          			...
          		}
          	);
          }
        
  4. 컨텍스트와 DI
    1. 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 동일
          		...	
          	}
        
      • UserDaoJdbcContext를 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() {...}	
          		);
          	}
        
    2. JdbcContext의 특별한 DI
      • JdbcContext를 인터페이스가 아닌 구현체를 사용해서 DI를 하였는데, 이러면 DI의 의미가 있을까? 인터페이스를 구현해도 되지만, 꼭 그럴 필요는 없다. 그렇다면 그 기준은 무엇일까? JdbcContext와 UserDao는 의미상으로 강한 결합을 하고 있으므로, 구현체를 사용해도 괜찮다.
      • 스프링 빈으로 DI : JdbcContext를 빈으로 만든 이유는 무엇일까? 먼저 빈으로 만들면 싱글톤으로 관리가 된다. JdbcContext는 싱글톤으로 적절하므로 빈으로 관리하면 좋다. 또한 JdbcContext는 DataSource라는 빈을 주입받아야 하는데, 주입을 받으려면 자신도 빈이 되어야 한다.
      • 코드를 이용하는 수동 DI : JdbcContext를 빈으로 등록하지 않고 사용할 수도 있다. UserDao에서 JdbcContext를 생성해서 사용하고, UserDao에서 DataSorce 빈을 주입받아 JdbcContext를 생성할 때 넘겨주면 된다. 이 방법의 장점은 어색하게(UserDao와 강한 결합이므로) JdbcContext를 빈으로 만들지 않아도 DI를 적용할 수 있다는 점이다.
      • 둘다 장단점이 있으므로 적절하게 사용한다.
  5. 템플릿과 콜백
    • 전략 패턴과 익명 내부 클래스를 활용한 방식을 스프링에서는 템플릿/콜백 패턴이라고 한다.
      1. 템플릿/콜백의 동작원리
      • 콜백은 보통 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스
      • 클라이언트는 콜백을 만들고, 템플릿에 전달하여 메소드 호출 → 템플릿은 작업을 진행하고 콜백함수를 호출하여 추가 작업을 수행 → 결과를 클라이언트에게 전달 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); 
          				}	
          		);
          }
        
    1. 템플릿/콜백의 응용
      • 제네릭스를 이용한 콜백 인터페이스
  6. 스프링의 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;
        	}
        	...
      
    1. 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());
          }
        
    2. 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");
          }
        
    3. 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;
          		}});
          }
        
    4. 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를 리턴한다.

    5. 재사용 가능한 콜백의 분리
      • 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는 관련 없이, 쿼리문만 독립적으로 수정 가능함.