템플릿과 콜백의 기본적인 원리와 동작방식, 만드는 방법을 알아보았으니, 지금 부터는 Spring에서 제공하는 템플릿/콜백 기술을 살펴보자. 스프링은 JDBC를 이용하는 DAO에서 사용할 수는 다양한 템플릿과 콜백을 제공한다. 

스프링이 제공하는 JDBC코드용 기본 템플릿은 JdbcTemplate이다.  앞에서 만든 JdbcContext와 비슷하지만 더 강력한 기능을 제공한다.   아래는 기존에 작성 JdbcContext를 이용하여 작성했던 코드이다. 지금 부터는 아래 코드를 JdbcTemplate을 이용하는 코드로 변경 해보자. 

public class UserDao{
	private DataSource dataSource;
	JdbcContext jdbcContext;
	
	public void setDataSource(DataSource dataSource) {		
		this.jdbcContext = new JdbcContext();	 
		this.jdbcContext.setDataSource(dataSource);
		this.dataSource = dataSource;
	}
	
	//반복되는 부분 분리 
	public void executeSQL(final String strSQL) throws SQLException{
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makeStatement(Connection c) throws SQLException {
					PreparedStatement ps=c.prepareStatement(strSQL);
					return ps;
				}
			}				
		);	
	}
	public void deleteAll() throws SQLException {		
		executeSQL("delete from users");
	}	
	public void add(final User user) throws SQLException {	
		this.jdbcContext.workWithStatementStrategy(
			 new StatementStrategy() {
				 public PreparedStatement makeStatement(Connection c) throws SQLException {
						PreparedStatement ps = c.prepareStatement(
								"insert into users(id, name, password) values(?,?,?)");
							ps.setString(1, user.getId());
							ps.setString(2, user.getName());
							ps.setString(3, user.getPassword());
						return ps;
					}
			 }				
		);
	}
	//아직 템플릿 / 콜백 방식을 적용하지 않은 함수들
	/*public User get(String id) throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		ResultSet rs =null;
		User user = null;
		
		try {		
			c = this.dataSource.getConnection();
			ps = c.prepareStatement("select * from users where id = ?");
			ps.setString(1, id);			
			rs = ps.executeQuery();			
			if (rs.next()) {
				user = new User();
				user.setId(rs.getString("id"));
				user.setName(rs.getString("name"));
				user.setPassword(rs.getString("password"));
			}
		}catch(SQLException e) {
			throw e; 
		}finally{
			if(rs != null) {
				try {
					// try/catch문 사용하지 않았을때 rs.close()시 exception발생시 그다음 코드는 실행이 안된다.
					// ps,c가 close가 안되는 문제가 발생하게 됨.
					rs.close();  
				}catch(SQLException e) {
					
				}
			}
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
						}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
		if (user == null) throw new EmptyResultDataAccessException(1);
		return user;
	}

	

	public int getCount() throws SQLException  {
		Connection c = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		int count =0;
		try {
			c= dataSource.getConnection();	
			ps=c.prepareStatement("select count(*) from users");
			rs=ps.executeQuery();
			rs.next();
			count = rs.getInt(1);
		}catch(SQLException e) {
			throw e;
		}finally {
			if(rs != null) {
				try {
					rs.close();
				}catch(SQLException e) {
					
				}
			}
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
		return count;
	}*/
}

1. JdbcContext를 JdbcTemplate으로 변경했다.

private DataSource dataSource;
JdbcTemplate jdbcContext;

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

 2. 이제 deleteAll() 함수를 변경해보자. 

public void deleteAll() throws SQLException {		
	this.jdbcTemplate.update("delete from users");
}	

3. add()함수를 변경해보자

public void add(final User user) throws SQLException {	
	this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
			user.getId(),user.getName(),user.getPassword());
}

4. 아직 템플릿/콜백 방식을 적용하지 않았던 메소드에 JdbcTemplate을 적용해보자. 

 getCount( )  함수 수정

기존에 작성된 getCount( ) 함수는 JdbcTemplate의 query( ) 함수와 PreparedStatementCreator콜백과          resultSetExtractor 콜백을 사용하여 수정 할수 있다.  

  • PreparedStatementCreator callback 함수 - statement생성
  • ResultSetExtractor callback 함수 - 쿼리실행후 얻은 Resultset에서 원하는 값 추출 
public int getCount() throws SQLException  {
		return this.jdbcTemplate.query(new PreparedStatementCreator() {			
			@Override
			public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
				// TODO Auto-generated method stub
				return con.prepareStatement("select count(*) from users");
			}
		}, new ResultSetExtractor<Integer>() {
			public Integer extractData(ResultSet rs) throws SQLException,DataAccessException {
				rs.next();
				return rs.getInt(1);
			}
		});
	}

