Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 13장 JDBC를 사용한 데이터 접근 #1

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.




13. JDBC를 사용한 데이터 접근

13.1 스프링 JDBC 소개
스프링 프레임워크의 JDBC 추상화가 제공하는 가치는 다음 표에 나온 나와있는 일련의 동작에 가장 잘 나와있다고 할 수 있다. 다음 표는 스프링이 어떤 동작을 처리하고 어플리케이션은 어떤 동작에 책임을 지는지 보여준다.

Table 13.1. 스프링 JDBC - 누가 무엇을 하는가?
동작 스프링 어플리케이션 개발자
연결 파라미터 정의.   X
연결 오픈. X  
SQL 문 지정.   X
파라미터 선언과 파라미터 값 제공   X
스테이트먼트(statement) 준비와 실행. X  
(존재한다면)결과를 반복하는 루프 설정 X  
각 이터레이션에 대한 작업 수행.   X
모든 예외 처리. X  
트랜잭션 제어. X  
연결, 스테이트먼트(statement), 리절트셋(resultset) 닫기. X  
















개발하기 지루한 JDBC의 API의 모든 저수준 세부사항을 스프링 프레임워크가 처리한다.


13.1.1 JDBC 데이터베이스 접근에 대한 접근법(approach) 선택하기
JDBC 데이터베이스 접근의 기반을 이루는 여러가지 접근방법 중에서 선택할 수 있다. JdbcTemplate의 세가지 좋은 점에 추가적으로 새로운 SimpleJdbcInsert와 SimplejdbcCall 접근은 데이터베이스 메타데이터를 최적화하고 RDBMS 객체 방식은 JDO 쿼리 디자인과 유사하게 더 객체지향적인 접근방법을 취한다. 이러한 접근방법 중 하나를 사용해서 시작하고 나서도 다른 접근방법의 기능을 포함시키기 위해서 섞을 수 있다. 모든 접근방법은 JDBC 2.0 호환 드라이버가 필요하고 몇가지 고급 기능은 JDBC 3.0 드라이버를 필요로 한다.

Note
스프링 3.0은 제너릭(generic)과 가변인자(varargs)같은 Java 5의 지원으로 다음의 모든 접근을 업데이트했다.

  • JdbcTemplate은 전형적인 스프링 JDBC 접근방법이고 가장 인기가 좋다. 이 "최저수준"의 접근방법과 다른 접근방법들은 보이지 않게 JdbcTemplate를 사용하고 모든 접근방법은 제너릭과 가변인자같은 Java 5의 지원으로 업데이트되었다.
  • NamedParameterJdbcTemplate는 전통적인 JDBC의 "?" 플레이스홀더 대신에 이름있는(named) 파라미터를 제공하기 위해서 JdbcTemplate를 감싼다. 이 접근방법은 더 좋은 문서화를 제공하고 SQL 문에 다중 파라미터가 있을 때 사용하기가 쉽다.
  • SimpleJdbcTemplate에는 JdbcTemplate와 NamedParameterJdbcTemplate에서 가장 빈번하게 사용하는 작업을 합쳐놨다.
  • SimpleJdbcInsert와 SimpleJdbcCall는 필요한 설정의 양을 제한하려고 데이터베이스의 메타데이터를 최적화한다. 이 접근방법은 코딩을 간소화해서 테이블이나 프로시저의 이름을 제공하고 컬럼명과 일치하는 파라미터들의 맵을 제공하기만 하면 된다. 이는 데이터베이스가 충분한 메타데이터를 제공할 때만 동작한다. 데이터베이스가 이러한 메타데이터를 제공하지 않는다면 파라미터의 명시적인 설정을 제공해야 할 것이다.
  • MappingSqlQuery, SqlUpdate, StoredProcedure를 포함하는 RDBMS 객체들은 데이터 접근 계층을 초기화하는 동안 재사용가능하고 쓰레드세이프한 객체의 생성을 필요로 한다. 이 접근방법은 쿼리스프링을 정의하고, 파라미터를 선언하고 쿼리를 컴파일하는 JDO Query 이후에 만들어진다. 일단 만들고 나면 실행 메서드는 전달하는 다양한 파라미터 값들로 여러번 호출할 수 있다.


13.1.2 패키지 계층
스프링 프레임워크의 JDBC 추상화 프레임워크는 네가지 다른 패키지로 이뤄져 있다. 즉, core, datasource, object, support 이다.

org.springframework.jdbc.core 패키지는 JdbcTemplate 클래스와 JdbcTemplate의 다양한 콜백 인터페이스를 포함하고 있고 추가로 여러가지 관련 클래스를 포함하고 있다. 하위 패키지인 org.springframework.jdbc.core.simple는 SimpleJdbcTemplate 클래스, 관련된 SimpleJdbcInsert, SimpleJdbcCall 클래스를 포함하고 있다. 또다른 하위 패키지인 org.springframework.jdbc.core.namedparam는 NamedParameterJdbcTemplate 클래스, 관련된 지원 클래스들을 포함하고 있다. Section 13.2, “Using the JDBC core classes to control basic JDBC processing and error handling”, Section 13.4, “JDBC 배치 작업”, Section 13.5, “SimpleJdbc 클래스로 JDBC 작업 간소화하기”를 참고해라.

