Outsider's Dev Story

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

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

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




13.4 JDBC 배치 작업
대부분의 JDBC 드라이버는 같은 프리페어드 스테이트먼트를 여러번 호출하는 배치에 향상된 성능을 제공한다. 갱신(update)을 배치로 그룹핑해서 데이터베이스에 라운드트립하는 수를 제한한다. 이번 섹션은 JdbcTemplate와 SimpleJdbcTemplate를 둘 다 사용하는 배치 처리를 다룬다.


13.4.1 JdbcTemplate의 기본적인 배치 작업
전 용 인터페이스인 BatchPreparedStatementSetter의 두 메서드를 구현해서 batchUpdate 메서드 호출의 두번째 파라미터로 전달함으로써 JdbcTemplate 배치 처리를 한다. 현재 배치의 크기를 공하려면 getBatchSize를 사용해라. 프리페어드 스테이트먼트의 파라미터에 값을 설정하려면 setValues 메서드를 사용해라. 이 메서드는 getBatchSize 호출에서 지정한 횟수만큼 호출될 것이다. 다음 예제는 리스트로 actor 테이블의 엔트리를 갱신한다. 이 예제에서 전체 리스트는 배치로 사용된다.

public class JdbcActorDao implements ActorDao {
  private JdbcTemplate jdbcTemplate;

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

  public int[] batchUpdate(final List<Actor> actors) {
    int[] updateCounts = jdbcTemplate.batchUpdate(
      "update t_actor set first_name = ?, last_name = ? where id = ?",
      new BatchPreparedStatementSetter() {
        public void setValues(PreparedStatement ps, int i) throws SQLException {
          ps.setString(1, actors.get(i).getFirstName());
          ps.setString(2, actors.get(i).getLastName());
          ps.setLong(3, actors.get(i).getId().longValue());
        }

        public int getBatchSize() {
          return actors.size();
        }
      } );
    return updateCounts;
  }

  //  ... 추가적인 메서드
}

파 일에서 읽거나 갱신한 스트림을 처리한다면 원하는 배치크기가 있을 것이지만 마지막 배치는 엔트리의 수를 갖지 않을 것이다. 이 경우에 입력소스가 모두 소비되면 배치를 인터럽트할 수 있게 하는 InterruptibleBatchPreparedStatementSetter 인터페이스를 사용할 수 있다. isBatchExhausted 메서드는 배치 마지막에 신호를 보낼 수 있게 한다.


13.4.2 객체 리스트의 배치 작업
JdbcTemplate 와 NamedParameterJdbcTemplate 모두 배치 갱신의 대안책을 제공한다. 전용 배치 인터페이스를 구현하는 대신에 호출의 모든 파라미터 값을 리스ㅡ로 제공한다. 프레임워크는 이 값들을 순회하면서 내부의 프리페어드 스테이트먼트의 setter를 사용한다. 이름있는 파라미터를 사용하는지 여부에 따라 API는 달라진다. 이름있는 파라미터에서는 배치의 각 멤버의 엔트리로 SqlParameterSource의 배열을 제공한다. 이 배열을 생성하는데 SqlParameterSource.createBatch 메서드를 사용할 수 있고 이 메서드에는 파라미터값을 담고 있는 JavaBean의 배열이나 맵의 배열을 전달한다.

이 예제는 이름있는 파라미터를 사용한 배치 갱신을 보여준다.

public class JdbcActorDao implements ActorDao {
  private NamedParameterTemplate namedParameterJdbcTemplate;

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

  public int[] batchUpdate(final List<Actor> actors) {
    SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray());
    int[] updateCounts = namedParameterJdbcTemplate.batchUpdate(
      "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
      batch);
    return updateCounts;
  }

  //  ... 추가적인 메서드
}

전통적인 "?" 플레이스홀더를 사용하는 SQL에서는 갱신할 값의 객체 배열을 담고 있는 리스트를 전달한다. 이 객체 배열은 SQL문의 각 플레이스홀더마다 하나의 엔트리를 가져야 하고 SQL문에 정의된 것과 같은 순서여야 한다.

전통적인 JDBC "?" 플레이스홀더를 사용하는 같은 예제

public class JdbcActorDao implements ActorDao {
  private JdbcTemplate jdbcTemplate;

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

  public int[] batchUpdate(final List<Actor> actors) {
    List<Object[]> batch = new ArrayList<Object[]>();
    for (Actor actor : actors) {
      Object[] values = new Object[] {
          actor.getFirstName(),
          actor.getLastName(),
          actor.getId()};
      batch.add(values);
    }
    int[] updateCounts = jdbcTemplate.batchUpdate(
      "update t_actor set first_name = ?, last_name = ? where id = ?",
      batch);
    return updateCounts;
  }

  //  ... 추가적인 메서드
}

위의 모든 배치 갱신 메서드들은 각 배치에서 업데이트된 열의 수를 담고 있는 배열을 반환한다. 이 갯수는 JDBC 드라이버가 보고한 숫자이다. 이 갯수가 없다면 JDBC 드라이버는 -2를 반환한다.


13.4.3 여러가지 배치를 가진 배치 작업
배 치 갱신의 마지막 예제는 여러 가지 작은 배치로 분리하기를 원하는 꽤 큰 배치이다. 물론 batchUpdate 메서드를 여러번 호출해서 앞에서 언급한 메서드들로 처리할 수도 있지만 더 편리한 메서드가 있다. 이 메서드는 SQL문 외에 파라미터를 담고 있는 객체의 컬렉션, 각 배치가 갱신해야하는 수, 프리페이스 스테이트먼트의 파라미터 값을 설정할 ParameterizedPreparedStatementSetter 받는다. 프레임워크는 제공받은 값들을 순회하면서 지정한 크기로 갱신 호출을 나눈다.

다음의 예제는 배치 사이즈 100을 사용하는 배치 업데이트를 보여준다.

public class JdbcActorDao implements ActorDao {
  private JdbcTemplate jdbcTemplate;

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

  public int[][] batchUpdate(final Collection<Actor> actors) {
    Collection<Object[]> batch = new ArrayList<Object[]>();
    for (Actor actor : actors) {
      Object[] values = new Object[] {
        actor.getFirstName(),
        actor.getLastName(),
        actor.getId()};
      batch.add(values);
    }
    int[][] updateCounts = jdbcTemplate.batchUpdate(
      "update t_actor set first_name = ?, last_name = ? where id = ?",
      actors,
      100,
      new ParameterizedPreparedStatementSetter<Actor>() {
        public void setValues(PreparedStatement ps, Actor argument) throws SQLException {
          ps.setString(1, argument.getFirstName());
          ps.setString(2, argument.getLastName());
          ps.setLong(3, argument.getId().longValue());
         }
       } );
    return updateCounts;
  }

  //  ... 추가적인 메서드
}

이 호출의 배치 갱신 메서드들은 갱신된 각각의 열의 갯수의 배열을 가진 각 배치의 배열 엔트리를 담고 있는 int 배열의 배열을 반환한다. 최상위 배열의 길이는 실행된 배치의 수를 나타내고 두번째 깊이의 배열 길이는 해당 배치가 갱신한 수를 나타낸다. 각 배치에서 갱신된 수는 모든 배치에 제공된 배치 크기여야 하지만 마지막 배치는 제공한 갱신 객체의 전체 수에 따라 더 적을 수도 있다. 각 업데이트 스테이트먼트에서 갱신된 수는 JDBC 드라이버가 보고한 수이다. 이 갯수가 없다면 JDBC 드라이버는 -2를 반환한다.