"select count(*) from users" 처럼 SQL의 실행 결과가 하나의 정수 값이 되는 경우 이용할 수 있는 함수를 JdbcTemplate에서 제공하고 있다. 바로 queryForInt() 함수 이다. 이 함수를 이용하여 위 코드를 다시 수정해보자.  코드가 훨씬 간결해졌다. 

public int getCount() throws SQLException  {
		return this.jdbcTemplate.queryForInt("select count(*) from users");
}
get( ) 메소드 수정 
  • queryForObject( ) 함수 사용
  • RowMapper callback 사용 

위에 두 함수를 사용하여 수정하였다.

	public User get(String id) throws SQLException {		
		return this.jdbcTemplate.queryForObject("select * from users where id=? ", 
				new Object[] {id}, //-> SQL에 바인딩할 파라미터값, 가변인자 대신 배열을 사용 
				new RowMapper<User>() {					
					@Override
					public User mapRow(ResultSet rs, int rowNum)throws SQLException {
						// TODO Auto-generated method stub
						User 	user = new User();
						user.setId(rs.getString("id"));
						user.setName(rs.getString("name"));
						user.setPassword(rs.getString("password"));
						return user;
					}
				});
	}

 

5. 모든 사용자 정보를 가져오는 getAll() 함수를 추가해 보자. 

public List<User> getAll() throws SQLException{
	return this.jdbcTemplate.query("select * from users",
			new RowMapper<User>() {					
				@Override
				public User mapRow(ResultSet rs, int rowNum)throws SQLException {
					// TODO Auto-generated method stub
					User 	user = new User();
					user.setId(rs.getString("id"));
					user.setName(rs.getString("name"));
					user.setPassword(rs.getString("password"));
					return user;
				}
			});
}

위 코드는 JdbcTemplate의 query()  템플릿를 사용했다. 해당 query() 템플릿의 수행 Logic은 아래에 설명해 두었으니, 참고하기 바란다. 

query () 템플릿 parameter

  • 첫번째 : SQL문 
  • 두번째 : 바인딩할 파라미터가 있는 경우 넣고 없으면 생각가능
  • 세번째 : RowMapper콜백이다.

query() 템플릿 수행 순서

 

  1. query() 템플릿은 SQL을 실행해서 얻은 Resultset의 모든 Row을 열어 RowMapper 콜백을 호출한다. 즉, SQL 쿼리 실행 결과 Row 수만큼 호출된다. 
  2. 호출된 RowMapper콜백은 해당 Row를 User로 Mapping해서 돌려주게 된다.
  3.  이렇게 만들어진 User 오브젝트는 템플릿에서 미리 준비한 List<User> 컬렉션에 추가된다.
  4. 그리고 모든작업이 완료되면 템플릿은 List<User>을 리턴힌다. 

6. 재사용 가능한 콜백 분리 

DataSourc인스턴스 변수는 더이상 사용 하지 않으니, 제거하자.

하기 코드에서 보면 알수있듯이 get() , getAll() 함수에서 RowMapper가 중복됨을 확인했다. 중복되는 코드를 다시 분리시켜 보자.

중복되는 익명 내부 클래스를 인스턴스 변수 하나에 받아서 사용하도록 수정했다. 드디어 코드가 완성되었다.!!!!

public class UserDao{
	JdbcTemplate jdbcTemplate;
	private RowMapper<User> userMapper = new RowMapper<User>() {					
		@Override
		public User mapRow(ResultSet rs, int rowNum)throws SQLException {
			// TODO Auto-generated method stub
			User 	user = new User();
			user.setId(rs.getString("id"));
			user.setName(rs.getString("name"));
			user.setPassword(rs.getString("password"));
			return user;
		}
	};
	
	public void setDataSource(DataSource dataSource) {		
		this.jdbcTemplate = new JdbcTemplate();	 
		this.jdbcTemplate.setDataSource(dataSource);
	}
	public void deleteAll() {		
		this.jdbcTemplate.update("delete from users");
	}	
	public void add(final User user) {	
		this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
				user.getId(),user.getName(),user.getPassword());
	}

	public User get(String id) {		
		//가변 인자 사용하고 싶을때 
		return this.jdbcTemplate.queryForObject("select * from users where id=? ", this.userMapper,id);
	}
	public int getCount()  {
		return this.jdbcTemplate.queryForInt("select count(*) from users");
	}
	public List<User> getAll() {
		return this.jdbcTemplate.query("select * from users",this.userMapper);
	}
}

'Spring' 카테고리의 다른 글

4장 예외  (0) 2020.01.05
3장 템플릿IV - 템플릿/콜백의 응용  (0) 2020.01.04
3장 템플릿 II- 템플릿/ 콜백패턴  (0) 2020.01.04
3장 템플릿I - 전략패턴  (0) 2020.01.04
2장 테스트  (0) 2019.12.28

앞에 글에서 전략 패턴으로 UserDao를 변경해 봤다.  전략패턴의 기본구조에 익명 내부 클래스를 활용한 방식이었다. 이런방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다. 전략 패턴의 컨텍스트를 템틀릿, 전략을 콜백이라고 부른다. 템를릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙여진 이름이다. 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트이다. 

