스프링의 데이터 베이스 연동 - jdbcTemplate

지금까지 DB연동시 JDBC API를 사용해 왔다.
ojdbc6.jar안의 DriverManager.getConnection(url, user, password) 메서드를 사용해 Connection객체를 얻어 DB와 연동하였다.

Connection객체를 이용해 Resultset, PreparedStatement 등의 객체를 생성하고
쿼리를 삽입하고 실행하고 결과값을 반복문을 돌며 집어 넣고

모두 사용후에는 .close()메서드를 통해 DB연결 객체를 모두 해제하는 반복코딩을 많이 사용하였다.

Spring에서 jdbcTemplate를 사용하면 위와같은 귀찮고 중복되는 코드를 없앨 수 있다.

DataSource

jdbcTemplate에 대해 알아보기 전 jdbcTemplate을 사용하려면 꼭 필요한 DataSource를 먼저 알아보자.

데이터 베이스와 연동하는 방법은 여러가지 인데 스프링 프레임워크에선 DataSource방식을 사용하고 있다.

DataSource : 커넥션풀에는 여러개의 Connection 객체가 생성되어 운용되는데, 이를 직접 웹 애플리케이션에서 다루기 힘들기 때문에 DataSource라는 개념을 도입하여 사용, 커넥션 풀의 Connection을 관리하기 위한 객체이다. DataSource 객체를 통해서 필요한 Connection을 획득, 반납 등의 작업을 한다.

스프링 프레임 워크에서 DataSource를 얻는 방법은 아래 3가지.

  1. 커넥션 풀을 이용한 DataSource 설정
  2. JNDI를 이용한 DataSource 설정
  3. DriverManager를 이용한 DataSource 설정(테스트 목적)

우리는 스프링 프레임 워크에서 JDBC를 지원하는 기능을 간단히 살펴볼 것 이기 때문에
3번째 방법인 DriverManager를 사용해서 DriverManagerDataSource를 가져오자.

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property>
  <property name="username" value="scott"></property>
  <property name="password" value="tiger"></property>
  <property name="url" value="jdbc:oracle:thin:@172.17.107.68:1521:xe"></property>
</bean>

dataSource라는 식별자로 DriverManagerDataSource 스프링 빈 객체를 생성!

만들어진 DataSource객체는 스프링에서 JDBC를 위해 지원하는 여러 빈 객체들의 의존관계로 설정된다.

JDBC 템플릿

Connection객체를 구하고 try-catch-finally로 이루어진 중복된 코드를 제거하기 위해 스프링에선 각종 템플릿 클래스를 제공하고 있다.

이중 JDBC에서 사용하는 템플릿 클래스는 아래와 같다.

JdbcTemplate - 기본적인 JDBC 템플릿 클래스, JDBC를 이용해 DB연동하는 메서드를 제공

NamesParameterJdbcTemplate - PreparedStatement에서 인덱스 기반의 파라미터가 아닌 이름 기반의파라미터를 사용할 수 있도록 지원하는 메서드 제공

SimpleJdbcInsert - 데이터 삽입을 위한 인터페이스를 제공하는 클래스

SimpleJdbcCall - 프로시저 호출을 위한 인터페이스를 제공하는 클래스

여기서 가장 많이 쓰이는 건 NamesParameterJdbcTemplate 템플릿 클래스이다.

JdbcTemplate

NamesParameterJdbcTemplate 도 사실 내부적으로는 JdbcTemplate을 사용하기 떄문에 JdbcTemplate먼저 알아보자.

먼저 JdbcTemplate를 일반 객체 혹은 빈 객체를 생성하기 위해선 생성자 혹은 set메서드를 통해 DataSource객체를 전달해야 한다.

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property>
  <property name="url" value="jdbc:oracle:thin:@172.17.107.68:1521:xe"></property>
  <property name="username" value="scott"></property>
  <property name="password" value="tiger"></property>
</bean>		

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
  <constructor-arg ref="dataSource"></constructor-arg>  
</bean>

설정 했으면 DAO객체에 의존설정을 해주자.

@Autowired 어노테이션을 통해 자동 의존 설정 해주던가 xml설정을 통해 빈 객체 생성시 주입시켜주면 된다.

public class SampleDAO {
	
	private JdbcTemplate jdbcTemplate;
  @Autowired
  public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }
  ...
  ...
}

JdbcTemplate - query() - 조회를 위한 메서드

query() 메서드는 다음과 같이 오버로딩 되어있다.

List<T> query(String sql, RowMapper<T> rowMapper)
List<T> query(String sql, RowMapper<T> rowMapper, Object ... args)
List<T> query(String sql, Object[] args, RowMapper<T> rowMapper)
List<T> query(String sql, Object[] args, int[] argsTypes)
List<T> query(String sql, Object[] args, int[] argsTypes, RowMapper<T> rowMapper)