org.springframework.jdbc.datasource 패키지는 DataSource 접근을 쉽게 하는 유틸리티 클래스와 Java EE 컨테이너 외부의 수정되지 않고 운영되는 JDBC 코드와 테스트에 사용할 수 있는 여러가지 간단한 DataSource 구현체를 포함하고 있다. org.springfamework.jdbc.datasource.embedded 하위패키지는 HSQL나 H2같은 자바 데이터베이스 엔진을 사용해서 인-메모리 데이터베이스 인스턴스의 생성을 지원한다. Section 13.3, “데이터베이스 연결 제어”와 Section 13.8, “내장 데이터베이스 지원”를 참고해라.

org.springframework.jdbc.object 패키지는 RDBMS의 조회, 갱신, 저장프로시저를 스레드세이프하고 재사용가능한 객체로 나타내는 클래스들을 포함하고 있다. Section 13.6, “자바객체처럼 JDBC 작업 모델링하기”를 참고해라. 이 접근은 데이터베이스의 “연결이 끊긴” 쿼리가 반환하는 객체조차도 JDO로 만든다. 이 더 높은 수준의 JDBC 추상화는 org.springframework.jdbc.core 패키지의 저수준 추상화에 의존한다.

org.springframework.jdbc.support 패키지는 SQLException 변환 기능과 약간의 유틸리티 클래스를 제공한다. JDBC 처리과정중에 던져진 예외는 org.springframework.dao 패키지에 정의된 예외로 변환된다. 즉 스프링 JDBC 추상화 계층을 사용하는 코드는 JDBC나 RDBMS에 특화된 오류처리를 구현할 필요가 없다. 변환된 모든 예외는 언체크드이고 이 예외는 복구할 수 있는 예외를 잡거나 호출자에게 전파할 수 있다. Section 13.2.4, “SQLExceptionTranslator”를 참고해라.


13.2 기본적인 JDBC 처리와 오류 처리에 JDBC 핵심 클래스 사용하기

13.2.1 JdbcTemplate
JdbcTemplate 클래스는 JDBC core 패키지에서 가장 중요한 클래스이다. 이 클래스는 리소스의 생성과 해지를 처리해서 연결을 닫는 걸 까먹는 등의 일반적인 오류를 피하도록 도와준다. 이 클래스는 스테이트먼트(statement)의 생성과 실행같은 JDBC 핵심 작업흐름의 기본작언 작업을 수행해서 어플리케이션 코드가 SLQ을 제공하고 결과를 받도록 한다. JdbcTemplate 클래스는 SQL 조회, 업데이트문, 저장 프로시저 호출, ResultSet의 반복 수행, 반환된 파라미터 값의 추출등을 실행한다. JdbcTemplate 클래스는 JDBC 예외를 잡아서 org.springframework.dao 패키지에 정의되어 있는 일반적이고 더 많은 정보를 가진 예외계층으로 변환한다.

코드에서 JdbcTemplate를 사용할 때 필요한 작업은 명확하게 정의된 계약에 따라 콜백 인터페이스를 구현하는 것 뿐이다. PreparedStatementCreator 콜백 인터페이스는 JdbcTemplate 클래스가 제공하는 Connection에 SQL과 필요한 파라미터를 제공해서 미리 준비된 스테이트먼트를 생성한다. 호출가능한(callable) 스테이트먼트를 생성하는 CallableStatementCreator 인터페이스도 마찬가지다. RowCallbackHandler 인터페이스는 ResultSet의 각 열에서 값을 추출한다.

DataSource 참조로 직접 인스턴스화 하거나 스프링 IoC 컨테이너에서 설정하고 DAO를 빈 참조로 제공해서 DAO 구현체내에서 JdbcTemplate를 사용할 수 있다.

Note
DataSource는 언제나 스프링 IoC 컨테이너의 빈으로 설정해야 한다. 첫번째 경우에 서비스에 빈을 직접 제공한다. 두번째 경우에는 미리 준비된 템플릿에 빈을 제공한다.

이 클래스에서 생긴 모든 SQL은 템플릿 인스턴스의 정규화된 클래스명에 대응되는 범주에서 DEBUG 레벨로 로깅된다. (일반적으로는 JdbcTemplate이지만 JdbcTemplate 클래스의 커스텀 하위클래스를 사용한다면 달라질 수 있다.)


13.2.1.1 JdbcTemplate 클래스의 사용방법 예제
이번 섹션에서는 JdbcTemplate 클래스 사용방법의 몇가지 예제를 보여준다. 이러한 예제들은 JdbcTemplate의 모든 기능 목록을 모두 다루지는 않는다. JdbcTemplate의 Javadoc을 참고해라.

조회 (SELECT)
다음은 열의 수를 얻어내는 간단한 쿼리가 있다.

int rowCount = this.jdbcTemplate.queryForInt("select count(*) from t_actor");

다음은 변수 바인딩을 사용한은 간단한 쿼리이다.

int countOfActorsNamedJoe = this.jdbcTemplate.queryForInt("select count(*) from t_actor where first_name = ?", "Joe");

다음은 String으로 조회하는 쿼리이다.

String lastName = this.jdbcTemplate.queryForObject("select last_name from t_actor where id = ?", new Object[]{1212L}, String.class);

다음은 조회해서 하나의 도메인 객체로 받는 예제이다.

Actor actor = this.jdbcTemplate.queryForObject(
  "select first_name, last_name from t_actor where id = ?",
  new Object[]{1212L},
  new RowMapper<Actor>() {
    public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
      Actor actor = new Actor();
      actor.setFirstName(rs.getString("first_name"));
      actor.setLastName(rs.getString("last_name"));
      return actor;
    }
  });