13.5 SimpleJdbc 클래스로 JDBC 작업 간소화하기
SimpleJdbcInsert 와 SimpleJdbcCall 클래스는 JDBC 드라이버로 얻을 수 있는 데이터베이스 메타데이터로 설정을 간소화하게 한다. 즉, 코드에서 세세하게 모든것을 제공하기를 좋아한다면 메타데이터 처리를 끄거나 덮어쓸 수 있다고 하더라도 설정이 더 줄어든다.


13.5.1 SimpleJdbcInsert를 사용해서 데이터 추가하기
최 소한의 설정 옵션으로 SimpleJdbcInsert 클래스를 살펴보자. 데이터 접근 계층의 초기화 메서드에서 SimpleJdbcInsert를 인스턴스화 해야한다. 이 예제에서 초기화 메서드는 setDataSource 메서드이다. SimpleJdbcInsert 클래스의 하위클래스를 만들 필요는 없다. 그냥 새로운 인스턴스를 생성하고 withTableName 메서드로 테이블명을 설정해라. 이 클래스의 설정 메서드는 모든 설정 메서드를 체인으로 연결 할 수 있도록 SimpleJdbcInsert의 인스턴스를 반환하는 "유동성있는(fluid)" 방식을 따른다. 이 예제는 딱 하나의 설정 메서드를 사용한다. 뒤에서 여러 설정메서드를 사용하는 예제를 볼 것이다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcInsert insertActor;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
  }

  public void add(Actor actor) {
    Map<String, Object> parameters = new HashMap<String, Object>(3);
    parameters.put("id", actor.getId());
    parameters.put("first_name", actor.getFirstName());
    parameters.put("last_name", actor.getLastName());
    insertActor.execute(parameters);
  }

  //  ... 추가적인 메서드
}

여 기서 사용할 실행 메서드는 유일한 파라미터로 평범한 java.utils.Map를 받는다. 여기서 중요한 점은 맵에 사용한 키가 데이터베이스에 정의한 테이블의 컬럼명과 일치해야 한다는 것이다. 이는 실제 insert문을 생성하려고 메타데이터를 읽기 때문이다.


13.5.2 SimpleJdbcInsert를 사용해서 자동생성된 키 얻기
이 예제는 앞의 예제와 같은 insert문을 사용하지만 id에 전달하는 하는 대신 자동생성된 키를 얻어서 새로운 Actor 객체에 설정한다. SimpleJdbcInsert를 생성할 때 테이블명을 지정하는 것 외에 usingGeneratedKeyColumns 메서드로 생성된 키의 컬럼명을 지정한다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcInsert insertActor;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    this.insertActor = new SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingGeneratedKeyColumns("id");
  }

  public void add(Actor actor) {
    Map<String, Object> parameters = new HashMap<String, Object>(2);
    parameters.put("first_name", actor.getFirstName());
    parameters.put("last_name", actor.getLastName());
    Number newId = insertActor.executeAndReturnKey(parameters);
    actor.setId(newId.longValue());
  }

  //  ... 추가적인 메서드
}

두 번째 접근으로 인서트할 때 가장 큰 차이점은 맵에 id를 추가하지 않고 executeReturningKey 메서드를 호출한다는 점이다. 이는 도메인 클래스에서 사용하는 숫자타입의 인스턴스를 생성할 수 있는 java.lang.Number 객체를 반환한다. 여기서 특정 자바 클래스를 리턴하도록 모든 데이터베이스에 의존할 수 없다. java.lang.Number는 의존할 수 있는 기반 클래스이다. 자동생성되는 컬럼이 다수이거나 생성된 값이 숫자가 아니라면 executeReturningKeyHolder 메서드가 반환하는 KeyHolder를 사용할 수 있다.


13.5.3 SimpleJdbcInsert에 컬럼 지정하기
usingColumns 메서드에 컬럼명의 리스트를 지정해서 인서트할 컬럼을 제한할 수 있다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcInsert insertActor;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    this.insertActor = new SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingColumns("first_name", "last_name")
        .usingGeneratedKeyColumns("id");
  }

  public void add(Actor actor) {
    Map<String, Object> parameters = new HashMap<String, Object>(2);
    parameters.put("first_name", actor.getFirstName());
    parameters.put("last_name", actor.getLastName());
    Number newId = insertActor.executeAndReturnKey(parameters);
    actor.setId(newId.longValue());
  }

  //  ... 추가적인 메서드
}

인서트 실행은 사용할 컬럼을 결정하는 메타데이터에 의존하는 것과 같다.


13.5.4 파라미터 값 제공에 SqlParameterSource 사용하기
파 라미터 값을 제공하는데 Map을 사용해서 잘 동작하지만 Map은 사용하기 가장 편리한 클래스는 아니다. 대신 사용할 수 있는 SqlParameterSource 인터페이스의 구현체를 스프링이 다수 제공한다. 먼저 살펴볼 구현체는 파라미터 값을 담고 있는 JavaBean호환 클래스를 가지고 있다면 아주 편리한 클래스인 BeanPropertySqlParameterSource이다. BeanPropertySqlParameterSource는 파라미터 값을 추출하는데 대응되는 getter 메서드를 사용할 것이다. 다음은 그 예제이다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcInsert insertActor;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    this.insertActor = new SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingGeneratedKeyColumns("id");
  }

  public void add(Actor actor) {
    SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
    Number newId = insertActor.executeAndReturnKey(parameters);
    actor.setId(newId.longValue());
  }

  //  ... 추가적인 메서드
}

또다른 옵션은 Map과 비슷하지만 체인으로 연결할 수 있는 더 편리한 addValue 메서드를 제공하는 MapSqlParameterSource이다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcInsert insertActor;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    this.insertActor = new SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingGeneratedKeyColumns("id");
  }

  public void add(Actor actor) {
    SqlParameterSource parameters = new MapSqlParameterSource()
        .addValue("first_name", actor.getFirstName())
        .addValue("last_name", actor.getLastName());
    Number newId = insertActor.executeAndReturnKey(parameters);
    actor.setId(newId.longValue());
  }

  //  ... 추가적인 메서드
}

여기서 보듯이 설정은 동일하다. 실행코드에서 유일하게 달라진 점은 다른 입력 클래스를 사용한 것뿐이다.


13.5.5 SimpleJdbcCall로 저장 프로시저 호출하기
SimpleJdbcCall 클래스는 in과 out 파라미터의 이름을 검색하는데 데이터베이스의 메타데이터를 사용하므로 명시적으로 파라미터 이름을 지정하지 않아도 된다. 명시적으로 지정하는 것을 더 선호하거나 자바 클래스로 자동매핑되지 않는 ARRAY나 STRUCT같은 파라미터라면 파라미터를 선언할 수 있다. 첫 예제는 MySQL 데이터베이스에서 VARCHAR와 DATE 형식의 스칼라(scalar) 값만을 반환하는 간단한 프로시저를 보여준다. 이 예제의 프로시저는 지정한 액터(actor) 엔트리를 읽고 out 파라미터의 형식으로 first_name, last_name, birth_date컬럼을 반환한다.

CREATE PROCEDURE read_actor ( 
  IN in_id INTEGER, 
  OUT out_first_name VARCHAR(100), 
  OUT out_last_name VARCHAR(100), 
  OUT out_birth_date DATE) 
BEGIN 
  SELECT first_name, last_name, birth_date 
  INTO out_first_name, out_last_name, out_birth_date 
  FROM t_actor where id = in_id;
END;

in_id파라미터는 검색하는 액터의 id를 담고 있다. out 파라미터들은 테이블에서 읽은 데이터를 반환한다.