String sql - 당연히 실행할 쿼리문, 위치기반 ?를 사용하는 PreparedStatement용 쿼리를 사용할 수 있다.

RowMapper<T> rowMapper - 조회결과 ResultSet을 읽어와 원하는 객체형식으로 생성해주는 매퍼

Object[] args, int[] argsTypes - args는 ?에 바인딩할 파라미터 값 목록, argsTypes은 파라미터를 바인딩 할 때 사용할 SQL 타입 목록

Object ... args - 배열 형식으로도 바인딩할 파라미터를 지정 가능하지만 가변인자 형식으로도 바인딩 처리 가능하다.

RowMapper는 인터페이스로 ResultSet과 객체를 매핑시킬때 사용하는 mapRow()를 구현해야 한다.

RowMapper를 익명 인터페이스로 구현해도 되지만 여러 메서드에 같은형식의 쿼리가 등장해 같은 객체를 매핑해야 한다면 다음과 같이 RowMapper를 구현한 클래스를 만들어 두자.

반환형이 List<T>인 것 을 알수 있는데 검색된 행을 객체로 매핑해 리스트화 시켜 반환한다.

public class NoticeRowMapper implements RowMapper<Notice> {
  @Override
  public Notice mapRow(ResultSet rs, int rowNum) throws SQLException {
    Notice notice = new Notice();
    notice.setSeq(rs.getInt("seq"));
    notice.setTitle(rs.getString("title"));
    notice.setWriter(rs.getString("writer"));
    notice.setRegdate(rs.getString("regdate"));
    notice.setFilesrc(rs.getString("filesrc"));
    notice.setContent(rs.getString("content"));
    notice.setHit(rs.getInt("hit"));
  }
}
public List<Notice> getNotices(int page, String field, String query) throws ClassNotFoundException, SQLException
{					
  int srow = 1 + (page-1)*15;
  int erow = 15 + (page-1)*15;
  
  StringBuffer sql = new StringBuffer();
  sql.append("SELECT * FROM ");
  sql.append("(SELECT ROWNUM NUM, N.* FROM (SELECT * FROM NOTICES WHERE "+field+" LIKE ? ORDER BY REGDATE DESC) N) ");
  sql.append("WHERE NUM BETWEEN ? AND ? ");
  
  List<Notice> n_list = this.jdbcTemplate.query(
    sql, 
    new Object[] {"%" + query + "%", srow, erow},
    new NoticeRowMapper()
  );
  return n_list;
}

Object[]을 사용하지 않고 인덱스를 통해 바인딩 파라미터를 지정하고 싶다면 PreparedStatementSetter을 사용

List<T> query(String sql, PreparedStatementSetter setter, RowMapper<T> rowMapper)

List<Notice> n_list = this.jdbcTemplate.query(
  sql, 
  new PreparedStatementSetter() {
    @Override
    public void setValues(PreparedStatement ps) throws SQLException {
      ps.setString(1, "%" + query + "%");
      ps.setInt(2, srow);
      ps.setInt(3, erow);
    }
  }
  new NoticeRowMapper()
);

BeanPropertyRowMapper

NoticeRowMapper같은 클래스를 따로 만드는게 번거롭지 않은가?

이런 규칙성 있는 클래스는 스프링 MVC에서 자동으로 만들 수 있다.

RowMapper 인터페이스를 구현한 BeanPropertyRowMapper가 바로 그 클래스이다!

기존에 정의했떤 NoticeRowMapper를 없애버리고 다음과 같이 사용할 수 있다.

List<Notice> n_list = this.jdbcTemplate.query(
  sql, 
  new Object[] {"%" + query + "%", srow, erow},
  new BeanPropertyRowMapper<Notice>(Notice.class)
);

템플릿 클래스를 사용하면 try, catch, finally, close등 귀찮은 코드가 생략됨을 알 수 있다.

JdbcTemplate - queryForList() - 칼럼 개수가 하나라면

SELECT쿼리로 조회한 데이터가 칼럼 하나라면 queryForList()를 사용해 List형태의 값을 반환받을 수 있다.

List<T> queryForList(String sql, Class<T> elementType)
List<T> queryForList(String sql, Object[] args, Class<T> elementType)
List<T> queryForList(String sql, Object[] args, int[] argsTypes, Class<T> elementType)

Class<T> elementType을 제외하고 형식은 query()메서드와 똑같다.

정수형, 문자열에 따라 Integer.class 혹은 String.calss등의 값이 매개변수로 전달될 수 있다.

JdbcTemplate - queryForObject() - 행의 개수가 하나라면

SELECT쿼리로 조회한 데이터가 한 행 이라면 queryForObject()를 사용할 수 있다.

만약 쿼리 실행결과의 행 개수가 0개 혹은 2개 이상이라면 예외 발생.