다음은 조회해서 다수의 도메인 객체로 받는 예제이다.

List<Actor> actors = this.jdbcTemplate.query(
  "select first_name, last_name from t_actor",
  new RowMapper<Actor>() {
    public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
      Actor actor = new Actor();
      actor.setFirstName(rs.getString("first_name"));
      actor.setLastName(rs.getString("last_name"));
      return actor;
    }
  });

마지막 두 예제코드가 같은 어플리케이션에 존재한다면 두 RowMapper 익명 내부 클래스의 중복을 제거하고 필요에 따라 DAO 메서드로 참조할 수 있는 하나의 클래스(보통 static 내부 클래스)로 추출하는 것이 더 바람직하다. 예를 들어 다음과 같이 마지막 예제코드를 작성하는 것이 더 나을 것이다.

public List<Actor> findAllActors() {
  return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}

private static final class ActorMapper implements RowMapper<Actor> {

  public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
    Actor actor = new Actor();
    actor.setFirstName(rs.getString("first_name"));
    actor.setLastName(rs.getString("last_name"));
    return actor;
  }
}


jdbcTemplate로 수정하기 (INSERT/UPDATE/DELETE)
추가(insert), 갱신(update), 삭제(delete) 작업을 수행하는데 update(..) 메서드를 사용한다. 보통 파라미터 값들은 가변인자(var args)나 객체의 배열로 제공한다.

this.jdbcTemplate.update("insert into t_actor (first_name, last_name) values (?, ?)",  "Leonor", "Watling");


this.jdbcTemplate.update("update t_actor set = ? where id = ?",  "Banjo", 5276L);


this.jdbcTemplate.update("delete from actor where id = ?", Long.valueOf(actorId));


다른 jdbcTemplate 작업들
임의의 SQL을 실행하는데 execute(..) 메서드를 사용할 수 있고 이 메서드는 DDL문에 사용하기도 한다. execute(..)는 콜백 인터페이스나 변수 배열의 바인딩 등을 받아들이게 되면 상당히 큰 부하가 생긴다.

this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

다음 예제는 간단힌 저장 프로시저를 호출한다. 좀 더 복잡한 저장 프로시저 지원은 뒷부분에서 다룬다.

this.jdbcTemplate.update("call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));


13.2.1.2 JdbcTemplate의 좋은 사용사례
JdbcTemplate 클래스의 인스턴스는 설정되고 나면 스레드세이프하다. 이는 JdbcTemplate의 하나의 인스턴스를 설정하고 나서 안전하게 이 인스턴스의 공유된 참조를 여러 DAO(또는 레파지토리)에 주입할 수 있다는 것을 의미하므로 매우 중요하다. JdbcTemplate는 상태를 가지고(stateful) 있지만(내부에 DataSource에 대한 참조를 유지하고 있다) 이 상태는 대화식(conversational)의 상태는 아니다.

JdbcTemplate 클래스(그리고 연관된 SimpleJdbcTemplate와 NamedParameterJdbcTemplate 클래스들) 사용시의 일반적인 관행은 스프링 설정파일에서 DataSource를 설정하고 공유된 DataSource 빈을 DAO 클래스에 의존성 주입하는 것이다. JdbcTemplate는 DataSource에 대한 setter로 생성된다. 이는 다음과 같이 DAO가 된다.

public class JdbcCorporateEventDao implements CorporateEventDao {

  private JdbcTemplate jdbcTemplate;

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

  // CorporateEventDao의 메서드의 JDBC에 기반한 구현들이 이어진다...
}

여기에 대응되는 설정은 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
      http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd">
  
  <bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
  </bean>

  <context:property-placeholder location="jdbc.properties"/>
</beans>

명시적인 설정의 다른 대안은 의존성 주입에 컴포넌트 스캔과 어노테이션 지원을 사용하는 것이다. 이 경우에 클래스에 @Repository 어노테이션(컴포넌트 스캔의 후보가 되도록 한다)을 붙히고 DataSource setter 메서드에 @Autowired 어노테이션을 붙힌다.

@Repository
public class JdbcCorporateEventDao implements CorporateEventDao {

  private JdbcTemplate jdbcTemplate;

  @Autowired
  public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
  }

  // CorporateEventDao의 메서드의 JDBC에 기반한 구현들이 이어진다...
}

여기에 대응되는 XML 설정파일은 다음과 같을 것이다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
      http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd">
  
  <!-- Scans within the base package of the application for @Components to configure as beans -->
  <context:component-scan base-package="org.springframework.docs.test" />
  
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
  </bean>

  <context:property-placeholder location="jdbc.properties"/>
</beans>

스프링의 JdbcDaoSupport 클래스를 사용하고 JDBC를 사용하는 여러 DAO 클래스들이 이 클래스를 확장한다면 하위클래스들은 JdbcDaoSupport 클래스에서 setDataSource(..) 메서드를 상속받는다. JdbcDaoSupport 클래스를 상속받을지 여부를 선택할 수 있다. JdbcDaoSupport 클래스는 단지 편리함을 위해서 제공될 뿐이다.