SimpleJdbcInsert 과 유사한 방법으로 SimpleJdbcCall를 선언한다. 데이터 접근 계층의 초기화 메서드에서 SimpleJdbcCall를 인스턴스화하고 설정해야 한다. StoredProcedure 클래스와 비교해서 하위클래스를 만들 필요가 없고 데이터베이스 메타데이터에서 검색할 수 있는 파라미터를 선언할 필요가 없다. 다음은 위의 저장 프로시저를 사용하는 SimpleJdbcCall 설정의 예제이다. DataSource외에 유일한 설정 옵션은 저장 프로시저의 이름이다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcCall procReadActor;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    this.procReadActor = new SimpleJdbcCall(dataSource)
          .withProcedureName("read_actor");
  }

  public Actor readActor(Long id) {
    SqlParameterSource in = new MapSqlParameterSource().addValue("in_id", id); 
    Map out = procReadActor.execute(in);
    Actor actor = new Actor();
    actor.setId(id);
    actor.setFirstName((String) out.get("out_first_name"));
    actor.setLastName((String) out.get("out_last_name"));
    actor.setBirthDate((Date) out.get("out_birth_date"));
    return actor;
  }

  //  ... 추가적인 메서드
}

호 출을 실행하려고 작성한 코드에서 IN 파라미터를 담고 있는 SqlParameterSource를 생성한다. 입력 값에 제공한 이름과 저장 프로시저에서 선언한 파라미터 이름이 일치해야 한다는 점이 중요하다. 데이터베이스 객체가 저장 프로시저에서 어떻게 참조해야 하는지를 메타데이터를 사용해서 결정하므로 이 경우에는 일치하지 않아도 된다. 저장 프로시저의 소스에 정의한 것은 데이터베이스에 저장되어 있지 않아도 된다. 몇몇 데이터베이스는 이름을 모두 대분자로 변환하는데 반해 어떤 데이터베이스는 소문자를 사용하거나 지정한대로 사용한다.

execute 메서드는 IN 파라미터를 받고 저장 프로시저가 지정한 이름을 키로 사용하는 out 파라미터를 담고 있는 Map을 반환한다. 이 예제의 경우에는 키가 out_first_name, out_last_name, out_birth_date이다.

execute 메서드의 마지막 부분은 받은 데이터를 반환하려고 사용하는 Actor 인스턴스를 생성한다. 다시 얘기하지만 저장 프로시저에 선언한 것과 같은 out 파라미터의 이름을 사용한 것이 중요하다. 또한, 결과로 받은 맵에 저장된 out 파라미터의 이름은 데이터베이스(데이터베이스는 다양할 수 있다)의 out 파라미터 이름과 일치해야 한다. 코드가 더 이식성이 있게 하려면 대소문자를 구별하지 않는 검색을 하거나 스프링이 Jakarta Commons 프로젝트의 CaseInsensitiveMap 를 사용하도록 해야한다. 후자를 사용하려면 자신만의 JdbcTemplate를 생성하고 setResultsMapCaseInsensitive 프로퍼티를 true로 설정해라. 그 다음 이 커스터마이징한 JdbcTemplate 인스턴스를 SimpleJdbcCall의 생성자에 전달해라. 제대로 동작하려면 클래스패스에 commons-collections.jar를 포함해야 한다. 다음은 이 설정에 대한 예제이다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcCall procReadActor;

  public void setDataSource(DataSource dataSource) {
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    jdbcTemplate.setResultsMapCaseInsensitive(true);
    this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
            .withProcedureName("read_actor");
  }

  //  ... 추가적인 메서드
}

이번 섹션을 통해서 반환된 out 파라미터의 이름을 사용하는 경우의 충돌을 피할 수 있다.


13.5.6 SimpleJdbcCall에 사용할 파라미터 명시적으로 선언하기
파 라미터가 메타데이터에 기반해서 어떻게 추론되는지 보았지만 원하다면 명시적으로 선언할 수 있다. 입력으로 다수의 SqlParameter 객체들을 받는 declareParameters 메서드를 가진 SimpleJdbcCall를 생성하고 선언해서 명시적으로 선언할 수 있다. SqlParameter를 정의하는 방법은 다음 섹션에서 자세히 살펴본다.

Note
사 용하는 데이터베이스를 스프링이 지원하지 않을 때 명시적인 선언이 필요하다. 현재 스프링은 다음 데이터베이스들에서 저장 프로시저 호출의 메타데이터 검색을 지원한다. Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle, Sybase. MySQL, Microsoft SQL Server, Oracle에서는 저장 함수(stored function)의 메타데이터 검색도 지원한다.

하나의 파라미너나 몇몇 파라미터 혹은 파라미터 전부를 명시적으로 선언할 수 있다. 파라미터를 명시적으로 선언하지 않은 곳에서는 여전히 파라미터 메타데이터를 사용한다. 잠재적인 파라미터에 대한 메타데이터 검색과정을 모두 건너뛰고 선언된 파라미터만 사용하려면 선언의 일부분으로 withoutProcedureColumnMetaDataAccess 메서드를 호출해라. 데이터베이스 함수에 선언된 두가지 이상의 다른 시그니처를 호출할 것이다. 이 경우에는 주어진 시그니처에 포함하도록 IN 파라미터 이름의 목록을 지정하는 useInParameterNames를 호출한다.

다음 예제는 앞의 예제의 정보를 사용하는 완전히 선언된 프로시저 호출을 보여준다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcCall procReadActor;

  public void setDataSource(DataSource dataSource) {
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    jdbcTemplate.setResultsMapCaseInsensitive(true);
    this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
          .withProcedureName("read_actor")
          .withoutProcedureColumnMetaDataAccess()
          .useInParameterNames("in_id")
          .declareParameters(
                  new SqlParameter("in_id", Types.NUMERIC),
                  new SqlOutParameter("out_first_name", Types.VARCHAR),
                  new SqlOutParameter("out_last_name", Types.VARCHAR),
                  new SqlOutParameter("out_birth_date", Types.DATE)
          );
  }


  //  ... 추가적인 메서드
}

두 예제의 실행과 최종 결과는 동일하다. 이 예제는 메타데이터에 의존하지 않고 모든 것을 명시적으로 지정했다.


13.5.7 SqlParameters를 지정하는 방법
Section 13.6, “자바객체처럼 JDBC 작업 모델링하기”에서 다뤘던 SimpleJdbc 클래스와 RDBMS 작업(operation) 클래스에 파라미터를 정의하려면 SqlParameter나 SqlParameter의 하위클래스를 사용해라. 일반적으로 생성자에 파라미터 명과 SQL 유형(type)를 지정한다. SQL 유형는 java.sql.Types 상수로 지정한다. 이미 다음과 같은 선언문을 보았다.

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),

SqlParameter의 첫 라인은 IN 파라미터를 선언한다. IN 파라미터는 저장 프로시저 호출과 SqlQuery를 사용하는 쿼리에 모두 사용할 수 있다. SqlQuery의 하위 클래스는 다음 섹션에서 다룬다.

SqlOutParameter가 있는 두번째 라인은 저장 프로시저 호출에 사용하는 out 파라미터를 선언한다. 프로시저에 IN 값을 제공하고 값을 반환하는 InOut 파라미터에 대한 SqlInOutParameter 도 있다.

Note
SqlParameter 와 SqlInOutParameter처럼 유일하게 선언된 파라미터는 입력값을 제공하는데 사용한다. 이 부분이 StoredProcedure 클래스와 다른 점이다. 하위 호환성때문에 SqlOutParameter로 선언한 파라미터에 제공하는 입력값을 허용한다.

