Spring Framework를 이용한 Oracle JDBC 액세스의 개선
저자 Dustin Marx
Spring Framework를 이용하여 Oracle JDBC 액세스를 개선하는 방법을 알아봅니다.
|
Spring Framework는 Apache 라이센스를 통해 공개된 Java/J2EE 애플리케이션 프레임워크로 J2EE 애플리케이션의 다양한 계층을 지원하고 있습니다. Spring Framework의 중요한 장점의 하나로 JDBC 데이터 액세스에 관련하여 뛰어난 유지보수성과 안정성을 지원한다는 점을 들 수 있습니다. 이 문서에서는 Spring Framework와 Oracle TopLink 오브젝트/관계형(O/R) 매핑 툴을 이용하여 JDBC 코드를 작성하는데 수반되는 반복적 요소와 리스크를 줄이는 방법을 설명합니다. 개발자는 Spring Framework를 이용하여 오라클 데이터베이스 기반 환경에서 보다 깔끔하고, 유연하고, 에러 가능성이 적은 JDBC 코드를 작성할 수 있습니다.
데이터베이스 리소스에 대한 연결의 해제 JDBC 코딩 과정에서 연결을 정상적으로 해제하지 않는 실수가 종종 발생하곤 합니다. 그 결과로 데이터베이스 리소스의 할당에 문제가 발생할 수 있습니다. 마찬가지로 결과 셋과 구문에 대한 해제(close) 작업 역시 필요하며, 이는 일반적으로 권장되는 관행이기도 합니다. 코드의 실행이 비정상적으로 종료되는 상황에서도 이러한 해제 작업이 정상적으로 이루어지도록 하기 위한 코딩의 예를 아래 Listing 1의 finally 블록에서 확인하실 수 있습니다.try { // JDBC Connection/Statement/Result Set } catch (SQLException sqlEx) { // Handle the exception } finally { try { // Closing connection *should* close statement and result set if (stmt != null) stmt.close(); if (conn != null) conn.close(); } catch (SQLException sqlEx) { System.err.println("SQLException NOT handled"); } }위 예제에서 finally 블록은 데이터베이스 연결 및 구문이 정상적으로 해제되었음을 보장합니다. 이와 같은 메커니즘을 이용하여 연결 해제를 관리하는 경우, 코드가 길어지고 반복되는 현상이 나타날 수 밖에 없습니다. Spring Framework는 연결 관리 및 리소스 관리를 개발자의 관점에서 추상화하고, 보다 일관적인 리소스 관리 및 코드 가독성의 향상을 가능하게 합니다. Spring 코드 예제 – 첫 번째 Listing 2의 JDBC 코드는 여러분이 잘 알고 계신 scott/tiger 스키마에서 직원의 커미션을 조회하는 예제입니다. 이 경우에도, 실제로 데이터베이스를 쿼리하기 위한 코드 이외에도 많은 양의 “반복적인” 코드가 함께 작성되어야 합니다.
List commissions = new ArrayList(); Statement stmt = null; ResultSet rs = null; try { stmt = this.myConnection.createStatement(); rs = stmt.executeQuery("SELECT comm FROM emp"); while ( rs.next() ) { Integer commission = new Integer( rs.getInt("COMM") ); if ( rs.wasNull() ) { // By assigning the commission to null, this effectively // represents a null in the database as a Java null. System.out.println( "\tCommission seen as " + commission + " is really null"); commission = null; } commissions.add( commission ); } } catch (SQLException sqlEx) // checked { System.err.println( "Message: " + sqlEx.getMessage() ); System.err.println( "Error Code: " + sqlEx.getErrorCode() ); System.err.println( "SQL State: " + sqlEx.getSQLState() ); } finally { try { if ( rs != null ) { rs.close(); } if ( stmt != null ) { stmt.close(); } } catch (SQLException sqlEx) // checked { System.err.println( sqlEx.getMessage() ); } }Listing 3은 Spring Framework를 이용하여 Listing 2와 유사한 기능을 구현한 예를 보여주고 있습니다.
List commissions = new ArrayList(); try { JdbcTemplate jt = new JdbcTemplate(this.myDataSource); List commList = jt.queryForList( "SELECT comm FROM emp"); Iterator commIter = commList.iterator(); while ( commIter.hasNext() ) { Number comm = (Number) ((Map) commIter.next()).get("COMM"); if (comm != null) commissions.add( new Integer(comm.intValue()) ); else commissions.add( null ); } } catch ( DataAccessException ex ) // unchecked exception { System.err.println( ex.getMessage() ); }위 예제를 통해, Spring Framework를 사용하는 경우 순수 JDBC 코딩에 비해 훨씬 적은 양의 코딩만으로도 동일한 기능을 구현할 수 있음을 확인할 수 있습니다. Listing 3에서 볼 수 있듯, (연결, 구문, 결과 셋 등의) 리소스 관리를 위한 코드를 별도로 작성하고 유지할 필요가 없습니다. Listing 3 예제에 포함된 소량의 예외 처리 코드도 반드시 필요한 것은 아닙니다. DataAccessException는 체크되지 않는 예외사항(unchecked exception)입니다. 커미션 조회 결과를 반환하는 과정에서 Number 타입이 사용되기 때문에 ResultSet의 wasNull 메소드를 명시적으로 호출할 필요가 없습니다. 실제로, Listing 3에서는 ResultSet 신택스가 전혀 사용되고 있지도 않습니다. Listing 3에는 또 Spring Framework의 JDBC 지원을 위해 사용되는 가장 기본적인 클래스의 하나인 JdbcTemplate이 사용되고 있습니다. 예제에서 JdbcTemplate 클래스의 인스턴스는 데이터 소스와 함께 인스턴스화(instantiate) 되며, 그런 다음 overriden된 queryForList 메소드가 제공된 SQL 구문과 함께 템플릿 클래스에서 호출됩니다. queryForList 메소드는 HashMap의 ArrayList를 반환합니다. 여기서 ArrayList의 각 엘리먼트는 반환된 데이터 로우로 구성되며, 어레이 리스트 엘리먼트의 각 맵 엔트리는 해당 로우의 컬럼 값을 의미합니다. JdbcTemplate이 제공하는 다양한 overriden된 버전의 queryForList 메소드를 사용하면 다수의 로우에 대한 쿼리가 가능합니다. 또 JdbcTemplate 클래스는 queryForInt (하나의 integer를 반환), queryForLong (return a single long), query, update, 등의 다양한 메소드 버전을 제공합니다. Spring Framework가 제공하는 Javadoc 기반 API 문서의 "Method Detail" 부분을 참고하면 overridden 메소드에 대한 자세한 정보를 얻을 수 있습니다. 각각의 메소드는 사용하는 구문의 유형 (Statement,PreparedStatement 등), 지원되는 기능 면에서 차이를 갖습니다. JdbcTemplate은 그 밖에도 (JDBC에 관련한 일정 수준의 지식이 요구되는) 메소드들을 제공하며, 이 메소드를 이용하여 보다 높은 유연성을 확보하는 것이 가능합니다. (이러한 유형의 메소드에 대해서는 뒷부분에서 설명합니다.) JDBC Exception 핸들링
다시 Listing 1으로 돌아가 봅시다. 예제 코드에서 명시적으로 처리되는 exception은 java.sql.SQLException이 유일함을 확인할 수 있습니다. SQLException은 데이터베이스 및 SQL에 관련한 다양한 예외 상황을 처리하기 위한 exception입니다. Javadoc은 SQLException 인스턴스로부터 가져올 수 있는 기본적인 정보에 대해 설명하고 있습니다. 이 정보에는 에러의 내용을 기술하는 문자열[getMessage()], 표준화된 SQLState exception String [getSQLState()], 벤더별 정수 에러 코드 [getErrorCode()]등이 포함됩니다. 이 세 가지 정보는 Listing 1에서 모두 사용되고 있습니다.
SQLException은 (java.lang.Exception을 직접적으로 확장한) checked exception입니다. Java의 checked exception은 Java 커뮤니티에서 열띤 논란의 주제가 되고 있으며, exception이 애플리케이션 내에서 처리될 수 있는 경우에만 checked exception을 사용해야 한다는 것이 일반적인 관례로 굳어지고 있습니다. 애플리케이션에서 의미 있는 형태로 처리하기 어려운 exception이라면 unchecked로 처리되는 것이 바람직합니다. SQLException은 checked exception이며, 따라서 애플리케이션 내에서 예외 처리를 수행하거나, 호출 코드로 명시적으로 전달하는 방법을 통해 애플리케이션 코드에서 직접 처리해야 합니다. SQLException은 SQL 구문을 사용하는 관계형 데이터 소스의 종류에 따라 달라질 수 있습니다. 따라서 데이터 저장소 유형과 언어로부터 독립적인, 진정한 의미의 Data Access Objects (DAO) 호환성이 보장되지 않는다는 문제가 있습니다. Spring Framework의 SQLException 처리 방식은 JDBC 개발/유지보수의 관점에서 볼 때 매우 유용한 기능으로 꼽힙니다. Spring Framework는 SQLException를 추상화하고, DAO 친화적인 unchecked exception의 계위(hierarchy)를 제공합니다. (Oracle JDeveloper 10g의 UML 모델링 툴을 통해 생성한) 그림 1은 JDBC 및 DAO를 위한 Spring Framework의 exception 클래스 중 중요한 몇 가지를 보여주고 있습니다. 그림 1에서 보여지는 모든 클래스는 org.springframework 패키지 내의 서브패키지에 포함되어 있습니다. JDBC 관련 exception 핸들링 클래스는 jdbc 서브패키지에, DAO 관련 exception 핸들링 클래스는 dao 서브패키지에 포함됩니다.오라클 데이터베이스에 액세스하는 JDBC 드라이버 관련 에러의 대표적인 예가 표 1에 정리되어 있습니다. "Example Code and/or Comment" 컬럼은 에러의 원인이 되는 SQL 구문의 예와 추가적인 설명을 포함하고 있습니다.
Error Label | Oracle Error | SQLState | Example Code and/or Comment |
SQL-related errors based on variations of statement: SELECT ename FROM emp | |||
"unique constraint" | 1 | 2300 | 예: 프라이머리 키 제약조건 위반 |
"resource busy and acquire with NOWAIT specified" |
54 | 61000 | NOWAIT이 명시된 경우에만 발생 |
"invalid SQL statement" |
900 | 42000 |
ename FROM emp |
"invalid table name" |
903 | 42000 |
SELECT ename FROM |
"invalid identifier" |
904 | 42000 |
SELECT empname FROM emp |
"invalid character" |
911 | 42000 |
SELECT ename FROM emp; |
"missing column" |
917 | 42000 |
예: INSERT 구문의 컬럼 구분을 위한 쉼표 기호가 누락된 경우. |
"FROM keyword not found where expected" |
923 | 42000 |
SELECT ename emp |
"missing expression" |
936 | 42000 |
SELECT FROM emp |
"table or view does not exist" |
942 | 42000 |
SELECT ename FROM empp |
"cannot insert null into" |
1400 | 23000 | NOT NULL 제약조건을 갖는 컬럼에 NULL 값을 삽입하려 시도하는 경우 |
"value larger than specified precision allows for this column" |
1438 | 22003 | 컬럼이 허용하는 정밀도(precision)을 초과하는 수를 삽입하려 시도하는 경우 |
"invalid number" |
1722 | 42000 | 문자에 대해 numeric 함수를 적용하려 시도한 경우 |
"integrity constraint failed" |
2291 | 23000 | 기존 프라이머리 키와 일치하지 않는 외래 키를 삽입하려 시도하는 경우 |
"value too large for column" |
12899 | 72000 | 컬럼이 허용하는 것보다 큰 값(예: 너무 많은 수의 문자)을 삽입하려 시도하는 경우 |
"Io exception" |
17002 | none | Oracle JDBC 드라이버에서 발생한 에러에 대응되는 SQLState 코드가 존재하지 않는 경우 |
"Invalid column index" |
17003 | none |
|
"Invalid column name" |
17006 | none |
|
"Numeric Overflow" |
17026 | none |
|
본 문서의 마지막 부분의 “온라인 리소스” 섹션에는 사용자가 마주칠 수 있는 다양한 오라클 데이터베이스 exception에 대한 상세한 정보를 제공하는 웹 사이트 링크가 소개되어 있습니다. Oracle JDBC 드라이버 에러 코드는 Oracle JDBC Developer's Guide and Reference의 Appendix B 에서 참고하실 수 있으며 Oracle Database Error Messages 문서는 오라클 데이터베이스 에러 코드에 대한 일반적인 정보를 담고 있습니다.
Spring Framework는 표준 기반 SQLState와 벤더별 에러 코드를 모두 지원합니다. Spring Framework의 벤더별 에러 코드 지원은, 각 데이터베이스와의 느슨한 커플링(loose coupling)을 통해 구현됩니다. Spring Framework는 XML 구성 파일을 이용하여 JDBC에서 자주 발생하는 벤더별 에러에 대한 링크를 제공합니다. Spring Framework의 sql-error-codes.xml 구성 파일에 포함된 오라클 데이터베이스 관련 설정이 Listing 4와 같습니다 (다른 데이터베이스에 관련된 설정 항목은 Listing 4에 포함되어 있지 않습니다.)<bean id="Oracle” class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="badSqlGrammarCodes"> <value>900,903,904,917,936,942,17006</value> </property> <property name="invalidResultSetAccessCodes"> <value>17003</value> </property> <property name="dataAccessResourceFailureCodes"> <value>17002</value> </property> <property name="dataIntegrityViolationCodes"> <value>1,1400,1722,2291</value> </property> <property name="cannotAcquireLockCodes"> <value>54</value> </property> </bean>sql-error-codes.xml에서 쉼표로 구분된 정수들은 앞에서 설명한 벤더별 에러 코드의 숫자를 의미합니다. 특히 "badSqlGrammarCodes" 카테고리에 표시된 숫자의 대부분이 표 1에서 설명되고 있습니다. 17006은 “invalid column name”을 의미하는 JDBC 드라이버 에러 코드입니다. Listing 4의 property 태그에서 name 속성은 각각의 에러 코드를 처리하기 위해 사용되는 exception의 타입을 지정하는 용도로 사용됩니다. 예를 들어, 917 (ORA-00917) 에러가 발생한 경우 Spring Framework는 unchecked BadSqlGrammarException을 발생시키게 됩니다. 이러한 설정이 코드 외부에 XML 파일의 형태로 존재하기 때문에, 특정 벤더 에러 코드에 대해 대응되는 JDBC exception을 발생시키도록 설정을 변경하기가 매우 용이하다는 장점이 있습니다. 데이터베이스의 에러 코드 별로 대응되는 exception을 설정하는 것이 바람직한 이유에는 여러 가지가 있습니다. 먼저, (어차피 런타임에서 코드를 처리하는 것에는 한계가 있기 때문에) SQLException이 발생하는 상황 중 일부에 대해서만 exception 처리를 수행하기를 원할 수 있습니다. Spring Framework는 데이터베이스 개발자를 위한 정교한 exception hierarchy와 데이터베이스 및 exception 간에 느슨하게 커플링된 연결된 관계를 제공함으로써 특정 exception만을 처리하고, 런타임에 처리가 불가능한 unchecked exception을 무시할 수 있습니다.
BadSqlGrammarException은 Spring이 JDBC 환경을 위해 지원하는 유용한 exception 클래스의 하나입니다. 이 exception 클래스가 제공하는 getSql() 메소드는 exception이 발생한 시점에 호출된 SQL 구문을 반환합니다. 이 클래스가 (일반적인 DAO 클래스와 달리) SQL 구문에 관련한 상세 정보를 갖고 있기 때문에 getSQLException() 메소드를 통한 표준 SQLException 으로의 핸들로써 이용될 수도 있습니다.
Bsql-error-codes.xml파일에 오라클 에러 코드를 추가하고 기존 Spring exception 클래스로 매핑하는 대신, 커스텀 exception 핸들링 클래스를 생성하는 방법을 사용할 수도 있습니다. 그런 다음, 커스텀 SQLExceptionTranslator 클래스를 이용하여 생성된 커스텀 exception 핸들링 클래스와 Oracle 에러 코드를 연결하게 됩니다. Spring에서 PreparedStatement 사용하기Listing 3의 Spring 코드 예제는 실행된 SQL의 Statement에 대한 Spring의 래핑(wrapping) 기능에 의존하고 있습니다. 하지만, 데이터베이스에 대해 SQL을 실행하는 과정에서 Statement 대신 PreparedStatement가 사용될 수도 있습니다. Spring JdbcTemplate 클래스는 Statement와 PreparedStatement를 모두 지원하는 다양한 메소드를 제공하고 있으며, 사용자는 자신이 사용하고자 하는 JDBC 구문의 유형을 선택할 수 있습니다.
Spring의 Javadoc 기반 API 문서는 각 메소드의 Statement 또는 PreparedStatement 지원 여부를 명시하고 있습니다. 또 SQL 매개변수의 전달 여부에 따라 JdbcTemplate에서 사용되는 구문의 유형을 구분하는 것도 가능합니다. SQL 구문의 문자열만이 전달되는 경우, 이 메소드는 Statement를 사용하는 것으로 간주될 수 있습니다. 메소드가 SQL 구문과 별개로 매개변수를 사용하는 경우, 이 메소드는 PreparedStatement를 사용하는 것으로 간주됩니다. 다음에 소개할 두 개의 예제 코드(Listing 5와 Listing 6)는 PreparedStatement를 이용한 표준 JDBC 액세스와 PreparedStatement를 래핑한 Spring 기반 액세스를 각각 보여주고 있습니다.String updateStr = "UPDATE salgrade SET losal = ?, hisal = ? WHERE grade = ?"; PreparedStatement stmt = null; try { stmt = this.myConnection.prepareStatement(updateStr); stmt.setInt(1,aLowSal); stmt.setInt(2,aHighSal); stmt.setInt(3,aSalGrade); updateStatus = stmt.execute(); stmt.close(); } // lots of catch and finally code typically follows here
Listing 6
String updateStr = "UPDATE salgrade SET losal = ?, hisal = ? WHERE grade = ?"; JdbcTemplate jt = new JdbcTemplate(this.myDataSource); jt.update( updateStr, new Object[] { new Integer(aLowSal), new Integer(aHighSal), new Integer(aSalGrade) } ); // No handling/closing of PreparedStatement or catch/finally neededListing 6는 Spring Framework를 이용하여 데이터베이스를 업데이트하는 예를 보여주고 있습니다. 코드 내에서 "PreparedStatement"가 직접 명시되지는 않고 있지만,JdbcTemplate's의 update 메소드는 실제로 PreparedStatement를 사용하고 있습니다. JDBC 표준은 발생된 SQLException를 캡처할 것을, 그리고 구문을 블록으로 처리할 것을 요구하고 있습니다. Listing 6에서 확인할 수 있듯, Spring 기반 코드에서는 이러한 요구사항이 존재하지 않습니다. Listing 6에서 PreparedStatement가 명시적으로 사용되지 않고 있음을 참고하시기 바랍니다. JdbcTemplate update 메소드를 사용하는 개발자는 PreparedStatement의 자세한 사용법, API, 또는 SQLException에 대해 알고 있을 필요가 없습니다. 개발자는 “anonymous”한 형태의 inner 클래스를 사용하여 SQL 구문과 함께 JdbcTemplate.update 메소드를 전달하고 있습니다. Oracle-Specific SQL의 사용 Spring Framework는 JDBC 개발자들이 일상적이고 반복적으로 수행하는 코딩 작업에 대해 “래핑(wrapping)” 기능을 제공하지만, 반면 비표준적인 SQL/JDBC가 필요한 경우에는 이를 충분히 활용할 수 있는 환경을 제공합니다. 코드가 완벽하게 표준화된 환경이 바람직할 수도 있겠지만, 경우에 따라서는 특정 벤더가 제공하는 비표준적인 기능이 매우 유용하게 사용될 수도 있습니다. 오라클 환경의 경우, ROWID를 이용하여 오라클 테이블의 로우(row)를 지정하는 기능을 그 예로 들 수 있습니다. Listing 7과 8은 scott/tiger EMP 테이블에서 employee number를 기준으로 ROWID를 가져오기 위한 고전적 JDBC 코드와 Spring 기반 JDBC 코드를 보여주고 있습니다. 두 가지 경우 모두, ROWID는 String의 형태로 반환됩니다.
String queryStr = "SELECT rowid FROM emp WHERE empno = " + aEmpNum; // aEmpNum set previously String rowId = null; try { stmt = this.myConnection.createStatement(); rs = stmt.executeQuery(queryStr); while ( rs.next() ) { rowId = rs.getString("ROWID"); } } // lots of catch-finally code needed after this
Listing 8
String queryStr = "SELECT rowid FROM emp WHERE empno = " + aEmpNum; String rowId = null; try { JdbcTemplate jt = new JdbcTemplate(this.myDataSource); oracle.sql.ROWID oraRowId = (ROWID) jt.queryForObject(queryStr, ROWID.class); rowId = oraRowId.stringValue(); } catch ( IncorrectResultSizeDataAccessException wrongSizeEx ) { // This unchecked exception is thrown in this case if more // than one result is returned from the query. // Explicitly printing out the results of this exception's // methods getExpectedSize() and getActualSize() is really not // necessary in this case because this exception's getMessage() // returns this same information in sentence form. System.err.println( wrongSizeEx.getMessage() ); System.err.print( "Expected " + wrongSizeEx.getExpectedSize() + " results, but actually got back " + wrongSizeEx.getActualSize() + " results."); }Listing 8를 통해 Spring Framework이 오라클이 사용하는 비표준적 키워드를 유연하게 지원하고 있음을 확인할 수 있습니다. 또 Listing 8에서는, Spring의 DAO exception이 활용되고 있습니다. queryStr이 전체 ROWID를 한꺼번에 반환하는 경우 IncorrectResultSizeDataAccessException이 발생됩니다. Oracle에서 사용되는 비표준적 SQL의 가장 잘 알려진 예로 “SELECT sysdate FROM dual”을 들 수 있을 것입니다 (이 쿼리는 ANSI 표준에는 포함되어 있지 않습니다). Listing 9은 Spring Framework에서 이 쿼리를 사용하는 방법을 보여주고 있습니다.
String queryStr = "SELECT sysdate FROM dual"; Date date = null; try { JdbcTemplate jt = new JdbcTemplate(this.myDataSource); date = (Date) jt.queryForObject(queryStr, Date.class); } catch ( BadSqlGrammarException badSqlEx ) // unchecked { System.err.println( badSqlEx.getMessage() ); System.err.println( "Bad SQL: " + badSqlEx.getSql() ); }Spring 및 JDBC 기반의 DDL 구문 실행 위의 코드 예제들은 모두 Spring Framework를 이용하여 DML 구문을 처리하는 방법을 보여주고 있습니다. Spring Framework는 매우 간단한 신택스를 통해 DDL 구문을 지원합니다. Listing 10과 11은 오라클 데이터베이스에서 테이블을 drop/purge하기 위한 표준 JDBC 코드와 Spring으로 래핑된 JDBC 코드를 예시하고 있습니다.
Statement stmt = null; try { stmt = this.myConnection.createStatement(); stmt.executeUpdate("DROP TABLE salgrade PURGE"); } catch ( SQLException sqlEx ) { System.err.println("Message: " + sqlEx.getMessage()); System.err.println("Error Code: " + sqlEx.getErrorCode()); System.err.println("SQL State: " + sqlEx.getSQLState()); } finally { try { if (stmt != null) { stmt.close(); } } catch (SQLException finallySqlEx) // checked exception { System.err.println(finallySqlEx.getMessage()); } }
Listing 11
try { JdbcTemplate jt = new JdbcTemplate(this.myDataSource); jt.execute("DROP TABLE salgrade PURGE"); } catch ( BadSqlGrammarException badSqlEx ) // unchecked { System.err.println( badSqlEx.getMessage() ); System.err.println( "BadSQL: " + badSqlEx.getSql() ); }이제 Spring 기반 코드가 고전적인 JDBC 코드에 비해 읽기(그리고 작성과 관리)에 훨씬 편리하다는 사실을 분명하게 확인하셨을 것입니다. 위의 경우, 캡처된 exception이 unchecked exception이므로 실제로 작성이 필요한 코드는 단 두 줄에 불과합니다. Spring Framework를 이용한 스토어드 프로시저의 액세스 Listings 13과 14는 (Listing 12에 사용된) 스토어드 프로시저를 일반적인 JDBC와 Spring 기반의 JDBC를 이용해 액세스하는 방법을 보여주고 있습니다.
CREATE OR REPLACE PROCEDURE salary_percentile ( salary IN emp.sal%TYPE, low IN salgrade.losal%TYPE, high IN salgrade.hisal%TYPE, percentile OUT NUMBER ) AS BEGIN IF salary < 0 THEN percentile := null; ELSE percentile := (salary - low) / (high - low); END IF; END;
Listing 13
String escapeString = "{call salary_percentile (?,?,?,?)}"; CallableStatement cStmt = null; double percentile = -1.0; final int PERCENTILE_INDEX = 4; try { cStmt = this.myConnection.prepareCall(escapeString); cStmt.setInt(1, aSalary); // aSalary passed into this method cStmt.setInt(2, aLow); // aLow passed into this method cStmt.setInt(3, aHigh); // aHigh passed into this method cStmt.registerOutParameter(PERCENTILE_INDEX, Types.DOUBLE); cStmt.execute(); percentile = cStmt.getDouble(PERCENTILE_INDEX); } catch ( SQLException sqlEx ) { System.err.println("Message: " + sqlEx.getMessage()); System.err.println("Error Code: " + sqlEx.getErrorCode()); System.err.println("SQL State: " + sqlEx.getSQLState()); } finally { if ( cStmt != null ) { try { cStmt.close(); } catch (SQLException finallySqlEx) { System.err.println(finallySqlEx.getMessage()); } } } return percentile;Spring 기반의 코드를 통해 스토어드 프로시저에 대한 액세스를 예시하고 있는 Listing 14에는 org.springframework.jdbc.object.StoredProcedure 클래스가 사용되고 있습니다. (Spring 패키지의 StoredProcedure 클래스에는 스토어드 프로시저 이외에도 다른 유형의 SQL 구문을 위한 오브젝트가 함께 포함되어 있습니다. 자세한 정보는 Spring 기술문서를 참고하시기 바랍니다.)
private class SalaryCalculator extends StoredProcedure { /** Name of procedure in database. */ public static final String PROC_NAME = "salary_percentile"; /** * Constructor for this StoredProcedure class. * @param ds Data Source. */ public SalaryCalculator(DataSource ds) { setDataSource(ds); setSql(PROC_NAME); // Parameters should be declared in same order here that // they are declared in the stored procedure. declareParameter(new SqlParameter("salary", Types.DOUBLE)); declareParameter(new SqlParameter("low", Types.INTEGER)); declareParameter(new SqlParameter("high", Types.INTEGER)); declareParameter(new SqlOutParameter( "percentile", Types.DOUBLE ) ); compile(); } /** * Execute stored procedure. * @return Results of running stored procedure. */ public Map executeCalculation( double aSalary, int aLow, int aHigh ) { Map inParameters = new HashMap(); inParameters.put( "salary", new Double(aSalary) ); inParameters.put( "low", new Integer(aLow) ); inParameters.put( "high", new Integer(aHigh) ); Map out = execute( inParameters ); // Call on parent class return out; } } // . . . // Code below is all that is needed to call your Stored Procedure // created above. // . . . SalaryCalculator calcPercentile = new SalaryCalculator(this.myDataSource); Map calcResults = calcPercentile.executeCalculation(aSalary, aLow, aHigh); return ((Double)calcResults.get("percentile")).doubleValue(); // . . . // remainder of class // . . .Listing 14의 코드는 하나의 클래스만을 사용하고 있습니다. 코드의 대부분은 Spring의 StoredProcedure 클래스를 확장하는 inner 클래스(SalaryCalculator)로 구성되며, 개발자가 생성한 클래스를 이용해 Listing 12의 스토어드 프로시저를 래핑(wrapping)하는 구조로 구현되어 있습니다. SalaryCalculator 클래스를 호출하기 위한 코드는 단 몇 줄에 불과합니다. 이처럼, SalaryCalculator 클래스는 스토어드 프로시저의 호출에 수반되는 복잡한 과정을 추상화하는 효과를 제공합니다. 개발자는 StoredProcedure를 확장하는 클래스를 작성하는 경우 다양한 효과를 확인할 수 있습니다. 일반적인 checked SQLException 대신 Spring이 제공하는 unchecked DAO, JDBC 익스텐션을 사용할 수 있다는 것도 기대할 수 있는 효과의 하나입니다. 또 위 코드에서 확인할 수 있는 것처럼 리소스의 해제를 위한 반복적인 코딩 작업을 생략할 수 있습니다.
0보다 작은 salary가 스토어드 프로시저에 전달되는 경우, Listings 13과14 에서 각각 다르게 처리된다는 점을 참고하시기 바랍니다. 일반적인 JDBC 코드(Listing 13)의 경우, 0.0의 값이 반환되며, 결과가 null인지 확인하기 위해서는 wasNull()을 사용해야만 합니다. Spring 기반 코드(Listing 14)에서는, Java null이 반환되며 wasNull()의 호출이 불필요합니다.
Spring을 이용한 Oracle 오브젝트의 액세스 Spring Framework'의 JDBC 추상화 기능을 이용하여 Oracle 오브젝트에 액세스하는 예를 Listing 15에서 확인하실 수 있습니다.CREATE OR REPLACE TYPE address_type AS OBJECT ( STREET VARCHAR2(20), CITY VARCHAR2(15), STATE CHAR(2), ZIP CHAR(5) ); / CREATE TABLE emp_address_table ( EMP_NUM NUMBER(4), ADDRESS ADDRESS_TYPE );JDBC를 통해 Oracle 오브젝트에 액세스하는 방법은 크게 두 가지가 있습니다. 그 하나는 표준 JDBC 인터페이스 java.sql.Struct를 Oracle 드라이버를 위한 클래스인 oracle.sql.STRUCT와 함께 사용하는 방법입니다. 두 번째는 Oracle 오브젝트 타입에 매핑되는 Java 클래스를 생성하는 방법입니다. Oracle JDeveloper 10g IDE 와 JPublisher를 이용하면 이러한 작업을 매우 간단하게 마무리할 수 있습니다. java.sql.Struct를 이용하거나 JPublisher를 이용하는 경우, 오브젝트 내의 데이터에 액세스하는 과정에서 SQLException을 직접 처리해야 한다는 요구사항이 뒤따릅니다. java.sql.Struct를 사용하는 경우, getAttributes() 메소드가 SQLException을 발생시킵니다. 마찬가지로 JDeveloper/JPublisher에 의해 생성된 Java 클래스에 포함된 메소드 역시 SQLException을 발생시킵니다. 이러한 Java 오브젝트에 액세스하는 개발자들은 SQLException을 직접 처리하거나 Listing 16 및 17과 같은 Spring 기반 코드를 사용해야 합니다.
String queryStr = "SELECT address FROM emp_address_table WHERE " + "emp_num = " + aEmpNum; // aEmpNum passed in final List addresses = new ArrayList(); JdbcTemplate jt = new JdbcTemplate(this.myDataSource); jt.query( queryStr, new RowCallbackHandler() { public void processRow(ResultSet rs) throws SQLException { // The Struct and ResultSet methods throw // SQLException, so throws above is necessary java.sql.Struct address = (java.sql.Struct) rs.getObject(1); String street = address.getAttributes()[0].toString(); String city = address.getAttributes()[1].toString(); String state = address.getAttributes()[2].toString(); String zipCode = address.getAttributes()[3].toString(); String addressStr = street + ", " + city + ", " + state + " " + zipCode; addresses.add( addressStr ); } } );
Listing 17
String updateStr = "UPDATE emp_address_table SET address = ? " + "WHERE emp_num = ?"; JdbcTemplate jt = new JdbcTemplate( getDataSource() ); jt.update( updateStr, new PreparedStatementSetter() { public void setValues(PreparedStatement ps) throws SQLException { Address address = new Address(); address.setStreet(aStreet); address.setCity(aCity); address.setState(aState); address.setZip(aZipCode); ps.setObject(1, address); ps.setInt(2, aEmpNum); } } );두 가지 접근방식 모두 JDBC(Struct와 SQLData)를 이용하여 Oracle 오브젝트에 액세스하고 있으며, 반환된 클래스에 액세스하는데 사용되는 메소드는 SQLException을 발생시킵니다. Listing 16, 17은 anonymous inner callback 클래스를 이용하여 SQLException을 unchecked Spring Framework exception hierarchy 내부로 “숨기는” 방법을 예시하고 있습니다. 이렇게 발생된 exception은 본 문서의 다른 예제와 동일한 exception 변환 방법을 사용하여 처리됩니다. 위 코드 예제는 Spring 기반의 코드를 사용하여 Oracle 오브젝트에 액세스하는 방법을 보여주는 동시에, 다른 JdbcTemplate 메소드의 적용이 불가한 상황에서 anonymous inner callback 클래스를 이용하는 방법을 예시하고 있습니다. Listing 16에서 Spring 기반의 코드에 ResultSet과 SQLException이 처음으로 사용된 것을 확인하실 수 있습니다. 하지만 여기에서도 SQLException은 직접적으로 사용되지 않고, Spring Framework가 내부적인 exception 핸들링 메커니즘을 통해 발생된 SQLException을 처리하고 있습니다. 그러므로 개발자는 Spring exception의 캡처 및 핸들링에 대해 신경을 쓰지 않아도 됩니다. 3, 6의 JdbcTemplat 활용 예제)에서는 ResultSet, Statement, PreparedStatement, SQLException 등이 아예 언급조차 되지 않았다는 사실을 상기하시기 바랍니다. 이처럼 고도로 추상화된 접근방식은 JDBC의 세부에 관여하기를 원하지 않는 개발자들에게 특히 유용합니다. 하지만 이처럼 극도로 단순화된 접근방식은 Listings 16 및 17에 예시된 inner-class 방식의 접근법에 비해 유연성이 떨어지는 것이 사실입니다. 개발자는 JDBC API에 대한 매우 기본적인 지식만을 활용하여 Listing 16, 17과 같은 유연한 코드를 작성할 수 있습니다. 어떠한 경우든 exception은 Spring exception hierarchy를 통해 일관성 있게 처리되며, 개발자는 SQLException에 대해 신경을 쓸 필요가 없습니다. 그 밖의 장점 Spring Framework가 JDBC 환경에서 제공하는 효과는 그 밖에도 여러 가지가 있습니다. 위에서 언급되지 않은 몇 가지 중요한 장점이 아래와 같습니다:
SSISO Community