앞의 템플릿 초기화 방식을 사용하는지(또는 사용하지 않든지) 여부에 상관없이 SQL을 실행하려고 할 때마다 JdbcTemplate 클래스의 새로운 인스턴스를 생성해야 하는 경우는 드물다. 일단 설정하고 나면 JdbcTemplate 인스턴스는 스레드세이프하다. 어플리케이션이 여러가지 DataSources를 필요로 하는 여러 데이터베이스에 접근하고 다르게 설정된 다수의 JdbcTemplates에 접근한다면 여러가지 JdbcTemplate 인스턴스를 필요로 할 것이다.


13.2.2 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate 클래스는 이름이 있는 파라미터들을 사용해서 JDBC 문의 프로그래밍을 지원하고 이는 전형적인 플레이스홀더 ('?') 아규먼트만 사용하는 JBDC 문을 프로그래밍하는 방법과는 정반대가 된다. NamedParameterJdbcTemplate 클래스는 JdbcTemplate를 감싸고 작업을 수행을 감싸진 JdbcTemplate에 위임한다. 이번 섹션에서는 NamedParameterJdbcTemplate 클래스가 JdbcTemplate와는 다른 부분인 이름있는 파라미터를 사용해서 JDBC문을 프로그래밍하는 부분만을 설명한다.

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
  this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

  String sql = "select count(*) from T_ACTOR where first_name = :first_name";

  SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);

  return namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}

sql 변수에 값을 할당하는 이름있는 파라미터와 namedParameters 변수(MapSqlParameterSource타입의)에 끼워넣는 대응되는 값의 사용방법을 유심히 봐라.

다른 방법으로는 Map에 기반한 형식을 사용해서 NamedParameterJdbcTemplate 인스턴스에 이름있는 파라미터와 대응되는 값을 전달할 수 있다. NamedParameterJdbcOperations가 노출하고 NamedParameterJdbcTemplate 클래스가 구현한 다른 메서드들은 유사한 형식을 따르고 있으므로 여기서는 다루지 않는다.

다음 예제는 Map기반 방식의 사용방법을 보여준다.

// JDBC를 사용하는 DAO 클래스...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
  this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

  String sql = "select count(*) from T_ACTOR where first_name = :first_name";

  Map namedParameters = Collections.singletonMap("first_name", firstName);

  return this.namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}

NamedParameterJdbcTemplate와 관련해서 한가지 좋은 기능(그리고 같은 자바 패키지에 존재하는)은 SqlParameterSource 인터페이스다. 앞의 코드 예제(MapSqlParameterSource 클래스)에서 SqlParameterSource 인터페이스의 구현 예제를 이미 보았다. SqlParameterSource는 NamedParameterJdbcTemplate의 이름있는 파라미터값의 소스이다. MapSqlParameterSource 클래스는 키가 파라미터 이름이고 값이 파라미터의 값인 java.util.Map의 아답터 역할을 하는 아주 간단한 구현체이다.

또다른 SqlParameterSource 구현체는 BeanPropertySqlParameterSource 클래스이다. 이 클래스는 임의의 JavaBean(즉, JavaBean의 관례를 충실하게 따르는 클래스의 인스턴스)을 감싸고 감싼 JavaBean의 프로퍼티들을 이름있는 파라미터 값들의 소스로 사용한다.

public class Actor {

  private Long id;
  private String firstName;
  private String lastName;
  
  public String getFirstName() {
    return this.firstName;
  }
  
  public String getLastName() {
    return this.lastName;
  }
  
  public Long getId() {
    return this.id;
  }
  
  // setter는 생략했다...
}


// JDBC를 사용하는 DAO 클래스...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
  this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActors(Actor exampleActor) {

  // 이름있는 파라미터들이 어떻게 위의 'Actor' 클래스의 프로퍼티에서 일치되는 것을 찾는지 주의깊게 봐라.
  String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

  SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

  return this.namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}

NamedParameterJdbcTemplate 클래스가 전통적인 JdbcTemplate 템플릿을 감싼다는 것을 기억해라. JdbcTemplate 클래스에만 존재하는 기능에 접근하기 위해 감싸진 JdbcTemplate 인스턴스에 접근해야 한다면 JdbcOperations 인터페이스를 통해 감싸진 JdbcTemplate에 접근하기 위해 getJdbcOperations() 메서드를 사용할 수 있다.

어플리케이션의 컨텍스트에서 NamedParameterJdbcTemplate 클래스를 사용하는 가이드라인에 대해서는 Section 13.2.1.2, “JdbcTemplate의 좋은 사용사례”도 참고해라.


13.2.3 SimpleJdbcTemplate
SimpleJdbcTemplate 클래스는 전통적인 JdbcTemplate를 감싸고 가변인자와 오토박싱(autoboxing)같은 자바 5의 기능을 사용한다.

Note
스프링 3.0에서는 JdbcTemplate도 제너릭과 가변인자같은 자바 5의 개선된 문법을 지원한다. 하지만 SimpleJdbcTemplate는 JdbcTemplate가 제공하는 모든 메서드에 접근하지 않아도 될 때 가장 잘 동작하는 더 간단한 API를 제공한다. 또한 SimpleJdbcTemplate가 자바 5에 맞춰서 설계되었기 때문에 SimpleJdbcTemplate는 파라미터를 정렬을 다르게 하는데서 오는 가변인자의 장점을 취하는 더 많은 메서드를 가진다.