IN 파라미터에 이름과 SQL 유형외에 숫자 데이터의 진법(scale)나 커스텀 데이터베이스 유형의 이름을 지정할 수 있다. out 파라미터에 REF 커서가 반환한 열(row)의 매핑을 다루는 RowMapper를 제공할 수 있다. 또 다른 옵션은 반환값을 조작을 커스터마이징할 수 있는 SqlReturnType를 지정하는 것이다.


13.5.8 SimpleJdbcCall를 사용해서 저장 함수(stored function) 호출하기
저 장 프로시저 이름 대신에 함수 이름을 제공한다는 점을 제외하고는 저장 프로시저를 호출한 것과 거의 같은 방법으로 저장 함수를 호출한다. 호출하고자 하는 함수를 나타내는 설정의 일부로 withFunctionName 메서드를 사용하고 함수 호출에 대응하는 문자열이 생성된다. 전문화된 실행 호출인 executeFunction,를 함수를 호출하는데 사용하고 executeFunction,는 지정한 타입의 객체로 값을 반환하는 함수를 반환하므로 리절트맵에서 반환값을 획득하지 않다도 된다. 비슷하게 편리한 메서드인 executeObject도 하나의 out 파라미터만 가진 저장 프로시저에 사용할 수 있다. 다음 예제는 액터의 전체이름을 반환하는 get_actor_name이라는 저장 함수에 기반하고 있다. 다음은 이 함수의 MySQL 버전이다.

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA 
BEGIN
  DECLARE out_name VARCHAR(200);
  SELECT concat(first_name, ' ', last_name)
    INTO out_name
    FROM t_actor where id = in_id;
  RETURN out_name;
END;

이 함수를 호출하려면 초기화 메서드에서 다시 SimpleJdbcCall를 생성해야 한다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcCall funcGetActorName;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    jdbcTemplate.setResultsMapCaseInsensitive(true);
    this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
            .withFunctionName("get_actor_name");
  }

  public String getActorName(Long id) {
    SqlParameterSource in = new MapSqlParameterSource().addValue("in_id", id); 
    String name = funcGetActorName.executeFunction(String.class, in);
    return name;
  }

  //  ... 추가적인 메서드
}

사용한 실행 메서드는 함수 호출의 반환값을 담고 있는 String을 반환한다.


13.5.9 SimpleJdbcCall에서 ResultSet/REF 커서 반환하기
리 절트셋을 반환하는 저장 프로시저나 저장 함수의 호출은 약간 복잡하다. 다른 데이터베이스들이 특정 타입의 out 파라미터를 명시적으로 등록해야 해야하는 반면 JDBC가 결과를 처리하는 동안 몇몇 데이터베이스는 리절트 셋을 반환한다. 두 접근은 모두 리절트 셋을 순회하고 반환된 열을 처리하기 위해 추가적인 처리과정을 필요로 한다. SimpleJdbcCall에서 returningResultSet 메서드를 사용하고 특정 파라미터에 사용할 RowMapper 구현체를 선언한다. 결과 처리과정 중 반환하는 리절트 맵의 경우 이름이 정의되어 있지 않으므로 반환된 결과는 RowMapper 구현체에 선언한 순서와 일치해야 한다. 실행 스테이트먼트가 반환한 리절트 맵에서 처리된 결과 목록을 저장하려고 지정한 이름을 계속 사용한다.

다음 예제는 IN 파리미터를 받지 않고 t_actor 테이블의 모든 열을 반환하는 저장 프로시저를 사용한다. 다음은 이 프로시저의 MySQL 버전이다.

CREATE PROCEDURE read_all_actors()
BEGIN
 SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;

이 프로시저를 호출하기 위해 RowMapper를 선언한다. 매핑할 클래스가 JavaBean 규칙을 따르므로 newInstance 메서드에 매핑해야 하는 클래스에 전달함으로써 생성하는 ParameterizedBeanPropertyRowMapper를 사용할 수 있다.

public class JdbcActorDao implements ActorDao {
  private SimpleJdbcTemplate simpleJdbcTemplate;
  private SimpleJdbcCall procReadAllActors;

  public void setDataSource(DataSource dataSource) {
    this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    jdbcTemplate.setResultsMapCaseInsensitive(true);
    this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
        .withProcedureName("read_all_actors")
        .returningResultSet("actors", ParameterizedBeanPropertyRowMapper.newInstance(Actor.class));
  }

  public List getActorsList() {
    Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
    return (List) m.get("actors");
  }

  //  ... 추가적인 메서드
}

이 호출은 어떤 파라미터도 받지 않으므로 비어있는 Map에 실행호출을 전달한다. 리절트맵에서 Actor의 목록을 얻어서 호출자(caller)에게 반환한다.


13.6 자바객체처럼 JDBC 작업 모델링하기
org.springframework.jdbc.object 패키지는 좀 더 객체지향적인 방법으로 데이터베이스에 접근하도록 하는 클래스들을 포함하고 있다. 예제처럼 쿼리를 실행하고 관계형 컬럼 데이터가 비즈니스 객체의 프로퍼티로 매핑된 비즈니스 객체의 리스트를 결과로 받을 수 있다. 저장 프로시저뿐만 아니라 update, delete, insert문을 실행할 수 있다.

Note
수 많은 스프링 개발자들은 아래에 나열한 여러가지 RDBMS 작업 클래스들(StoredProcedure의 예외와 함께)을 직접 JdbcTemplate을 호출해서 대체할 수 있다고 생각한다. (완전한 클래스로 쿼리를 은닉화하는 것과는 반대로)직접 JdbcTemplate의 메서드를 호출하는 DAO 메서드를 작성하는 것이 때로는 더 간단하다.

하지만 RDBMS 작업 클래스를 사용해서 예측할 수 있는 값을 얻어온다면 계속해서 이러한 클래스들을 사용해라.



13.6.1 SqlQuery
SqlQuery 는 재사용가능하고 스레드세이프한 클래스로 SQL 쿼리를 은릭화한다. 하위클래스들은 쿼리를 수행하면서 생성된 ResultSet을 순회해서 얻은 열마다 하나의 객체를 생성할 수 있는 RowMapper 인스턴스를 제공하는 newRowMapper(..) 메서드를 구현해야 한다. MappingSqlQuery 하위클래스가 자바 클래스에 열(row)을 매핑하기에 훨씬 더 편리한 구현체이므로 SqlQuery를 직접 사용하는 경우는 흔치않다. SqlQuery를 확장하는 다른 구현체들은 MappingSqlQueryWithParameters와 UpdatableSqlQuery이다.


13.6.2 MappingSqlQuery
MappingSqlQuery 는 제공받은 ResultSet의 각 열을 지정한 타입의 객체로 변환하는 추상 mapRow(..) 메서드를 구현해야하는 하위클래스의 재사용할 수 있는 쿼리이다. 다음 예제는 t_actor의 데이터를 Actor 클래스의 인스턴스로 매핑하는 커스텀 쿼리를 보여준다.

public class ActorMappingQuery extends MappingSqlQuery<Actor> {

  public ActorMappingQuery(DataSource ds) {
    super(ds, "select id, first_name, last_name from t_actor where id = ?");
    super.declareParameter(new SqlParameter("id", Types.INTEGER));
    compile();
  }

  @Override
  protected Actor mapRow(ResultSet rs, int rowNumber) 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;
  }
}