템플릿
템플릿(template)은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 학생들이 도형을 그릴때 사용하는 도형자 또는 모양자가 바로 템플릿이다. 프로그래밍에서는 고정된 틀 안에 바꿀수 있는 부분을 넣어서 사용하는 경우에 템플릿이라고 부른다. JSP는 HTML이라는 고정된 부분에 EL과 스크립릿이라는 변하는 부분을 넣은 일종의 템플릿 파일이다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분을 서브클래스의 메소드에 두는 구조로 이루어진다. 

콜백
콜백(callback)은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행 시키키 위해 사용한다. 자바에선 메소드 자체를 파라미터로 전달할 방법은 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 Functional Object라고 한다. 

템플릿/콜백의 동작원리

1.템플릿/콜백 특징


  • 단일 메소드 인터페이스를 사용
  • 콜백은 하나의 메소드를 가진 인퍼페이스를 구현한 익명 내부 클래스 만들어짐
  • 콜백 인터페이스의 메소드는 보통 파라미터가 있음

여러개의 메소드를 가진 인터페이스를 사용 사용할 수 있는 전랙패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한번 호출되는 경우가 일반적이기 때문이다.콜백은 일반적으로 하나의 메소드를 가진 인퍼페이스를 구현한 익명 내부 클래스로 만들어진다. 콜백 인터페이스의 메소드는 보통 파라미터가 있다. 템플릿의 작업 흐름중에 만들어지는 컨테스트 정보를 전달 받을때 사용된다. ( UserDao를 전략 패턴으로 변경 했을때 전략클래스의 메소드를 작성시  Context로 부터 connection을 파라미터로 전달 받았었다. )템플릿/콜백 방식은 전략 패턴과 DI장점을 익명 내부 클래스 사용 전략과 결합한 독특한 확용법이다. 

아래 그림은 템플릿/콜백의 작업흐름을 보여준다.  

2. 콜백의 분리와 재활용 

이번에는 복잡한 익명 내부클래스 사용을 최소화 할 수 있는 방법을 찾아보자. 

위 그림에서 처럼 변하는 부분은 변하지 않는 부분에서 분리 시켜 보자. 아래 반영한 코드를 작성해 두었다. 

//반복되는 부분 분리 
public void executeSQL(final String strSQL) throws SQLException{
	this.jdbcContext.workWithStatementStrategy(
		new StatementStrategy() {
			public PreparedStatement makeStatement(Connection c) throws SQLException {
				PreparedStatement ps=c.prepareStatement(strSQL);
				return ps;
			}
		}				
	);	
}
public void deleteAll() throws SQLException {		
	executeSQL("delete from users");
}	

그런데 executeSQL() 메소드는 다른 DAO에서도 사용할수 있으므로 템플릿 클래스 안으로 옮겨보자. 

package springbook.user.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import javax.sql.DataSource;

public class JdbcContext {
	private DataSource dataSource;
	
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void workWithStatementStrategy(StatementStrategy strategy) throws SQLException{
		Connection c = null;
		PreparedStatement ps = null;
		try {
			c = dataSource.getConnection();	
			ps=strategy.makeStatement(c); // <-- 이부분이 바뀌는 부분이다.. "전략" 적용하는 부분 
			ps.executeUpdate();
			
		}catch(SQLException e) {
			throw e;
		}finally {
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
	}	

	//반복되는 부분 분리 
	public void executeSQL(final String strSQL) throws SQLException{
		workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makeStatement(Connection c) throws SQLException {
					PreparedStatement ps=c.prepareStatement(strSQL);
					return ps;
				}
			}				
		);	
	}
}

 deleteAll()함수도 수정했다.

public void deleteAll() throws SQLException {		
		this.jdbcContext.executeSQL("delete from users");
}

콜백 재활용을 적용한 JdbcContext를 아래 그림으로 나타낼수 있다. 

이번에는 좀더 복잡한 add()메소드에도 적용해보자. add()메소드에는 PreparedStatement에 바인딩될 파라미터 내용이 추가 되어야 한다. 그래서 자바5에서 부터 제공하는 가변인자를 활용했다. 

public void add(final User user) throws SQLException {	
	this.jdbcContext.executeSQL("insert into users(id, name, password) values(?,?,?)",user.getId(),user.getName(),user.getName());
}

아래는 JdbcContext의 executeSQL에 가변인자를 추가한 코드다. 

public void executeSQL(final String strSQL, final String ...args) throws SQLException{		
	workWithStatementStrategy(
		new StatementStrategy() {
			public PreparedStatement makeStatement(Connection c) throws SQLException {
				int i=1;
				PreparedStatement ps=c.prepareStatement(strSQL);
				for(String a: args) {
					ps.setString(i++, a);
				}
				return ps;
			}
		}				
	);	
}

+ Recent posts