편리한 문법적인 면에서 SimpleJdbcTemplate 클래스가 주는 부가적인 가치는 이전-이후(before-and-after) 예시에서 가장 잘 설명한다. 다은 코드 예제는 전통적인 JdbcTemplate를 사용하는 데이터 접근 코드를 보여주고 그 뒤에 이어진 코드 예제는 같은 작업을 SimpleJdbcTemplate로 구현했다.

// 전통적인 JdbcTemplate 방식...
private JdbcTemplate jdbcTemplate;

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

public Actor findActor(String specialty, int age) {

  String sql = "select id, first_name, last_name from T_ACTOR" + 
    " where specialty = ? and age = ?";
  
  RowMapper<Actor> mapper = new RowMapper<Actor>() {
    public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
      Actor actor = new Actor();
      actor.setId(rs.getLong("id"));
      actor.setFirstName(rs.getString("first_name"));
      actor.setLastName(rs.getString("last_name"));
      return actor;
    }
  };

  
  // 배열에 아규먼트들을 담은 것을 주의깊게 봐라.
  return (Actor) jdbcTemplate.queryForObject(sql, new Object[] {specialty, age}, mapper);
}

다음은 SimpleJdbcTemplate로 작성한 같은 메서드이다.

// SimpleJdbcTemplate 방식...
private SimpleJdbcTemplate simpleJdbcTemplate;

public void setDataSource(DataSource dataSource) {
  this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
}

public Actor findActor(String specialty, int age) {

  String sql = "select id, first_name, last_name from T_ACTOR" + 
    " where specialty = ? and age = ?";
  RowMapper<Actor> mapper = new RowMapper<Actor>() {  
    public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
      Actor actor = new Actor();
      actor.setId(rs.getLong("id"));
      actor.setFirstName(rs.getString("first_name"));
      actor.setLastName(rs.getString("last_name"));
      return actor;
    }
  };

  // 파라미터의 값들이 RowMapper 파라미터 이후에 오기 때문에
  // 가변인자를 사용하고 있다.
  return this.simpleJdbcTemplate.queryForObject(sql, mapper, specialty, age);
}

어플리케이션의 컨텍스트에서 SimpleJdbcTemplate 클래스를 사용하는 방법에 대한 가이드라인은 Section 13.2.1.2, “JdbcTemplate의 좋은 사용사례”를 참고해라.

Note
SimpleJdbcTemplate 클래스는 JdbcTemplate 클래스가 노출하는 메서드의 하위세트만을 제공한다. SimpleJdbcTemplate에 정의되지 않은 JdbcTemplate 메서드를 사용해야 한다면 SimpleJdbcTemplate에서 getJdbcOperations() 메서드를 호출해서 의존하는 JdbcTemplate에 항상 접근해서 원하는 메서드를 호출할 수 있다. JdbcOperations 인터페이스의 메서들들은 제너릭이 아니라는 점이 유일한 장점이므로 캐스팅등의 작업을 해야한다.


13.2.4 SQLExceptionTranslator
SQLExceptionTranslator는 데이터 접근 전략에 관해서는 알 필요없이 SQLExceptions와 스프링 자체의 org.springframework.dao.DataAccessException사이의 변환을 할 수 있는 클래스가 구현하는 인터페이스이다. 구현체들은 제너릭이 되거나(예를 들면 JDBC에 SQLState 코드를 사용하는) 훨씬 더 정확하게 소유자(proprietary)가 될 수 있다(예를 들면 Oracle 오류 코드를 사용하는).

SQLErrorCodeSQLExceptionTranslator는 기본적으로 사용하는 SQLExceptionTranslator의 구현체이다. 이 구현체는 특정 벤더의 코드를 사용하고 이는 SQLState 구현체보다 훨씬 더 정확하다. 오류코드 변환은 SQLErrorCodes라는 JavaBean 타입의 클래스에 있는 코드에 기반한다. 이 클래스는 SQLErrorCodesFactory가 생성하고 유지한다. SQLErrorCodesFactory는 이름에서 알 수 있듯이 sql-error-codes.xml라는 설정 파일의 내용에 기반해서 SQLErrorCodes를 생성하는 팩토리이다. sql-error-codes.xml 파일에는 벤더코드가 있고 DatabaseMetaData에서 가져온 DatabaseProductName에 기반한다. 실제로 사용하는 데이터베이스에 대한 코드를 사용하게 된다.

SQLErrorCodeSQLExceptionTranslator는 다음의 순서로 매칭규칙을 적용한다.

Note
Error 코드와 커스텀 예외 변환을 정의하는데 SQLErrorCodesFactory를 기본적으로 사용한다. 클래스패스의 sql-error-codes.xml 파일에서 검색을 하고 찾아낸 SQLErrorCodes 인스턴스는 사용하는 데이터베이스의 데이터베이스 메타데이터에서 데이터베이스 이름에 기반해서 위치하고 있다.

  1. 하위클래스가 구현한 모든 커스텀 변환. 보통 제공된 SQLErrorCodeSQLExceptionTranslator 구현을 사용하므로 이 규칙은 적용되지 않는다. 실제로 제공된 하위클래스 구현체가 있을 때만 적용한다.
  2. SQLErrorCodes 클래스의 customSqlExceptionTranslator 프로퍼티로 제공되는 SQLExceptionTranslator 인터페이스의 모든 커스텀 구현체.
  3. CustomSQLErrorCodesTranslation 클래스 (SQLErrorCodes 클래스의 customTranslations 프로퍼티에 제공된)의 인스턴스 목록은 매칭을 위해서 검색된다.
  4. Error 코드 매칭은 적용된다.
  5. 폴백(fallback) 변환기(translator)를 사용해라. SQLExceptionSubclassTranslator는 기본 폴백 변환기이다. 이 변환을 사용할 수 없다면 다음 폴백 변환기는 SQLStateSQLExceptionTranslator이다.