이 클래스는 Actor 타입을 파라미터로 가진 MappingSqlQuery를 확장한다. 이 커스텀 쿼리의 생성자는 유일한 파라미터로 DataSource를 받는다. 이 생성자에서 DataSource와 이 쿼리로 데이터를 조회하기위해 실행되어야 하는 SQL로 수퍼클래스의 생성자를 호출한다. PreparedStatement를 생성하는데 이 SQL을 사용할 것이므로 실행중에 전달되는 파라미터에 대한 플레이스홀더를 포함할 것이다. SqlParameter에 전달되는 declareParameter 메서드를 사용해서 각 파라미터를 선언해야 한다. SqlParameter는 이름과 java.sql.Types로 정의된 JDBC 타입을 받는다. 모든 파라미터를 정의했으면 compile() 메서드를 호출해서 스테이트먼트를 준비하고 나중에 실행될 수 있게 한다. 이 클래스는 컴파일된 후에는 스레드세이프하므로 DAO가 초기화되었을 때 이러한 인스턴스들이 생성되는 한 인스턴스 변수들은 유지가 되고 재사용될 수 있다.

private ActorMappingQuery actorMappingQuery;

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

public Customer getCustomer(Long id) {
  return actorMappingQuery.findObject(id);
}

이 예제의 메서드는 유일한 파라미터로 전달된 고객의 id를 얻는다. 하나의 객체만 얻기 원하므로 편리한 메서드인 findObject를 id 파라미터와 함께 호출한다. 대신 객체의 리스트를 반환하는 쿼리가 있고 추가적인 메서드를 받는다면 가변인자로 파라미터 값의 배열을 받는 실행 메서드 중의 하나를 사용할 것이다.

public List<Actor> searchForActors(int age, String namePattern) {
  List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
  return actors;
}


13.6.3 SqlUpdate
SqlUpdate 클래스는 SQL update를 감춤다. 쿼리처럼 update 객체는 재사용할 수 있고 모든 RdbmsOperation 클래스들처럼 update도 파라미터를 가질 수 있고 SQL에 정의될 수 있다. 이 클래스는 query 객체의 execute(..)와 유사한 다수의 update(..) 메서드를 재공한다. SQLUpdate 클래스는 구체화된(concrete) 클래스이다. 예를 들어 단순히 execute를 호출하는 다음의 예제처럼 SQLUpdate 클래스는 커스텀 업데이트 메서드를 추가하는 하위클래스가 될 수 있다. 하지만 SQL을 설정하고 파라미터를 선언해서 쉽게 파라미터화할 수 있으므로 SqlUpdate 클래스를 하위클래스로 가지고 있지 않다.

import java.sql.Types;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

  public UpdateCreditRating(DataSource ds) {
    setDataSource(ds);
    setSql("update customer set credit_rating = ? where id = ?");
    declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
    declareParameter(new SqlParameter("id", Types.NUMERIC));
    compile();
  }

  /**
   * @param 업데이트할 Customer의 id
   * @param 신용등급의 새로운 값
   * @return 업데이트된 열의 수
   */
  public int execute(int id, int rating) {
    return update(rating, id);
  }
}


13.6.4 StoredProcedure
StoredProcedure 는 RDBMS 저장 프로시저의 객체 추상화에 대한 수퍼클래스이다. 이 클래스는 abstract이고 StoredProcedure의 다양한 execute(..) 메서드는 protected 접근권한을 가져서 더 제한적인 타입을 제공하는 하위클래스외에는 사용할 수 없게 한다.

상속받은 sql 프로퍼티는 RDBMS의 저장 프로시저의 이름이 될 것이다.

StoredProcedure 클래스에 파라미터를 정의하려면 SqlParameter나 SqlParameter의 하위클래스중 하나를 사용해라. 다음 예제 코드처럼 생성자에 파라미터명과 SQL 유형을 지정해야 한다. SQL 유형은 java.sql.Types 상수를 사용해서 지정한다.

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),

SqlParameter를 사용한 첫 라인은 IN 파라미터를 선언한다. IN 파라미터는 다음 섹션에서 다룰 SqlQuery와 SqlQuery의 하위클래스를 사용한 쿼리와 저장 프로시저 호출 모두에 사용할 수 있다.

SqlOutParameter 가 있는 두번째 라인은 저장 프로시저 호출에 사용하는 out 파라미터를 선언한다. 프로시저의 in 값을 제공하고 프로시저가 반환하는 값도 담고 있는 InOut 파라미터에 대한 SqlInOutParameter도 존재한다.

in 파라미터에서는 이름과 SQL 유형외에도 숫자 데이터의 진법(scale)나 커스텀 데이터베이스 유형의 이름을 지정할 수 있다. out에서는 REF 커서가 반환하는 열의 매핑을 다루는 RowMapper를 제공할 수 있다. 또 다른 옵션으로 반환값의 조작을 커스터마이징할 수 있는 SqlReturnType를 지정할 수 있다.

다음은 오라클 데이터베이스에서 sysdate() 함수를 호출하는 StoredProcedure를 사용하는 간단한 DAO 예제다. 저장 프로시저의 모든 기능을 사용하려면 StoredProcedure를 확장한 클래스를 생성해야 한다. 이 예제에서는 StoredProcedure 클래스가 내부 클래스이지만 StoredProcedure를 재사용해야하면 최상위 클래스로 정의하면 된다. 이 예제에는 입력 파라미터가 없지만 출력 파라미터는 SqlOutParameter 클래스를 사용해서 날짜 타입으로 선언했다. execute() 메서드는 프로시저를 실행하고 받은 Map이 반환한 데이터를 추출한다. 실행해서 얻은 Map은 키로 파라미터의 이름을 사용하고 선언된 각 출력 파라미터에 대한 엔트리를 가진다.(이 경우에는 딱 하나만 있다.)

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class StoredProcedureDao {

  private GetSysdateProcedure getSysdate;
  
  @Autowired
  public void init(DataSource dataSource) {
    this.getSysdate = new GetSysdateProcedure(dataSource);
  }
  
  public Date getSysdate() {
    return getSysdate.execute();
  }

  private class GetSysdateProcedure extends StoredProcedure {
      
    private static final String SQL = "sysdate";

    public GetSysdateProcedure(DataSource dataSource) {
      setDataSource(dataSource);
      setFunction(true);
      setSql(SQL);
      declareParameter(new SqlOutParameter("date", Types.DATE));
      compile();
    }

    public Date execute() {
      // 'sysdate' 저장 프로시저에 입력 파라미터가 없으므로 비어있는 Map을 제공한다...
      Map<String, Object> results = execute(new HashMap<String, Object>());
      Date sysdate = (Date) results.get("date");
      return sysdate;    
    }
  }
}

다음 예제의 StoredProcedure는 2개의 출력 파라미터를 가진다.(이 경우에는 Oracle REF 커서)

import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

  private static final String SPROC_NAME = "AllTitlesAndGenres";

  public TitlesAndGenresStoredProcedure(DataSource dataSource) {
    super(dataSource, SPROC_NAME);
    declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
    declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
    compile();
  }

  public Map<String, Object> execute() {
    // 다시 말하지만 이 저장 프로시저에 입력 파라미터가 없으므로 비어있는 Map을 제공한다
    return super.execute(new HashMap<String, Object>());
  }
}

TitlesAndGenresStoredProcedure 생성자에서 사용되고 있는 declareParameter(..) 메서드를 오버라이드한 여러 메서드들이 어떻게 RowMapper 구현 인스턴스를 전달하는지 봐라. 이는 이미 존재하는 기능을 재사용하는 아주 편리하고 강력한 방법이다. RowMapper의 두 구현체의 코드는 아래에 나와있다.

TitleMapper 클래스는 ResultSet의 각 열을 Title 도메인객체에 매핑한다.

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.foo.domain.Title;

public final class TitleMapper implements RowMapper<Title> {
    