T queryForObject(String sql, RowMapper<T> rowMapper)
T queryForObject(String sql, Object[] args, RowMapper<T> rowMapper)
T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper)
T queryForObject(String sql, Class<T> requiredType)
T queryForObject(String sql, Object[] args, Class<T> requiredType)
T queryForObject(String sql, Object[] args, int[] argTypes, Class<T> requiredType)
T queryForObject(String sql, Object[] args, int[] argTypes, Object ... args)

query() 메서드와 비교해서 다른 매개변수가 딱 2개 있다.

Class<T> requiredType, Object ... args

Class<T> requiredType는 만약 행 뿐만 아니라 칼럼도 하나일 경우
그 하나의 칼럼 타입을 지정할 때 쓰인다. (queryForList()Class<T> elementType과 같음)

public int counts() {
  String sql = "SELECT COUNT(*) FROM notice";
  return jdbcTemplate.queryForObject(
    sql,
    Integer.class
  );
}

JdbcTemplate - update() - insert, update. delete를 위한 메서드

INSERT, UPDATE, DELETE 쿼리를 실행할 때 사용하는 메서드 ,

int update(String sql)
int update(String sql, Objet... args)
int update(String sql, Object[] args, int[] argTypes)
int update(String sql, PreparedStatementSetter pss)
int update(PreparedStatementCreator psc)

query() 메서드와 다를 것 이 없다.
똑같은 방법으로 바인딩 처리하고 리턴값으로 sql이 수행된 행의 개수를 반환한다.

NamedParameterJdbcTemplate - 이름 기반 파라미터를 사용하는 jdbcTemplate (권장)

NamedParameterJdbcTemplate는 스프링에서 JDBC 템플릿 클래스중 가장 많이 사용되는 템플릿 클래스로

빈 객체로 생성하기 위해 JdbcTemplate 템플릿 클래스와 마찬가지로 DataSource 객체를 필요로 한다.

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property>
  <property name="url" value="jdbc:oracle:thin:@172.17.107.68:1521:xe"></property>
  <property name="username" value="scott"></property>
  <property name="password" value="tiger"></property>
</bean>		

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
  <constructor-arg ref="dataSource"></constructor-arg>  
</bean>

NamedParameterJdbcTemplate역시 사용하려면 @Autowired 어노테이션을 설정하거나 <bean>태그를 통해 DI 주입…

public class SampleDAO {
	
	private NamedParameterJdbcTemplate jdbcTemplate;
  @Autowired
  public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }
  ...
  ...
}

지금까지 PreparedStatement의 sql 문자열의 ?위치에 파라미터를 바인딩 하기 위해 인덱스를 사용하거나 Object[]에 집어넣을 값을 순서대로 넣어 바인딩 처리 하였는데

NamedParameterJdbcTemplate을 사용하면 이름기반으로 바인딩 가능하다.

NamedParameterJdbcTemplate 템플릿 클래스가 제공하는 메서드들을 알아보자.
NamedParameterJdbcTemplate에서도 JdbcTemplate에서 제공하는 query(), queryForList(), queryForObject(), update() 등의 메서드 역시 제공된다.

Map을 이용한 파라미터 값 설정 메서드

List<T> query(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper)
List<T> queryForList(String sql, Map<String, ?> paramMap, Class<T> elementType)
T queryForObject(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper)
T queryForObject(String sql, Map<String, ?> paramMap, Class<T> elementType)
int update(String sql, Map<String, ?> paramMap)
int update(String sql, Map<String, ?> paramMap, keyHolder keyHolder)

중간의 Map<String, ?> paramMap 매개변수를 제외하곤 JdbcTemplate의 메서드들과 차이점이 없어보인다.

Map<String, ?> paramMap매개변수를 통해 PreparedStatement의 쿼리문에 바인딩 시키는데 사용법은 아래와 같다.

public List<Notice> getNotices(int page, String field, String query) throws ClassNotFoundException, SQLException
{					
  int srow = 1 + (page-1)*15;
  int erow = 15 + (page-1)*15;
  
  StringBuffer sql = new StringBuffer();
  sql.append("SELECT * FROM ");
  sql.append("(SELECT ROWNUM NUM, N.* FROM (SELECT * FROM NOTICES WHERE "+field+" LIKE :query ORDER BY REGDATE DESC) N) ");
  sql.append("WHERE NUM BETWEEN :srow AND :erow ");

  Map<String, Object> params = new HashMap<>();
  params.put("query", query);
  params.put("srow", srow);
  params.put("erow", erow);

  List<Notice> n_list = this.jdbcTemplate.query(
    sql, 
    params,
    new BeanPropertyRowMapper<Notice>(Notice.class)
  );
  return n_list;
}

sql쿼리문을 보면 ? 대신 :바인딩변수명 이 들어가있다.