SQLErrorCodeSQLExceptionTranslator를 확장할 수 있다.

public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {

  protected ㄷDataAccessException customTranslate(String task, String sql, SQLException sqlex) {
    if (sqlex.getErrorCode() == -12345) {
      return new DeadlockLoserDataAccessException(task, sqlex);
    }
    return null;
  }
}

이 예제에서 지정한 오류코드인 -12345는 변환되고 그외 다른 오류코드들은 기본 변환기 구현체가 변환하도록 내버려둔다. 이 커스텀 변환기를 사용하려면 setExceptionTranslator 메서드로 JdbcTemplate에 커스텀 변환기를 전달해야 하고 이 변환기가 필요한 모든 데이터 접근 처리에 이 JdbcTemplate를 사용해야 한다. 다음은 이 커스텀 변환기를 어떻게 사용할 수 있는지에 대한 예제이다.

private JdbcTemplate jdbcTemoplate;

public void setDataSource(DataSource dataSource) {
  // JdbcTemplate를 생성하고 데이터 소스를 설정한다 
  this.jdbcTemplate = new JdbcTemplate(); 
  this.jdbcTemplate.setDataSource(dataSource); 
  // 커스텀 변환기를 생성하고 기본적으로 변환을 위해 검색되도록 DataSource에 설정한다 
  CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator(); 
  tr.setDataSource(dataSource); 
  this.jdbcTemplate.setExceptionTranslator(tr); 
}

public void updateShippingCharge(long orderId, long pct) {
  // 이 update에 prepared JdbcTemplate를 사용한다
  this.jdbcTemplate.update("update orders" + 
          " set shipping_charge = shipping_charge * ? / 100" + 
          " where id = ?"
      pct, orderId); 
}

sql-error-codes.xml의 오류 코드를 검색하기 위해 데이터 소스에 커스텀 변환기를 전달한다.


13.2.5 스테이트먼트 실행
SQL 스테이트먼트를 실행하려면 아주 약간의 코드가 필요하다. DataSource와 편리한 메서드를 가진 JdbcTemplate이 필요하다. 다은 예제는 새로운 테이블을 생성하는 완전한 기능을 가진 클래스에 최소한으로 무엇을 포함해야 하는지를 보여준다.

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAStatement {

  private JdbcTemplate jdbcTemplate;

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

  public void doExecute() {
    this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
  }
}


13.2.6 조회 수행
몇몇 조회 메서드는 단일 값을 반환한다. 하나의 열에서 카운트나 특정 값을 획득하려면 queryForInt(..), queryForLong(..), queryForObject(..)를 사용해라. 후자의 경우 반환된 JDBC Type를 아규먼트로 전달한 자바 클래스로 변환한다. 타입 변환이 유효하지 않다면 InvalidDataAccessApiUsageException를 던진다. 다음은 두 개의 조회 메서드를 가진 예제다. 조회 메서드 중 하나는 int를 조회하고 다른 하나는 String을 조회한다.

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class RunAQuery {

  private JdbcTemplate jdbcTemplate;

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

  public int getCount() {
    return this.jdbcTemplate.queryForInt("select count(*) from mytable");
  }

  public String getName() {
    return (String) this.jdbcTemplate.queryForObject("select name from mytable", String.class);
  }

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

하나의 값을 반환하는 조회 메서드에 추가적으로 많은 메서드들은 조회결과의 각 열의 엔트리 목록을 반환한다. 가장 일반적인 메서드는 List를 반환하는 queryForList(..)인다. List의 각 엔트리는 해당 열의 컬럼값을 맵으로 나타낸 Map이다. 모든 열의 리스트를 얻기위해 위의 예제에 메서드를 추가하면 다음과 같이 될 것이다.

private JdbcTemplate jdbcTemplate;

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

public List<Map<String, Object>> getList() {
  return this.jdbcTemplate.queryForList("select * from mytable");
}

반환된 리스트는 다음과 같은 형태가 될 것이다.

[{name=Bob, id=1}, {name=Mary, id=2}]


13.2.7 데이터베이스 갱신
다음 예제는 지정한 기본 키(primary key)로 갱신된 컬럼을 보여준다. 이 예제에서 SQL문은 열(row) 파라미터를 위한 플레이스홀더를 가진다. 파라미터 값은 가변인자나 객체의 배열로 전달할 수 있다. 그러므로 프리미티브(primitives)는 프리미티브 랩퍼 클래스로 명식적으로 감싸거나 오토박싱을 사용해야 한다.

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAnUpdate {

  private JdbcTemplate jdbcTemplate;

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

  public void setName(int id, String name) {
    this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
  }
}


13.2.8 자동 생성된 키 얻기
편리한 update() 메서드는 데이터베이스가 생성한 기본 키의 획득을 지원한다. 이 지원은 JDBC 3.0 표준의 일부분이다. 자세한 내용은 명세서의 13.6장을 참고해라. 이 메서드는 첫 인자로 PreparedStatementCreator를 받은데 이것이 필수 insert문을 지정하는 방법이다. 다른 인자는 update가 성공정으로 반환하는 생성된 키를 담고 있는 KeyHolder이다. 적합한 PreparedStatement (메서드 시그니처가 왜 이렇게 되어 있는지를 설명한다.)를 생성하는 하나의 표준화된 방법은 존재하지 않는다. 다음 예제는 오라클에서는 동작하지만 다른 플랫폰에서는 동작하지 않는다.

final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";

KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
  new PreparedStatementCreator() {
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
      PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"});
      ps.setString(1, name);
      return ps;
    }
  },
  keyHolder);