  public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
    Title title = new Title();
    title.setId(rs.getLong("id"));
    title.setName(rs.getString("name"));
    return title;
  }
}

GenreMapper 클래스는 ResultSet의 각 열을 Genre 도메인객체에 매핑한다.

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.foo.domain.Genre;

public final class GenreMapper implements RowMapper<Genre> {
    
  public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
    return new Genre(rs.getString("name"));
  }
}

RDBMS 의 정의에서 하나이상의 입력 파라미터를 가진 저장 프로시저에 파라미터를 전달하기 위해 수퍼클래스의 타입이 없는 execute(Map parameters) 메서드 (protected 접근권한을 가진다)에 위임하는 강타입의 execute(..) 메서드를 작성할 수 있다. 예를 들면 다음과 같다.

import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

  private static final String SPROC_NAME = "TitlesAfterDate";
  private static final String CUTOFF_DATE_PARAM = "cutoffDate";

  public TitlesAfterDateStoredProcedure(DataSource dataSource) {
    super(dataSource, SPROC_NAME);
    declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
    declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
    compile();
  }

  public Map<String, Object> execute(Date cutoffDate) {
    Map<String, Object> inputs = new HashMap<String, Object>();
    inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
    return super.execute(inputs);
  }
}


13.7 파라미터와 데이터 값 조작과 관련된 일반적인 문제들
스프링 프레임워크 JDBC의 다른 접근들 사이에서 파라미터와 데이터의 값과 관련한 일반적인 문제들이 존재한다.


13.7.1 파라미터에 SQL 유형에 관한 정보 제공하기
스프링은 보통 전달된 파라미터의 타입에 기반해서 파라미터의 SQL 유형을 결정한다. 파라미터 값을 설정할 때 사용하는 SQL 유형을 명시적으로 제공할 수 있다. 종종 NULL 값을 제대로 설정해야할 때 필요하다.

여러 가지 방법으로 SQL 유형의 정보를 제공할 수 있다.

  • JdbcTemplate 의 많은 update와 query 메서드들은 int 배열의 형식으로 추가적인 파라미터를 받는다. 이 배열은 java.sql.Types 클래스의 상수값을 사용해서 대응되는 파라미터의 SQL 타입을 가리키는데 사용한다. 각 파라미터마다 하나의 엔트리를 제공한다.
  • 이와 같은 부가적인 정보가 필요한 파라미터 값을 감싸는 SqlParameterValue 클래스를 사용할 수 있다. 각 값마다 새로운 인스턴스를 생성하고 생성자에서 SQL 유형과 파라미터 값을 전달한다. 숫자값의 진법(scale) 파라미터를 제공할 수도 있다.
  • 이름있는 파라미터로 동작하는 메서드에서는 SqlParameterSource계열의 BeanPropertySqlParameterSource, MapSqlParameterSource를 사용해라. 둘 다 이름있는 파라미터 값에 SQL 유형을 등록하는 메서드를 가진다.


13.7.2 BLOB과 CLOB 객체 다루기
이 미지나 다른 바이너리 객체들, 대량의 문자를 저장할 수 있다. 이처럼 큰 객체가 바이너리 데이터이면 BLOB이라고 하고 문자 데이터이면 CLOB이라고 부른다. 스프링에서 JdbcTemplate을 직접 사용하거나 RDBMS 객체와 SimpleJdbc 클래스의 더 높은 추상화를 사용하는 경우에 이와 같은 큰 객체들을 다룰 수 있다. 이러한 모든 접근방법들은 LOB 데이터의 실제 관리에 LobHandler 인터페이스의 구현체를 사용한다. LobHandler는 getLobCreator 메서드로 추가할 새로운 LOB 객체를 생성하는데 사용하는 LobCreator 클래스에 접근한다.

LobCreator/LobHandler는 LOB 입력과 출력에 다음과 같은 지원을 제공한다.

  • BLOB
    • byte[] - getBlobAsBytes와 setBlobAsBytes
    • InputStream - getBlobAsBinaryStream와 setBlobAsBinaryStream
  • CLOB
    • String - getClobAsString와 setClobAsString
    • InputStream - getClobAsAsciiStream와 setClobAsAsciiStream
    • Reader - getClobAsCharacterStream와 setClobAsCharacterStream
다음 예제는 BLOB를 생성하고 삽입하는 방법을 보여준다. 뒤에서 데이터베이스에서 BLOB을 다시 읽어들이는 방법을 살펴볼 것이다.

이 예제는 JdbcTemplate와 AbstractLobCreatingPreparedStatementCallback의 구현체를 사용한다. 이 예제는 setValues 메서드를 구현했고 setValues 메서드는 SQL 인서트문에서 LOB 컬럼에 값을 설정하는데 사용하는 LobCreator를 제공한다.

이 예제에서는 이미 DefaultLobHandler 인스턴스에 설정된 lobHandler 변수가 존재한다고 가정한다. 보통은 의존성 주입으로 이 값을 설정한다.

final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute("INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
  new AbstractLobCreatingPreparedStatementCallback(lobHandler) {    // 1
    protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
      ps.setLong(1, 1L);
      lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());     // 2
      lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());      // 3
    }
  }
);
blobIs.close();
clobReader.close();

1 이 예제에서는 평범한 DefaultLobHandler인 lobHandler에 전달한다.
2 setClobAsCharacterStream 메서드를 사용해서 CLOB의 내용에 전달한다.
3 setBlobAsBinaryStream 메서드를 사용해서 BLOB의 내용에 전달한다.

이제 데이터베이스에서 LOB 데이터를 읽을 차례다. 다시 얘기하지만 같은 인스턴스 변수 lobHandler 와 DefaultLobHandler에 대한 참조를 가진 JdbcTemplate를 사용한다.

List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
  new RowMapper<Map<String, Object>>() {
    public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
      Map<String, Object> results = new HashMap<String, Object>();
      String clobText = lobHandler.getClobAsString(rs, "a_clob");      // 1
      results.put("CLOB", clobText);
      byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");      // 2
      results.put("BLOB", blobBytes);
      return results;
    }
  });

1 getClobAsString 메서드를 사용해서 CLOB의 내용을 얻는다.
2 getBlobAsBytes 메서드를 사용해서 BLOB의 내용을 얻는다.


13.7.3 IN절에 값 리스트 전달하기
SQL 표준에서는 변경될 수 있는 값의 목록을 포함하는 표현식에 기반해서 조회할 수 있다. 대표적인 예제는 select * from T_ACTOR where id in (1, 2, 3)이다. JDBC 표준의 프리페어드 스테이트먼트는 이 변경가능한 목록을 직접 지원하지 않는다. 달라질 수 있는 수의 플레이스홀더를 선언할 수 없다. 필요한 수의 플레이스홀더를 알고 있다면 원하는 수의 플레이스홀더로 다수를 준비하거나 SQL 문자열을 동적으로 만들어야 한다. NamedParameterJdbcTemplate와 SimpleJdbcTemplate가 지원하는 이름있는 파라미터는 후자의 접근을 취한다. 프리미티브 객체의 java.util.List로 값들을 전달해라. 이 리스트는 필요한 플레이스홀더를 추가하는데 사용할 것이고 스테이트먼트 실행하면서 값을 전달할 것이다.

Note
많은 값을 전달할때는 조심해라. JDBC 표준은 in 표현식에 100개 이상의 값을 사용하는 것을 보장하지 않는다. 다수의 데이터베이스가 이 값이상을 지원하지만 보통 허용하는 제한값이 존재한다. 오라클은 1000개로 제한한다.