그리고 query()메서드의 2번째 매개변수로 바인딩변수명과 같은 이름의 key값으로 설정한 value가 들어있는 Map객체를 전달.

이름을 식별자로 사용해 바인딩 하기 때문에 좀더 직관적인 코딩이 가능하다.

SqlParameterSource를 이용한 파라미터 값 설정 메서드

바인딩 처리를 위해 NamedParameterJdbcTemplate에서 Map객체 대신 SqlParameterSource를 사용 가능하다.

List<T> query(String sql, SqlParameterSource paramSource, RowMapper<T> rowMapper)
List<T> queryForList(String sql, SqlParameterSource paramSource, Class<T> elementType)
T queryForObject(String sql, SqlParameterSource paramSource, RowMapper<T> rowMapper)
T queryForObject(String sql, SqlParameterSource paramSource, Class<T> elementType)
int update(String sql, SqlParameterSource paramSource)
int update(String sql, SqlParameterSource paramSource, keyHolder keyHolder)

스프링MVC에선 SqlParameterSource는 인터페이스를 구현한 다음 두개의 클래스를 제공한다.

  1. MapSqlParameterSource
  2. BeanPropertySqlParameterSource

SqlParameterSource에 어떤 기능이 있는지 위의 두 클래스를 알아보자.

JdbcTemplate에서 Map콜렉션 클래스를 사용해 바인딩 처리를 하였는데
NamedParameterJdbcTemplateMapSqlParameterSource매개변수도 똑같은 역할을 한다.

MapSqlParameterSource 역시 <이름, 값> 쌍을 이용해 파라미터의 값을 설정한다.

public List<Notice> getNotices(int page, String field, String query) throws ClassNotFoundException, SQLException
{					
  int srow = 1 + (page-1)*15;
  int erow = 15 + (page-1)*15;
  
  StringBuffer sql = new StringBuffer();
  sql.append("SELECT * FROM ");
  sql.append("(SELECT ROWNUM NUM, N.* FROM (SELECT * FROM NOTICES WHERE "+field+" LIKE :query ORDER BY REGDATE DESC) N) ");
  sql.append("WHERE NUM BETWEEN :srow AND :erow ");

  MapSqlParameterSource<String, Object> paramSource = new MapSqlParameterSource<>();
  params.addValue("query", query);
  params.addValue("srow", srow);
  params.addValue("erow", erow);

  List<Notice> n_list = this.jdbcTemplate.query(
    sql, 
    paramSource,
    new BeanPropertyRowMapper<Notice>(Notice.class)
  );
  return n_list;
}



BeanPropertySqlParameterSource의 경우 바인딩 처리를 객체를 통해 처리한다.

스프링 MVC에서 커맨드 객체를 통해 파라미터를 자동으로 객체로 만들어저 매개변수로 받는다.

<form action="">
	이메일: <input type="text" name="email"/>><br>
	이름: <input type="text" name="name"/><br>
	암호: <input type="text" name="pw1"/><br>
	암호확인: <input type="text" name="pw2"/><br>
</form>
public class MemberRequest {
	private String email;
	private String name;
	private String pw1;
	private String pw2;

	//get, set 메서드....
}
@RequestMapping("member_join.htm")
public String member_join(MemberRequest member) throws ClassNotFoundException, SQLException {		 
	...
	...
	return "index.jsp";
}

이 정보를 DB에 INSERT 할때 일일이 객체안의 필드를 바인딩 시켜주어야 하는데 BeanPropertySqlParameterSource 객체를 사용하면 그럴필요 없다.

객체의 필드명과 DB 테이블 칼럼명이 같으면 자동으로 바인딩 시킨다.

@RequestMapping("member_join.htm")
public String member_join(MemberRequest member) throws ClassNotFoundException, SQLException {
  StringBuffer sql = new StringBuffer();
  sql.append(" INSERT INTO member (email, name, pw1, pw2) VALUES ");
  sql.append(" (:email, :name, :pw1, :pw2) ");
  SqlParameterSource paramSource = new BeanPropertySqlParameterSource(member);
  jdbcTemplate.update(
    sql,
    paramSource
  );
  ...
  return "index.jsp";
}

BeanPropertySqlParameterSource의 생성자로 member객체를 넘겨주면 끝.
INSERT의 경우 매우 간단하게 처리할 수 있다.

NamedParameterJdbcTemplate에서 JdbcTemplate 메서드 사용하기

오히려 Map클래스나 SqlParameterSource를 구현한 클래스를 생성해서 사용하는게 번거로울 수 있다. (간단한 작업의 경우)

그럴 때 JdbcTemplate의 메서드를 사용할 수 있도록
NamedParameterJdbcTemplategetJdbcTemplate()메서드를 제공한다.

getJdbcTemplate()가 반환한 JdbcTemplate을 사용하면 된다.