// keyHolder.getKey()는 이제 생성된 키를 담고 있다


13.3 데이터베이스 연결 제어

13.3.1 DataSource
스프링은 DataSource로 데이터베이스의 연결을 획득한다. DataSource는 JDBC 명세의 일부분이면서 일반화된 연결 팩토리이다. 컨테이너나 프레임워크는 DataSource로 어플리케이션 코드에서 연결 풀링(pooling)과 트랜잭션 관리 이슈를 감출 수 있다. 개발자들은 데이터베이스에 연결하는 방법에 대해서 자세히 알 필요가 없다. 이는 데이터소스를 설정하는 관리자의 책임이다. 아마 대부분은 코드를 작성하고 테스트하는 두가지 역할을 다 할 것이지만 프로덕션 데이터 소스가 어떻게 설정되는 지를 알아야 할 필요는 없다.

스프링의 JDBC 계층을 사용하는 경우 JNDI에서 데이터 소스를 얻거나 서드파티가 제공하는 커넥션풀 구현체로 설정한다. 많이 사용하는 구현체는 Apache Jakarta Commons DBCP와 C3P0이다. 스프링 배포판에 포함된 구현체는 오로지 테스트만을 위해서 제공되는 것이고 풀링(pooling)은 하지 않는다.

이번 섹션에서는 스프링의 DriverManagerDataSource 구현체를 사용하고 뒷부분에서 여러가지 부가적인 구현체를 다룬다.

Note
DriverManagerDataSource는 풀링을 지원하지 않으므로 테스트할 때만 사용해야 하고 연결을 하는 여러 요청이 있을 때는 성능이 나빠질 것이다.

일반적으로 JDBC 연결을 얻듯이 DriverManagerDataSource로 연결을 얻는다. DriverManager가 드라이버 클래스를 로드할 수 있도록 JDBC 드라이버의 정규화된 클래스명을 지정해라. 그 다음, JDBC 드라이버마다 다른 URL을 제공해라.(정확한 값은 드라이버의 문서를 참고해라.) 그리고 데이터베이스 연결에 사용자명과 비밀번호를 제공해라. 다음은 자바코드로 어떻게 DriverManagerDataSource 설정하는 지에 대한 예제이다.

DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

다음은 대응되는 XML 설정이다.

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="${jdbc.driverClassName}"/>
  <property name="url" value="${jdbc.url}"/>
  <property name="username" value="${jdbc.username}"/>
  <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

다음 예제는 DBCP와 C3P0의 기본적인 연결과 설정을 보여준다. 풀링 기능을 제어하는 더 많은 옵션에 대해서 알고 싶다면 각 커넥션 풀링 구현체의 제품 문서를 참고해라.

DBCP 설정:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  <property name="driverClassName" value="${jdbc.driverClassName}"/>
  <property name="url" value="${jdbc.url}"/>
  <property name="username" value="${jdbc.username}"/>
  <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

C3P0 설정:

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
  <property name="driverClass" value="${jdbc.driverClassName}"/>
  <property name="jdbcUrl" value="${jdbc.url}"/>
  <property name="user" value="${jdbc.username}"/>
  <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>


13.3.2 DataSourceUtils
DataSourceUtils 클래스는 JNDI에서 연결을 얻고 필요한 경우 연결을 닫는 static 메서드를 제공하는 편리하고 강력한 헬퍼 클래스이다. 예를 들어 DataSourceUtils 클래스는 DataSourceTransactionManager로 스레드에 기반한 연결을 지원한다.


13.3.3 SmartDataSource
SmartDataSource 인터페이스는 관계형 데이터베이스의 연결을 제공할 수 있는 클래스로 구현되어야 한다. SmartDataSource는 클래스가 SmartDataSource 인터페이스를 사용해서 해당 작업 후에 연결을 닫아야하는지 물어볼 수 있도록 DataSource를 확장한다. 이 방법은 연결을 재사용한다는 것을 알고 있을 때 효율적이다.


13.3.4 AbstractDataSource
AbstractDataSource는 모든 DataSource 구현체에 공통적인 코드를 구현하는 스프링의 DataSource 구현체의 abstract 기반 클래스이다. 자신만의 DataSource 구현체를 작성한다면 AbstractDataSource 클래스를 확장해라.


13.3.5 SingleConnectionDataSource
SingleConnectionDataSource는 SmartDataSource 인터페이스의 구현체로 사용할 때마다 닫지 않는 하나의 Connection을 감싸고 있다. 명백하게 이는 멀티쓰레드 기능은 아니다.

퍼시스턴스 도구를 사용할 때처럼 풀링된 연결에서 close를 호출하는 클라이언트 코드가 있다면 suppressClose 프로퍼티를 true로 설정해라. 이 설정은 물리적인 연결을 감싸고 있는 닫기차단 프록시(close-suppressing proxy)를 반환한다. 이를 네이티브 오라클 Connection 등으로 캐스팅 할 수 있다는 점에 주의해라.

