------------------------------------------------------------------------------------------------
출처: http://www.ibm.com/developerworks/kr/library/j-5things10/j-5things10.html
------------------------------------------------------------------------------------------------
오늘날 많은 Java 개발자는 JDBC(Java Database Connectivity) API를 Hibernate나 Spring 같은 데이터 액세스 플랫폼으로 알고 있다.
하지만 JDBC는 데이터베이스 연결의 배후에서 지원하는 역할만 수행하는 것은 아니다. 이 API에 대해 더 많이 알수록 RDBMS 상호작용의 효율성을 더 많이 향상시킬 수 있다.
5가지 사항 시리즈의 이 기사에서는 JDBC 2.0과 JDBC 4.0 사이에 소개된 여러 가지 최신 기능에 대해 설명한다. 최신 소프트웨어 개발 과제를 고려하여 설계된 이러한 기능은 오늘날 Java 개발자가 직면하고 있는 공통 과제인 애플리케이션 확장성과 개발자 생산성이라는 두 가지 과제를 지원한다.
다양한 RDBMS 구현에서 개발자가 쉽게 작업할 수 있도록 지원하기 위해 설계된 SQL 및/또는 부가 기능에 대한 비정규적인 지원을 제공하고 있다. 예를 들어, SQL에 특정 SQL 필터 기준(즉, WHERE
조건부)을 충족하는 행의 수를 리턴하는 COUNT()
라는 스칼라 오퍼레이션이 있다는 것은 잘 알려져 있다. 하지만 이 수준을 넘어서 SQL에서 리턴된 값을 수정하는 작업은 매우 까다로우며 데이터베이스에서 현재 날짜 및 시간을 가져오려고 할 경우에는 인내심이 많은 JDBC 개발자도 너무 복잡해서 두통을 호소하기도 한다.
이 문제를 해결하기 위해 JDBC 스펙에서는 스칼라 함수를 통해 다양한 RDBMS 구현을 일정 수준 분리/채택할 수 있는 기능을 제공한다. JDBC 스펙에는 JDBC 드라이버가 특정 데이터베이스 구현에 필요한 것으로 인식하여 채택해야 하는 지원되는 오퍼레이션 목록이 포함되어 있다. 따라서 현재 날짜 및/또는 시간을 리턴하는 기능을 지원하는 데이터베이스의 경우 Listing 1처럼 간단하게 현재 날짜 및 시간을 가져올 수 있다.
Connection conn = ...; // get it from someplace
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("{CURRENT_DATE()}");
|
JDBC API에 인식되는 전체 스칼라 함수 목록이 JDBC 스펙의 부록(참고자료 참조)에 있기는 하지만 전체 목록이 지정된 드라이버 또는 데이터베이스에서 지원되지 않을 수 있다. Connection
에서 리턴된 DatabaseMetaData
오브젝트를 사용하여 지정된 JDBC 구현에서 지원되는 함수를 가져올 수 있다(Listing 2 참조).
Connection conn = ...; // get it from someplace
DatabaseMetaData dbmd = conn.getMetaData();
|
스칼라 함수 목록은 다양한 DatabaseMetaData
메소드에서 리턴되는 쉼표로 구분된 String
이다. 예를 들어, 모든 숫자 스칼라는getNumericFunctions()
호출을 통해 나열된다. 결과에 대해 String.split()
을 수행하면 그 즉시 equals()-testable
목록이 표시된다.
JDBC에서 Connection
오브젝트를 작성하거나 기존 오브젝트를 가져온 후 이 오브젝트를 사용하여 Statement
를 작성하는 프로시저는 매우 일반적인 프로시저이다. Statement
에서는 SQL SELECT
를 사용하여 ResultSet
를 리턴한다. 그런 다음 ResultSet
는 왼쪽에서 오른쪽으로 한 번에 한 열씩 추출하는 본문으로 구성된 while
루프(Iterator
와 유사함)를 통해 ResultSet
가 비워질 때까지 제공된다.
이 전체 오퍼레이션은 대부분의 개발자가 당연하게 생각할 정도로 일반적으로 수행되고 있으며 지금까지 그렇게 해왔다는 이유만으로 습관적으로 이루어지고 있다. 하지만 이는 완전히 불필요한 조작이다.
JDBC의 새 버전 번호와 릴리스에 개선 사항이 반영되어 있음에도 불구하고 JDBC가 지난 여러 해 동안 상당히 개선되었다는 것을 모르는 개발자가 많다. 첫 번째 주요 개선 사항인 JDBC 2.0은 JDK 1.2와 비슷한 시기에 개발되었다. 이 기사의 작성 시점을 기준으로 JDBC의 최신 버전은 4.0이다.
JDBC 2.0의 흥미로운(그러면서도 자주 무시되는) 개선 사항 중 하나는 ResultSet
를 "스크롤"하는 기능이다. 이는 필요에 따라 앞으로, 뒤로 또는 앞뒤로 이동할 수 있다는 것을 의미한다. 하지만 이 작업을 수행하려면 미리 생각해서 처리해야 하는 작업이 있다. 즉, JDBC 호출에서 Statement
가 작성되는 시점에 스크롤 가능한 ResultSet
를 사용할 것임을 지정해야 한다.
기본 JDBC 드라이버가 스크롤을 지원하는 경우에는 스크롤 가능한ResultSet
가 해당 Statement
에서 리턴된다. 하지만 스크롤 가능한ResultSet
를 요청하기 전에 드라이버가 스크롤 기능을 지원하는지 먼저 확인하는 것이 좋다. 앞에서 설명한 대로 Connection
에서 가져올 수 있는 DatabaseMetaData
오브젝트를 통해 스크롤 기능을 확인할 수 있다.
DatabaseMetaData
오브젝트를 작성한 후에는getJDBCMajorVersion()
을 호출하여 드라이버가 적어도 JDBC 2.0 스펙을 지원하는지 여부를 확인한다. 물론 드라이버가 지정된 스펙에 대한 지원 레벨을 잘못 알려줄 수도 있으므로 특별히 안전하게 실행하기 위해 원하는 ResultSet
유형을 사용하여 supportsResultSetType()
메소드를 호출한다. (유형은 ResultSet
클래스에 상수로 정의되어 있으며 잠시 후 각 유형의 값에 대해 살펴본다.)
int JDBCVersion = dbmd.getJDBCMajorVersion();
boolean srs = dbmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
if (JDBCVersion > 2 || srs == true)
{
// scroll, baby, scroll!
}
|
드라이버가 예라고 응답한 것으로 가정하면(그렇지 않은 경우에는 새 드라이버 또는 데이터베이스가 필요함)Connection.createStatement()
호출에 두 매개변수를 전달하여 스크롤 가능한 ResultSet
를 요청할 수 있다(Listing 4 참조).
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
|
createStatement()
를 호출할 때는 첫 번째 및 두 번째 매개변수가 둘 다 int
이므로 특히 주의해야 한다. (왜냐하면 Java 5까지는 열거된 유형을 사용하지 않았기 때문이다.) 잘못된 상수 값을 포함한 모든 int
값이 createStatement()
에서 정상적으로 작동한다.
ResultSet
에서는 원하는 "스크롤 기능"을 나타내는 첫 번째 매개변수에 허용되는 값은 다음 세 값 중 하나이다.
ResultSet.TYPE_FORWARD_ONLY
: 기본값이며, 잘 알고 있는 Firehose 커서이다.ResultSet.TYPE_SCROLL_INSENSITIVE
: 이ResultSet
를 사용하면 순방향뿐만 아니라 역방향 반복도 가능하다. 하지만 데이터베이스의 데이터가 변경된 경우ResultSet
에 변경 사항이 반영되지 않는다. 이 스크롤 가능한ResultSet
가 가장 일반적으로 사용되는 유형이다.ResultSet.TYPE_SCROLL_SENSITIVE
: 작성된ResultSet
가 양방향 반복을 지원할 뿐만 아니라 데이터베이스의 데이터가 변경되더라도 데이터에 대한 "실시간" 보기를 제공한다.
두 번째 매개변수는 다음 섹션에서 설명한다.
Statement
에서 ResultSet
를 가져온 후에는 previous()
를 호출하여 한 행 뒤로 스크롤하거나 next()
를 호출하여 한 행 앞으로 스크롤할 수 있다. 또는 first()
를 호출하여 ResultSet
의 처음으로 이동하거나 last()
를 호출하여 ResultSet
의 끝으로 이동할 수 있다.
relative()
및 absolute()
메소드도 유용하게 사용할 수 있다. 첫 번째 메소드는 지정된 행 수만큼 이동하며(양수이면 앞으로, 음수이면 뒤로), 두 번째 메소드는 커서의 현재 위치에 상관 없이 ResultSet
에 있는 지정된 행으로 이동한다. 물론 현재 행 번호는 getRow()
를 사용할 수 있다.
특정 방향의 스크롤 작업을 많이 수행할 예정이라면 setFetchDirection()
을 호출하여 원하는 방향을 지정하면 ResultSet
를 효율적으로 사용할 수 있다. (ResultSet
는 스크롤 방향에 상관 없이 작동하지만 방향을 미리 알고 있으면 데이터 검색을 최적화할 수 있다.)
JDBC는 양방향 ResultSet
만 지원하는 것이 아니라 ResultSet
에 대한 직접(in-place) 업데이트도 지원한다. 이는 새 SQL문을 작성하여 데이터베이스에 현재 저장되어 있는 값을 변경하는 것이 아니라 사용자가 ResultSet
내에 저장된 값을 수정할 수 있으며, 그런 다음 수정된 값이 자동으로 데이터베이스에 있는 해당 행의 해당 열로 전송된다는 것을 의미한다.
업데이트 가능한 ResultSet
를 요청하는 프로세스는 스크롤 가능한 ResultSet
를 요청하는 프로세스와 유사하다. 실제로 이 프로세스에서 createStatement()
에 대한 두 번째 매개변수를 사용한다. ResultSet.CONCUR_READ_ONLY
를 두 번째 매개변수의 값으로 지정하는 대신 ResultSet.CONCUR_UPDATEABLE
을 전송한다(Listing 5 참조).
Listing 5. 업데이트 가능한 ResultSet가 필요합니다.
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
|
드라이버가 업데이트 가능한 커서(대부분의 "실제" 데이터베이스에서 지원하게 될 JDBC 2.0 스펙의 또 다른 기능)를 지원한다고 가정하면 해당 행으로 이동한 후 update...()
메소드 중 하나를 호출하여 ResultSet
에 있는 지정된 값을 업데이트할 수 있다(Listing 6 참조). ResultSet
의 get...()
메소드와 마찬가지로 update...()
는 ResultSet
의 실제 열 유형에 대해 오버로드된다. 따라서 "PRICE
"라는 부동 소수점 열을 변경하려면 updateFloat("PRICE")
를 호출한다. 하지만 이렇게 하면 ResultSet
에 있는 값만 업데이트된다. 이 값을 데이터베이스에도 적용하려면 updateRow()
를 호출한다. 사용자가 가격을 변경하지 않기로 마음을 바꾼 경우에는cancelRowUpdates()
를 호출하여 보류 중인 모든 업데이트를 취소할 수 있다.
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS =
stmt.executeQuery("SELECT * FROM lineitem WHERE id=1");
scrollingRS.first();
scrollingRS.udpateFloat("PRICE", 121.45f);
// ...
if (userSaidOK)
scrollingRS.updateRow();
else
scrollingRS.cancelRowUpdates();
|
JDBC 2.0은 업데이트 외에도 다른 여러 기능을 지원한다. 사용자가 완전히 새로운 행을 추가하려는 경우 새 Statement
를 작성하고INSERT
를 실행하는 대신 moveTo!InsertRow()
를 호출한 다음 각 열에 대해 update...()
를 호출한 후 insertRow()
를 호출하여 작업을 완료할 수 있다. 열 값을 지정하지 않은 경우에는 열 값이 SQL NULL
로 간주된다. (이 경우 데이터베이스 스키마에서 해당 열에 NULL
을 허용하지 않으면 SQLException
이 발생한다.)
기본적으로 ResultSet
가 행 업데이트를 지원하는 경우에는 deleteRow()
를 통한 행 삭제도 지원해야 한다.
그리고 앞에서 미처 언급하지 못하고 지나친 부분이 있다. 이러한 스크롤 기능 및 업데이트 기능은 모두 SQL 주입 공격의 상존하는 위험 때문에 일반 Statement
에 비해 많이 사용되는 PreparedStatement
에도 동일하게 적용된다(해당 매개변수를 prepareStatement()
메소드에 전달).
거의 10년 동안 이 모든 기능이 JDBC에 있었음에도 불구하고 대부분의 개발자가 아직까지도 ResultSet
를 앞으로 스크롤하는 기능과 연결되지 않은 액세스를 고집하는 이유는 무엇일까?
주 원인은 확장성 때문이다. 인터넷을 통해 회사의 웹 사이트에 액세스하는 수많은 사용자를 지원하기 위해서는 데이터베이스 연결을 최소 수준으로 유지하는 것이 중요하다. ResultSet
를 스크롤 및/또는 업데이트하려면 일반적으로 네트워크 연결이 열려 있어야 하기 때문에 많은 개발자가 이러한 기능을 사용하지 않고 있으며 심지어는 사용할 수 없는 경우도 있다.
다행스럽게도 JDBC 3.0에서는 데이터베이스 연결을 반드시 열어 놓지 않더라도 여러 가지 ResultSet
작업을 수행할 수 있도록 지원하는 대안이 도입되었다.
개념적으로 Rowset
는 기본적으로 ResultSet
이지만 연결된 또는 연결되지 않은 모델에 사용할 수 있다. Rowset
를 사용하기 위해서는 해당 오브젝트를 작성한 후 ResultSet
에서 작성된 오브젝트를 지정하기만 하면 된다. 그런 다음 해당 오브젝트가 모두 채워지면ResultSet
를 사용하듯이 사용할 수 있다(Listing 7 참조).
Listing 7. ResultSet를 대체하는 Rowset
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
if (wantsConnected)
JdbcRowSet rs = new JdbcRowSet(scrollingRS); // connected
else
CachedRowSet crs = new CachedRowSet(scrollingRS); disconnected
|
JDBC에는 Rowset
인터페이스의 다섯 가지 "구현"(확장 인터페이스)이 있다. JdbcRowSet
는 연결된 Rowset
구현이며 연결되지 않은 구현인 나머지 네 가지 구현은 다음과 같다.
CachedRowSet
는 단순히 연결되지 않은Rowset
이다.
WebRowSet
는 결과와 XML을 상호 변환하는 방법을 알고 있는CachedRowSet
의 서브클래스이다.
JoinRowSet
는 데이터베이스에 다시 연결할 필요 없이SQL JOIN
과 동등한 기능을 수행하는 방법을 알고 있는WebRowSet
이다.
FilteredRowSet
는 데이터베이스에 다시 연결할 필요 없이 다시 전달 받은 데이터를 추가로 필터링하는 방법을 알고 있는WebRowSet
이다.
Rowset
는 완전한 JavaBeans이다. 이는 곧 리스너 스타일 이벤트가 지원된다는 것을 의미하므로 원하는 경우 Rowset
에 대한 수정을 검색, 확인 및 대응할 수 있다. 실제로 Rowset
는 Username
, Password
, URL
및 DatasourceName
특성 세트(DriverManager.getConnection()
을 사용하여 연결을 작성한다는 의미) 또는 Datasource
특성 세트(아마도 JNDI를 통해 가져온 특성 세트)가 있으면 데이터베이스에 대한 전체 활동을 관리할 수 있다. 그런 다음 Command
특성에서 실행할 SQL을 지정하고 execute()
를 호출한 후 결과를 사용하여 작업을 시작할 수 있다. 이외에는 추가 작업이 필요하지 않다.
Rowset
구현은 일반적으로 JDBC 드라이버에서 제공하므로 실제 이름 및/또는 패키지는 사용하는 JDBC 드라이버에 따라 달라진다.Rowset
구현은 Java 5 이후로 표준 배포판에 포함되어 있으므로 ...RowsetImpl()
을 바로 작성해서 사용할 수 있다. (드라이버에서 이 구현을 제공하지 않는 경우를 대비해 Sun에서 참조 구현을 제공하고 있다. 참고자료에 소개된 링크를 참조하기 바란다.)
높은 유용성에도 불구하고 Rowset
가 사용자의 모든 요구를 충족하지 못하는 경우가 있으며, 결과적으로 사용자가 SQL문을 직접 작성해야 하는 상황이 발생할 수 있다. 이러한 상황에서 특히, 많은 작업을 수행해야 할 때 데이터베이스에 대한 둘 이상의 SQL문을 하나의 네트워크 라운드트립의 일부로 실행하는 일괄처리 업데이트가 큰 도움이 될 것이다.
JDBC 드라이버가 일괄처리 업데이트를 지원하는지 확인하기 위해 DatabaseMetaData.supportsBatchUpdates()
를 호출하면 결과를 알려 주는 부울 값이 표시된다. 일괄처리 업데이트가 지원된다고 간주하고(비SELECT
로 지정됨) Listing 8과 같이 호출을 큐에 저장한 후 한꺼번에 해제할 수 있다.
conn.setAutoCommit(false);
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO lineitems VALUES(?,?,?,?)");
pstmt.setInt(1, 1);
pstmt.setString(2, "52919-49278");
pstmt.setFloat(3, 49.99);
pstmt.setBoolean(4, true);
pstmt.addBatch();
// rinse, lather, repeat
int[] updateCount = pstmt.executeBatch();
conn.commit();
conn.setAutoCommit(true);
|
기본적으로 드라이버는 제공된 모든 명령문을 커미트하려고 시도하므로 setAutoCommit()
를 반드시 호출해야 한다. 이 부분을 제외한 코드의 나머지 부분은 상당히 단순하다. 즉, Statement
또는 PreparedStatement
를 사용하여 일반적인 SQL 작업을 수행한다. 하지만execute()
를 호출하는 대신 executeBatch()
를 호출한다. 이렇게 하면 호출이 직접 전송되지 않고 큐에 저장된다.
모든 명령문이 준비되었으면 executeBatch()
를 사용하여 데이터베이스에서 모든 명령문을 실행한다. 그러면 정수 값으로 구성된 배열이 리턴되며, 각 값은 executeUpdate()
를 사용했을 때와 동일한 결과이다.
일괄처리의 명령문이 실패하거나, 드라이버가 일괄처리 업데이트를 지원하지 않거나, 일괄처리의 명령문이 ResultSet
를 리턴한 경우에는 드라이버에서 BatchUpdateException
이 발생한다. 예외가 발생한 이후 드라이버가 명령문을 계속 실행하려고 시도하는 경우도 있다. JDBC 스펙에서는 특정 동작을 필수 동작으로 지정하고 있지 않기 때문에 드라이버를 미리 사용해 보면서 드라이버의 작동 방법을 정확히 파악해 두는 것이 좋다. (물론 단위 테스트를 실행하여 오류를 찾아내면 문제를 미연에 방지할 수 있을 것이다.)
Java 개발의 주요 요소인 JDBC API는 모든 Java 개발자가 자신의 손금을 보듯이 잘 알아야 한다. 하지만 아쉽게도 대부분의 개발자가 지난 수 년 동안 이 API의 개선 사항을 확인하지 않았으며, 결과적으로 이 기사에서 설명한 시간을 절약할 수 있는 기술을 놓치고 있다.
물론 JDBC의 새 기능을 사용할지 여부는 어디까지나 개발자의 결정에 달려 있다. 한 가지 중요한 고려 사항은 작업 중인 시스템의 확장성이다. 확장 필요성이 높을수록 데이터베이스 사용에 대한 제약이 많아지며, 따라서 데이터베이스에 대한 네트워크 트래픽을 더 많이 줄여야 한다. Rowset
, 스칼라 호출 및 일괄처리 업데이트가 이러한 상황에 많은 도움이 될 것이다. 그렇지 않은 경우 스크롤 가능 및 업데이트 가능 ResultSet
(Rowset
보다 메모리 사용량이 낮음)를 사용해 보고 확장성을 측정해 보자. 기대보다 좋은 결과를 얻을 수 있을 것이다.
5가지 사항 시리즈의 다음 주제는 명령행 플래그이다.
교육
- 모르고 있던 5가지 사항: Java 플랫폼에 대해 모르는 것이 많았다는 것을 일깨워 주는 이 시리즈에서는 사소하게 여겼던 Java 기술을 유용한 프로그래밍 팁으로 바꿔준다.
- "Get a head start with JDBC 4.0 using Apache Derby"(Victor J. Soderberg 저, developerWorks, 2006년 8월): 이 튜토리얼에서는 JDBC 4.0 스펙의 몇 가지 함수를 Apache Derby 데이터베이스와 함께 사용하는 방법을 보여 준다.
- "JDBC 4.0 enhancements in Java SE 6"(Srini Penchikala 저, onJava.com, 2006년 8월): 최신 JDBC API 버전 릴리스에 추가된 시간을 절약할 수 있는 기능에 대한 자세한 정보를 볼 수 있다.
- developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술 얻기
- JDBC 4.0 API Specification: 자신의 손금을 보듯이 최신 JDBC 스펙을 자세히 살펴보자.
- JDBC Rowset Implementations 1.0.1 Specification: JDBC
Rowset
RI를 다운로드할 수 있다.
토론
- My developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.
'IT_Programming > Java' 카테고리의 다른 글
JVM의 DNS 캐시 (0) | 2011.04.03 |
---|---|
커스텀 DecoratorMapper를 이용한 SiteMesh URL 패턴 매칭 기능 확장 (0) | 2011.04.03 |
[펌] 자바 1.4의 새기능: Assertion (0) | 2011.03.11 |
[펌] 멀티스레드 프로그래밍에 대해 모르고 있던 5가지 사항 (0) | 2011.02.23 |
Zip File 형태의 클래스 로드하기 (0) | 2011.01.07 |