값 의 리스트에서 프리미티브 값과 관련해서 추가적으로 객체 배열의 java.util.List를 생성할 수 있다. 이 리스트는 select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))같은 in절에 정의한 여러 표현식을 지원한다. 물론 이는 데이터베이스가 이 문법을 지원해야 한다.


13.7.4 저장 프로시저 호출에서 복잡한 타입 다루기
저 장 프로시저를 호출할 때 때로는 데이터베이스 특유의 복잡한 타입을 사용할 수 있다. 이러한 타입을 지원하려고 스프링은 저장 프로시저가 반환한 타입을 다루는 SqlReturnType와 저장 프로시저에 파리미터로 전달하기 위한 SqlTypeValue를 제공한다.

다음은 사용자 정의 타입인 ITEM_TYPE의 오라클 STRUCT 객체 값을 반환하는 예제이다. SqlReturnType 인터페이스는 반드시 구현해야하는 getTypeValue라는 하나의 메서드를 가진다. 이 인터페이스를 SqlOutParameter 선언의 일부로 사용한다.

final TestItem - new TestItem(123L, "A test item", new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"););

declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
  new SqlReturnType() {
    public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String typeName) throws SQLException {
      STRUCT struct = (STRUCT)cs.getObject(colIndx);
      Object[] attr = struct.getAttributes();
      TestItem item = new TestItem();
      item.setId(((Number) attr[0]).longValue());
      item.setDescription((String)attr[1]);
      item.setExpirationDate((java.util.Date)attr[2]);
      return item;
    }
  }));

저 장 프로시저에 TestItem같은 자바 객체의 값을 전달할 때 SqlTypeValue를 사용한다. SqlTypeValue 인터페이스는 반드시 구현해야 하는 createTypeValue라는 하나의 메서드를 가진다. 이 메서드에 활성화된 연결을 전달하고 다음 예제에 나오는 StructDescriptor나 ArrayDescriptor같은 데이터베이스에 특화된 객체를 생성할 때 사용할 수 있다.

final TestItem - new TestItem(123L, "A test item", new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"););

SqlTypeValue value = new AbstractSqlTypeValue() {
  protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
    StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
    Struct item = new STRUCT(itemDescriptor, conn,
      new Object[] {
        testItem.getId(),
        testItem.getDescription(),
        new java.sql.Date(testItem.getExpirationDate().getTime())
      });
    return item;
  }
};

이제 이 SqlTypeValue를 저장 프로시저를 호출을 실행하는 입력 파라미터를 담고 있는 Map에 추가할 수 있다.

SqlTypeValue 의 또 다른 사용방법은 오라클 저장 프로시저에 값의 배열을 전달하는 것이다. 오라클은 이러한 경우에 사용해야하는 내부 ARRAY 클래스를 가지고 있고 오라클 ARRAY 인스턴스를 생성하는데 SqlTypeValue를 사용할 수 있고 자바 ARRAY의 값을 가진다.

final Long[] ids = new Long[] {1L, 2L};

SqlTypeValue value = new AbstractSqlTypeValue() {
  protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
    ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
    ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
    return idArray;
  }
};


13.8 내장 데이터베이스 지원
org.springframework.jdbc.datasource.embedded 패키지는 자바의 내장 데이터베이스 엔진을 지원한다.HSQL, H2, Derby를 네이티브하게 지원한다. 새로운 내장 데이터베이스 유형과 DataSource 구현체에 추가하는 확장가능한 API를 사용할 수도 있다.


13.8.1 왜 내장 데이터베이스를 사용하는가?
내장 데이터베이스가 가볍다는(lightweight) 특성때문에 프로젝트의 개발단계에서 내장 데이터베이스는 유용한다. 설정이 쉽고 빠르게 시작할 수 있고 테스트가능하며 개발중에 SQL을 빠르게 발전시킬 수 있다는 장점이 있다.


13.8.2 스프링 XML을 사용하는 내장 데이터베이스의 인스턴스 생성하기
스프링 ApplicationContext의 빈처럼 내장 데이터베이스 인스턴스를 노출하고자 하면 spring-jdbc 네임스페이스에 embedded-database 태그를 사용해라.

<jdbc:embedded-database id="dataSource">
  <jdbc:script location="classpath:schema.sql"/>
  <jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>

앞 의 설정은 클래스패스의 schema.sql와 testdata.sql 리소스의 SQL로 내장 HSQL 데이터베이스를 생성한다. 데이터베이스 인스턴스는 javax.sql.DataSource타입의 빈으로 스프링 컨테이너에서 사용할 수 있다. 이 빈을 필요할 때 데이터접근 객체에 주입할 수 있다.


13.8.3 프로그래밍적으로 내장 데이터베이스 인스턴스 생성하기
EmbeddedDatabaseBuilder 클래스는 프로그래밍적으로 내장 데이터베이스를 생성하는 유연한 API를 제공한다. 데이터접근 객체 유닛테스트같은 독립적인 환경에서 내장 데이터베이스 인스턴스를 생성해야할 때 이 API를 사용해라.

EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase db = builder.setType(H2).addScript("my-schema.sql").addScript("my-test-data.sql").build();
// 디비에 대한 작업을 수행한다 (EmbeddedDatabase는 javax.sql.DataSource를 확장한다)
db.shutdown()


13.8.4 내장 데이터베이스 지원 확장하기
스프링 JDBC 내장 데이터베이스지원은 두가지 방법으로 확장할 수 있다.

Apache Derby같은 새로운 내장 데이터베이스 유형을 지원하려면 EmbeddedDatabaseConfigurer를 구현해라.
내장 데이터베이스 연결을 관리하는 커넥션풀같은 새로운 DataSource 구현체를 지원하려면 DataSourceFactory를 구현해라.

jira.springframework.org에서 스프링 커뮤니티에 확장한 부분을 공헌해 주길 바란다.


13.8.5 HSQL의 사용
스 프링은 HSQL 1.8.0 이상의 버전을 지원한다. 명시적으로 유형을 지정하지 않으면 HSQL이 기본 내장 데이터베이스다. HSQL을 명시적으로 지정하려면 embedded-database 태그의 type 속성을 HSQL로 설정해라. 빌더 API를 사용한다면 EmbeddedDatabaseType.HSQL로 setType(EmbeddedDatabaseType) 메서드를 호출해라.


13.8.6 H2의 사용
스프링은 H2도 잘 지원한다. H2를 활성화 하려면 embedded-database 태그의 type 속성을 H2로 설정해라. 빌더 API를 사용한다면 EmbeddedDatabaseType.H2로 setType(EmbeddedDatabaseType) 메서드를 호출해라.


13.8.7 Derby의 사용
스 프링은 Apache Derby 10.5 이상의 버전도 지원한다. Derby를 활성화하려면 embedded-database 태그의 type 속성을 Derby로 설정해라. 빌더 API를 사용한다면 EmbeddedDatabaseType.Derby로 setType(EmbeddedDatabaseType) 메서드를 호출해라.


13.8.8 내장 데이터베이스로 데이터접근 로직 테스트하기
내장 데이터베이스는 데이터접근 코드를 테스트하는 쉬운 방법을 제공한다. 다음은 내장 데이터베이스를 사용하는 데이터접근 유닛 테스트의 템플릿이다.

public class DataAccessUnitTestTemplate {
  private EmbeddedDatabase db;
  
  @Before
  public void setUp() {
    // 기본 스크립트인 classpath:schema.sql와 classpath:test-data.sql로 HSQL 인메모리(in-memory) 디비를 생성한다
    db = new EmbeddedDatabaseBuilder().addDefaultScripts().build();        
  }

  @Test
  public void testDataAccess() {
    JdbcTemplate template = new JdbcTemplate(db);
    template.query(...);
  }