SingleConnectionDataSource는 주로 테스트 클래스이다. 예를 들어 어플리케이션 서버 외부에서 간단한 JNDI 환경과 결합해서 쉽게 코드를 테스트할 수 있다. DriverManagerDataSource와는 반대로 SingleConnectionDataSource는 물리적인 연결의 과도한 생성을 피하려고 항상 같은 연력을 재사용한다.


13.3.6 DriverManagerDataSource
DriverManagerDataSource 클래스는 표준 DataSource 인터페이스의 구현체로 빈(bean) 프로퍼티로 평범한 JDBC 드라이버를 설정하고 매번 새로운 Connection를 반환한다.

이 구현체는 스프링 IoC 컨테이너의 DataSource 빈이든 간단한 JNDI 환경과 결합해서 사용하든 간에 Java EE 컨테이너 외부의 단독적인 환경이나 테스트에 유용하다. 풀링을 가정한 Connection.close() 호출은 연결을 그냥 닫아버리므로 DataSource에 의존하는 모든 퍼시스턴스 코드는 동작할 것이다. 하지만 commons-dbcp같은 JavaBean방식의 커넥션풀의 사용은 테스트 환경에서조차도 아주 쉽다. 테스트 환경에서는 거의 대부분 DriverManagerDataSource상의 커넥션풀등을 사용하는 것이 바람직하다.


13.3.7 TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy는 대상 DataSource의 프록시로 스프링이 관리하는 트랜잭션을 인식하도록 해당 대상 DataSource를 감싼다. 이런 점에서 Java EE 서버가 제공하는 트랜잭션이 가능한 JNDI DataSource와 유사하다.

Note
이미 존재하는 코드에서 표준 JDBC DataSource 인터페이스 구현체를 호출하고 전달해야 하는 경우를 제외하고는 이 클래스를 사용할 일은 많지 않을 것이다. 이러한 경우에도 여전히 이 코드를 사용할 수 있고 동시에 스프링이 관리하는 트랜잭션에 참가한다. JdbcTemplate나 DataSourceUtils같은 리소스 관리에 더 높은 수준의 추상화를 사용해서 새로운 코드를 작성하는 것이 일반적으로 더 낫다.

(더 자세한 내용은 TransactionAwareDataSourceProxy Javadoc을 참고해라.)


13.3.8 DataSourceTransactionManager
DataSourceTransactionManager 클래스는 단일 JDBC 데이터소스의 PlatformTransactionManager 구현체이다. DataSourceTransactionManager는 지정한 데이터 소스의 JDBC 연결을 현재 실행되고 있는 스레드에 바인딩하고 아마도 데이터 소스마다 하나의 스레드 연결을 할 것이다.

Java EE의 표준 DataSource.getConnection 대신 DataSourceUtils.getConnection(DataSource)로 JDBC 연결을 얻으려면 어플리케이션 코드가 필요하다. DataSourceUtils.getConnection(DataSource)는 체크드 SQLExceptions 대신 언체크드 org.springframework.dao 예외를 던진다. JdbcTemplate같은 모든 프레임워크 클래스들은 암묵적으로 이 전략을 사용한다. 이 트랜잭션 관리자를 사용하지 않는다면 검색 전략은 정확하게 공통된 것으로 수행한다. 그러므로 어떠한 경우에도 사용할 수 있다.

DataSourceTransactionManager 클래스는 커스텀 격리수준과 작절한 JDBC 스테이트먼트 쿼리 만료시간에 적용되는 만료시간(timeout)을 지원한다. 후자를 지원하려면 어클리케이션 코드가 JdbcTemplate를 사용하거나 스테이크먼트를 생성할 때마다 DataSourceUtils.applyTransactionTimeout(..) 메서드를 호출해야 한다.

이 구현체를 리소스가 하나인 경우에 JtaTransactionManager 대신 사용할 수 있으므로 JTA를 지원하는 컨테이너가 필요치 않다. 필요로하는 연결 검색패턴을 사용하는 경우에는 단지 설정만 바꾸면 된다. JTA는 커스텀 격리수준을 지원하지 않는다!


13.3.9 NativeJdbcExtractor
때로는 표준 JDBC API와는 다른 벤더에 특화된 JDBC 메서드에 접근해야 한다. 어플리케이션 서버를 운영하고 있거나 Connection, Statement, ResultSet 객체들을 랩퍼 객체로 감싸는 DataSource를 사용하고 있다면 벤더에 특화된 JDBC 메서드에 접근하는 것은 문제의 소지가 있다. 네이티브 객체에 접근하려면 JdbcTemplate나 OracleLobHandler를 NativeJdbcExtractor로 설정할 수 있다.

NativeJdbcExtractor는 실행환경에 따라 다양하다.

  • SimpleNativeJdbcExtractor
  • C3P0NativeJdbcExtractor
  • CommonsDbcpNativeJdbcExtractor
  • JBossNativeJdbcExtractor
  • WebLogicNativeJdbcExtractor
  • WebSphereNativeJdbcExtractor
  • XAPoolNativeJdbcExtractor
보통은 대부분의 환경에서 감싸지 않은 Connection 객체에 SimpleNativeJdbcExtractor로 충분한다. 자세한 내용은 Javadoc를 참고해라.
2012/12/27 03:53 2012/12/27 03:53