이번글은 공식 문서에서 소개하는 Modeling JDBC Operations as Java Objects에 대해 정리했습니다.
The org.springframework.jdbc.object package contains classes that let you access the database in a more object-oriented manner. As an example, you can run queries and get the results back as a list that contains business objects with the relational column data mapped to the properties of the business object. You can also run stored procedures and run update, delete, and insert statements.
org.springframework.jdbc.object 패키지는 데이터베이스에 보다 객체지향적인 방식으로 접근할 수 있도록 해주는 클래스들을 포함하고 있습니다. 예를 들어, 쿼리를 실행하고 그 결과를 비즈니스 객체들을 담고 있는 리스트로 받아올 수 있으며, 이 객체들의 속성에는 관계형 데이터베이스의 열(column) 데이터가 매핑됩니다. 또한, 저장 프로시저(stored procedure)를 실행하거나 update, delete, insert 문장들을 실행할 수도 있습니다.
Many Spring developers believe that the various RDBMS operation classes described below (with the exception of the StoredProcedure class) can often be replaced with straight JdbcTemplate calls. Often, it is simpler to write a DAO method that calls a method on a JdbcTemplate directly (as opposed to encapsulating a query as a full-blown class).
However, if you are getting measurable value from using the RDBMS operation classes, you should continue to use these classes.
많은 Spring 개발자들은 (StoredProcedure 클래스를 제외하고) 아래에 설명된 다양한 RDBMS 작업 클래스들이 종종 JdbcTemplate 호출을 직접 사용하는 방식으로 대체될 수 있다고 믿습니다. 종종, 쿼리를 하나의 완전한 클래스로 캡슐화하는 것보다, JdbcTemplate의 메서드를 직접 호출하는 DAO 메서드를 작성하는 것이 더 간단합니다.
그러나, RDBMS 작업 클래스들을 사용하는 것이 실질적인 가치를 준다고 느낀다면, 계속해서 이러한 클래스들을 사용하는 것이 좋습니다.
Understanding SqlQuery
SqlQuery is a reusable, thread-safe class that encapsulates an SQL query. Subclasses must implement the newRowMapper(..) method to provide a RowMapper instance that can create one object per row obtained from iterating over the ResultSet that is created during the execution of the query. The SqlQuery class is rarely used directly, because the MappingSqlQuery subclass provides a much more convenient implementation for mapping rows to Java classes. Other implementations that extend SqlQuery are MappingSqlQueryWithParameters and UpdatableSqlQuery.
SqlQuery는 SQL 쿼리를 캡슐화한 재사용 가능하고, 스레드에 안전한 클래스입니다.
하위 클래스는 반드시 newRowMapper(..) 메서드를 구현해야 하며, 이 메서드는 쿼리 실행 중 생성된 ResultSet을 반복(iterate)하면서
각 행(row)마다 하나의 객체를 생성할 수 있는 RowMapper 인스턴스를 제공해야 합니다.
SqlQuery 클래스는 직접 사용되는 경우는 드뭅니다, 왜냐하면 하위 클래스인 MappingSqlQuery가 행(row)을 Java 클래스에 매핑하는 훨씬 더 편리한 구현체를 제공하기 때문입니다.
SqlQuery를 확장한 다른 구현체들로는 MappingSqlQueryWithParameters와 UpdatableSqlQuery가 있습니다.
Using MappingSqlQuery
MappingSqlQuery is a reusable query in which concrete subclasses must implement the abstract mapRow(..) method to convert each row of the supplied ResultSet into an object of the type specified. The following example shows a custom query that maps the data from the t_actor relation to an instance of the Actor class:
MappingSqlQuery는 재사용 가능한 쿼리이며, 구체적인 하위 클래스는 반드시 추상 메서드인 mapRow(..)를 구현해야 합니다.
이 메서드는 제공된 ResultSet의 각 행(row)을 지정된 타입의 객체로 변환합니다.
다음 예시는 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 = ?");
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;
}
}
The class extends MappingSqlQuery parameterized with the Actor type. The constructor for this customer query takes a DataSource as the only parameter. In this constructor, you can call the constructor on the superclass with the DataSource and the SQL that should be run to retrieve the rows for this query. This SQL is used to create a PreparedStatement, so it may contain placeholders for any parameters to be passed in during execution. You must declare each parameter by using the declareParameter method passing in an SqlParameter. The SqlParameter takes a name, and the JDBC type as defined in java.sql.Types. After you define all parameters, you can call the compile() method so that the statement can be prepared and later run. This class is thread-safe after it is compiled, so, as long as these instances are created when the DAO is initialized, they can be kept as instance variables and be reused. The following example shows how to define such a class:
이 클래스는 Actor 타입으로 매개변수화된 MappingSqlQuery를 상속합니다. 이 사용자 정의 쿼리의 생성자는 DataSource 하나만을 매개변수로 받습니다.
이 생성자 안에서는, 상위 클래스의 생성자를 호출하여 이 쿼리로부터 행(row)들을 가져오기 위해 실행할 SQL문과 함께
DataSource를 전달할 수 있습니다.
이 SQL문은 PreparedStatement를 생성하는 데 사용되며, 실행 중에 전달될 매개변수를 위한 플레이스홀더(물음표 등)를 포함할 수 있습니다.
각 매개변수는 declareParameter 메서드를 사용하여 선언해야 하며, 이때 SqlParameter 인스턴스를 전달합니다. SqlParameter는 이름(name)과 java.sql.Types에 정의된 JDBC 타입을 받습니다.
모든 매개변수를 정의한 후에는, compile() 메서드를 호출하여 해당 SQL문을 준비할 수 있습니다.
이 클래스는 컴파일된 이후에는 스레드에 안전하므로,DAO가 초기화될 때 이러한 인스턴스들이 생성된다면, 인스턴스 변수로 보관하고 재사용할 수 있습니다.
다음 예시는 이러한 클래스를 어떻게 정의하는지를 보여줍니다.
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Actor getActor(Long id) {
return actorMappingQuery.findObject(id);
}
The method in the preceding example retrieves the actor with the id that is passed in as the only parameter. Since we want only one object to be returned, we call the findObject convenience method with the id as the parameter. If we had instead a query that returned a list of objects and took additional parameters, we would use one of the execute methods that takes an array of parameter values passed in as varargs. The following example shows such a method
앞선 예제의 메서드는 전달된 ID를 유일한 매개변수로 사용하여 해당 ID를 가진 actor를 조회합니다.
우리는 오직 하나의 객체만 반환되기를 원하기 때문에, ID를 매개변수로 하여 findObject라는 편의 메서드를 호출합니다.
반면에, 객체의 리스트를 반환하고, 추가적인 매개변수를 받는 쿼리가 있다면, varargs(가변 인자)로 전달된 매개변수 값들의 배열을 인자로 받는 execute 메서드 중 하나를 사용해야 합니다.
다음 예제는 이러한 메서드를 보여줍니다.
public List<Actor> searchForActors(int age, String namePattern) {
return actorSearchMappingQuery.execute(age, namePattern);
}
Using SqlUpdate
The SqlUpdate class encapsulates an SQL update. As with a query, an update object is reusable, and, as with all RdbmsOperation classes, an update can have parameters and is defined in SQL. This class provides a number of update(..) methods analogous to the execute(..) methods of query objects. The SqlUpdate class is concrete. It can be subclassed — for example, to add a custom update method. However, you do not have to subclass the SqlUpdate class, since it can easily be parameterized by setting SQL and declaring parameters. The following example creates a custom update method named execute:
SqlUpdate 클래스는 하나의 SQL 업데이트문을 캡슐화합니다.
쿼리와 마찬가지로, 업데이트 객체도 재사용 가능하며, 모든 RdbmsOperation 클래스들과 마찬가지로,
업데이트는 매개변수를 가질 수 있고 SQL로 정의됩니다.
이 클래스는 쿼리 객체의 execute(..) 메서드들과 유사한 여러 개의 update(..) 메서드를 제공합니다.
SqlUpdate 클래스는 구체 클래스(concrete class)입니다.
필요하다면 이 클래스를 상속(subclass)할 수 있습니다.
예를 들어, 사용자 정의 update 메서드를 추가하기 위해 상속할 수 있습니다.
그러나 굳이 이 클래스를 상속하지 않아도 됩니다.
SQL을 설정하고 매개변수를 선언함으로써 쉽게 파라미터화할 수 있기 때문입니다.
다음 예제는 execute라는 이름의 사용자 정의 업데이트 메서드를 생성하는 예시를 보여줍니다.
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 id for the Customer to be updated
* @param rating the new value for credit rating
* @return number of rows updated
*/
public int execute(int id, int rating) {
return update(rating, id);
}
}
Using StoredProcedure
The StoredProcedure class is an abstract superclass for object abstractions of RDBMS stored procedures.
The inherited sql property is the name of the stored procedure in the RDBMS.
To define a parameter for the StoredProcedure class, you can use an SqlParameter or one of its subclasses. You must specify the parameter name and SQL type in the constructor, as the following code snippet shows:
StoredProcedure 클래스는 RDBMS 저장 프로시저에 대한 객체 추상의 추상(superclass) 상위 클래스입니다.
상속받은 sql 속성은 RDBMS에 정의된 저장 프로시저의 이름입니다.
StoredProcedure 클래스에 매개변수를 정의하려면, SqlParameter 또는 그것의 하위 클래스들 중 하나를 사용할 수 있습니다.
다음 코드 조각에서 보여주듯이, 생성자에서 반드시 매개변수 이름과 SQL 타입을 지정해야 합니다.
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
The SQL type is specified using the java.sql.Types constants.
SQL 타입은 java.sql.Types의 상수를 사용하여 지정됩니다.
The first line (with the SqlParameter) declares an IN parameter. You can use IN parameters both for stored procedure calls and for queries using the SqlQuery and its subclasses (covered in Understanding SqlQuery).
The second line (with the SqlOutParameter) declares an out parameter to be used in the stored procedure call. There is also an SqlInOutParameter for InOut parameters (parameters that provide an in value to the procedure and that also return a value).
For in parameters, in addition to the name and the SQL type, you can specify a scale for numeric data or a type name for custom database types. For out parameters, you can provide a RowMapper to handle mapping of rows returned from a REF cursor. Another option is to specify an SqlReturnType that lets you define customized handling of the return values.
The next example of a simple DAO uses a StoredProcedure to call a function (sysdate()), which comes with any Oracle database. To use the stored procedure functionality, you have to create a class that extends StoredProcedure. In this example, the StoredProcedure class is an inner class. However, if you need to reuse the StoredProcedure, you can declare it as a top-level class. This example has no input parameters, but an output parameter is declared as a date type by using the SqlOutParameter class. The execute() method runs the procedure and extracts the returned date from the results Map. The results Map has an entry for each declared output parameter (in this case, only one) by using the parameter name as the key. The following listing shows our custom StoredProcedure class:
첫 번째 줄(SqlParameter 사용)은 입력(IN) 매개변수를 선언합니다. IN 매개변수는 저장 프로시저 호출뿐만 아니라, SqlQuery 및 그 하위 클래스들을 사용하는 쿼리에도 사용할 수 있습니다 (SqlQuery에 대한 이해는 [Understanding SqlQuery]에서 다룸).
두 번째 줄(SqlOutParameter 사용)은 저장 프로시저 호출에서 사용되는 출력(OUT) 매개변수를 선언합니다.
또한, 입출력(INOUT) 매개변수를 위한 SqlInOutParameter도 있습니다.
이 매개변수는 프로시저에 값을 전달함과 동시에, 결과 값을 반환받습니다.
입력 매개변수의 경우, 이름과 SQL 타입 외에도 숫자 데이터의 경우 scale을, 사용자 정의 데이터베이스 타입의 경우 type name을 지정할 수 있습니다.
출력 매개변수의 경우, REF cursor로부터 반환되는 행(row)들을 매핑하기 위해 RowMapper를 제공할 수 있습니다.
또 다른 옵션으로는 SqlReturnType을 지정하여 반환 값을 커스터마이징해서 처리할 수도 있습니다.
다음 예제는 간단한 DAO에서 StoredProcedure를 사용해 Oracle 데이터베이스에 기본 내장된 함수인 sysdate()를 호출하는 예시를 보여줍니다.
저장 프로시저 기능을 사용하려면, StoredProcedure를 상속하는 클래스를 만들어야 합니다. 이 예제에서는 StoredProcedure 클래스가 내부 클래스(inner class)로 정의되어 있지만, 재사용이 필요하다면 별도의 최상위 클래스(top-level class)로 선언할 수도 있습니다.
이 예제는 입력 매개변수가 없지만, SqlOutParameter 클래스를 사용해 출력 매개변수를 날짜 타입(date type)으로 선언합니다.
execute() 메서드는 프로시저를 실행하고, 결과 Map으로부터 반환된 날짜를 추출합니다.
이 결과 Map은 각 출력 매개변수에 대한 항목(entry)을 포함하며, 해당 매개변수의 이름을 키(key)로 사용합니다 (이 경우에는 매개변수가 하나뿐).
다음 목록(listing)은 사용자 정의 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() {
// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
Map<String, Object> results = execute(new HashMap<String, Object>());
Date sysdate = (Date) results.get("date");
return sysdate;
}
}
}
다음 StoredProcedure 예제는 출력 매개변수가 두 개입니다 (이 경우, Oracle의 REF 커서입니다).
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() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap<String, Object>());
}
}
Notice how the overloaded variants of the declareParameter(..) method that have been used in the TitlesAndGenresStoredProcedure constructor are passed RowMapper implementation instances. This is a very convenient and powerful way to reuse existing functionality. The next two examples provide code for the two RowMapper implementations.
The TitleMapper class maps a ResultSet to a Title domain object for each row in the supplied ResultSet, as follows:
TitlesAndGenresStoredProcedure 생성자에서 사용된 declareParameter(..) 메서드의 오버로딩된 형태들이
RowMapper 구현 인스턴스를 전달받고 있다는 점에 주목하세요.
이것은 기존 기능을 재사용할 수 있는 매우 편리하고 강력한 방법입니다. 다음 두 개의 예제는 이 두 개의 RowMapper 구현체에 대한 코드를 제공합니다. TitleMapper 클래스는 주어진 ResultSet의 각 행을 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;
}
}
The GenreMapper class maps a ResultSet to a Genre domain object for each row in the supplied ResultSet, as follows:
GenreMapper 클래스는, 주어진 ResultSet의 각 행(row)을 Genre 도메인 객체로 매핑하는 역할을 합니다. 그 방식은 다음과 같습니다.
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;
}
}
The GenreMapper class maps a ResultSet to a Genre domain object for each row in the supplied ResultSet, as follows:
GenreMapper 클래스는 주어진 ResultSet의 각 행(row)을 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) 메서드에 위임하는
강한 타입의 execute(..) 메서드를 직접 작성할 수 있습니다.
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);
}
}
정리
- Spring의 org.springframework.jdbc.object 패키지는 JDBC 작업을 객체지향적으로 캡슐화할 수 있도록 도와줍니다.
- MappingSqlQuery는 SELECT 결과를 Java 객체로 매핑하며, mapRow()를 구현해 사용합니다.
- SqlUpdate는 update/insert/delete 쿼리를 처리하며, 파라미터 선언 후 update()로 실행합니다.
- StoredProcedure는 저장 프로시저 호출용 추상 클래스이며, 파라미터 선언과 execute() 호출 방식으로 사용합니다.
- 복잡한 쿼리나 재사용 가능한 로직에는 유용하지만, 단순한 경우에는 JdbcTemplate이 더 간단할 수 있습니다.
JDBC 작업을 클래스 단위로 캡슐화하면 쿼리 재사용성, 가독성, 유지보수성이 높아집니다.
Spring은 이를 위해 MappingSqlQuery, SqlUpdate, StoredProcedure 같은 클래스를 제공합니다.
'공식문서' 카테고리의 다른 글
[Spring Docs] Embedded Database Support (0) | 2025.04.14 |
---|---|
[Spring Docs] Common Problems with Parameter and Data Value Handling (0) | 2025.04.11 |
[Spring Docs] Simplifying JDBC Operations with the SimpleJdbc Classes (0) | 2025.04.09 |
[Spring Docs] JDBC Batch Operations (0) | 2025.04.04 |
[Spring Docs] Controlling Database Connection (0) | 2025.04.02 |