  @After
  public void tearDown() {
    db.shutdown();
  }
}


13.9 DataSource 초기화
org.springframework.jdbc.datasource.init 패키지는 존재하는 DataSource 초기화를 지원한다. 내장 데이터베이스 지원은 어플리케이션의 DataSource의 생성과 초기화에 대한 한가지 선택사항을 제공하지만 때로는 동작하는 서버 어디선가 인스턴스를 초기화해야할 필요가 있다.


13.9.1 스프링 XML을 사용해서 데이터베이스 인스턴스 초기화하기
데이터베이스를 초기화하고자 하고 DataSource 빈에 대한 참조를 제공할 수 있다면 spring-jdbc 네임스페이스의 initialize-database 태그를 사용해라.

<jdbc:initialize-database data-source="dataSource">
  <jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
  <jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>

위 의 예제는 데이터베이스에 지정한 두 스크립트를 실행한다. 첫 스크립트는 스키마를 생성하고 두번째 스크립트는 테스트 데이터를 추가한다. 스크립트의 위치도 스프링에서 리소스에 사용하는 일반적인 ant 방식으로 와일드카드 패턴을 사용할 수 있다.(예시: classpath*:/com/foo/**/sql/*-data.sql) 패턴을 사용한다면 URL이나 파일명의 사전순으로 스크립트를 실행한다.

데이터베이스 초기자(initializer)의 기본 동작은 제공된 스크립트를 무조건 실행하는 것이다. 이 동작이 항상 우리가 원하는 동작은 아닌데 예를 들어 이미 테스트 데이터가 있고 동작하고 있는 데이터베이스가 있는 경우에는 실행할 필요가 없다. 테이블을 먼저 생성하고 데이터를 넣는 가장 일반적인 패턴(위에 보여줬듯이)으로 의도치않게 데이터를 삭제할 가능성을 줄일 수 있다. 테이블이 이미 존재한다면 테이블을 생성하는 첫번째 과정이 실패할 것이다.

하지만 존재하는 데이터의 생성과 삭제를 더 세밀하게 제어하려면 XML 네임스페이스에서 다수의 옵션을 제공한다. 첫 플래그는 초기화를 키고 끌 수 있다. 이 옵션은 환경에 따라서 다르게 설정할 수 있다.(예시. 시스템 프로퍼티나 환경 빈에서 값을 가져온다) 예시.

<jdbc:initialize-database data-source="dataSource" enabled="#{systemProperties.INITIALIZE_DATABASE}">
  <jdbc:script location="..."/>
</jdbc:initialize-database>

데 이터가 존재하고 있을때의 동작을 제어하는 두번째 옵션은 좀 더 장애를 허용한다(tolerant of failures). 이 옵션으로 스크립트를 실행할 때 특정 SQL 오류를 무시하도록 초기자(initializer)의 기능을 제어할 수 있다. 다음은 그 예시이다.

<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
  <jdbc:script location="..."/>
</jdbc:initialize-database>

이 예제에서는 비어있는 데이터베이스에 스크립트를 실행하고 스크립트에 DROP문이 있다면 실패할 것이다. 그래서 SQL DROP문의 실패는 무시하지만 다른 실패는 예외를 발생시킬 것이다. 이는 사용하는 SQL 방언이 DROP ... IF EXISTS(또는 비슷한)을 지원하지 않지만 테스트데이터를 다시 생성하기 전에 무조건 모두 삭제하길 원하는 경우 유용하다. 이러한 경우에 첫 스크립트는 보통 drop에 관한 것이고 이어서 CREATE문들이 온다.

ignore-failures 옵션을 NONE (기본값), DROPS (실패한 drop은 무시한다), ALL(모든 실패를 무시한다)으로 설정할 수 있다.

XML 네임스페이스로 설정하는 것보다 더 정밀하게 제어해야 한다면 DataSourceInitializer를 어플리케이션의 컴포넌트로 정의해서 직접 사용할 수 있다.


13.9.1.1 데이터베이스에 기반한 다른 컴포넌트들의 초기화
어 플리케이션에서 커다란 클래스는 더 이상 복잡해지지 않게 데이터베이스 초기화를 사용할 수 있다. 이러한 클래스들은 스프링 컨텍스트가 시작될 때까지 데이터베이스를 사용하지 않는다. 자신의 어플리케이션이 이러한 경우가 아니라면 이 섹션의 나머지 부분을 읽어야 할 것이다.

데이터베이스 초기화는 데이터 소스 인스턴스에 의존하고 초기화 콜백에서 제공된 스크립트를 실행한다. (XML 빈 정의의 init-method나 InitializingBean를 참고해라.) 다른 빈들이 같은 데이터 소스에 의존하고 초기화 콜백에서 데이터소스를 사용하고 있다면 데이터가 아직 초기화되지 않았기 때문에 문제가 될 수 있다. 이 문제의 일반적인 예시가 어플리케이션 시작시에 데이터베이스의 데이터를 초기화하고 로딩하는 캐시이다.

이 문제를 다루려면 두가지 선택사항이 있다. 캐시 초기화 전략을 나중의 단계로 바꾸거나 데이터베이스 초기자가 먼저 초기된다는 것을 보장해라.

어플리케이션을 제어하고 있다면 첫번째 방법이 쉽고 그렇지 않다면 어렵다. 이것을 구현하는 몇 가지 방법이 있다.

  • 최소 사용시 캐시 초기화가 지연되도록 한다.(어플리케이션 구동시간이 길어진다.)
  • 캐 시나 캐시를 초기화하는 분리된 컴포넌트가 Lifecycle나 SmartLifecycle를 구현한다. 어플리케이션 컨텍스트가 시작할 때 SmartLifecycle의 autoStartup 플래그가 설정되어 있다면 SmartLifecycle을 자동적으로 시작할 것이고 감싸진 컨텍스트에서 ConfigurableApplicationContext.start()를 호출해서 수동으로 Lifecycle를 시작할 수 있다.
  • 스프링 ApplicationEvent나 유사하게 캐시 초기화를 일으키는 커스텀 옵저버 메카니즘을 사용해라. ContextRefreshedEvent는 사용할 준비가 되었을 때(모든 빈이 초기화 된 후에) 컨텍스트가 항상 발행하므로 종종 유용한 훅(hook)이 된다.(이것이 기본적으로 SmartLifecycle이 동작하는 방법이다.)
두번째 방법도 쉬울 수 있다. 두번째 방법을 구현하는 몇가지 방법들이 있다.

  • 등 록순서로 빈을 초기화하는 스프링 BeanFactory 기본동작에 의존한다. 어플리케이션 모듈의 순서인 <import/> 요소들의 일반적인 사용방법으로 데이터베이스와 데이터베이스 초기화가 목록에서 먼저나오도록 함으로써 쉽게 배치할 수 있다.
  • 데 이터소스와 데이터소스를 사용하는 비즈니스 컴포넌트들을 분리하고 이 둘을 분리된 어플리케이션 인스턴스에 두고 시작 순서를 제어해라. (예시. 부모가 데이터소스를 가지고 있고 자식이 비즈니스 컴포넌트를 가진다.) 이 구조는 스프링 웹어플리케이션에서는 일반적이지만 더 범용적으로 적용할 수 있다.
  • SpringSource dm Server처럼 모듈화된 런타임을 사용하고 데이터소스와 데이터소스에 의존하는 컴포넌트들을 분리해라. 예를 들면 데이터소스 -> 초기자 -> 비즈니스 컴포넌트와 같은 시작순서로 지정해라.

2012/12/27 03:54 2012/12/27 03:54