IT_Programming/Java

Java Database Connectivity에 대해 모르고 있던 5가지 사항

JJun ™ 2011. 3. 29. 09:55

------------------------------------------------------------------------------------------------

출처: 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 개발자가 직면하고 있는 공통 과제인 애플리케이션 확장성과 개발자 생산성이라는 두 가지 과제를 지원한다.

 

1. 스칼라 함수

다양한 RDBMS 구현에서 개발자가 쉽게 작업할 수 있도록 지원하기 위해 설계된 SQL 및/또는 부가 기능에 대한 비정규적인 지원을 제공하고 있다. 예를 들어, SQL에 특정 SQL 필터 기준(즉, WHERE 조건부)을 충족하는 행의 수를 리턴하는 COUNT()라는 스칼라 오퍼레이션이 있다는 것은 잘 알려져 있다. 하지만 이 수준을 넘어서 SQL에서 리턴된 값을 수정하는 작업은 매우 까다로우며 데이터베이스에서 현재 날짜 및 시간을 가져오려고 할 경우에는 인내심이 많은 JDBC 개발자도 너무 복잡해서 두통을 호소하기도 한다.

이 문제를 해결하기 위해 JDBC 스펙에서는 스칼라 함수를 통해 다양한 RDBMS 구현을 일정 수준 분리/채택할 수 있는 기능을 제공한다. JDBC 스펙에는 JDBC 드라이버가 특정 데이터베이스 구현에 필요한 것으로 인식하여 채택해야 하는 지원되는 오퍼레이션 목록이 포함되어 있다. 따라서 현재 날짜 및/또는 시간을 리턴하는 기능을 지원하는 데이터베이스의 경우 Listing 1처럼 간단하게 현재 날짜 및 시간을 가져올 수 있다.


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 참조).


Listing 2. 무엇을 할 수 있는가?

Connection conn = ...; // get it from someplace
DatabaseMetaData dbmd = conn.getMetaData();

스칼라 함수 목록은 다양한 DatabaseMetaData 메소드에서 리턴되는 쉼표로 구분된 String이다. 예를 들어, 모든 숫자 스칼라는getNumericFunctions() 호출을 통해 나열된다. 결과에 대해 String.split()을 수행하면 그 즉시 equals()-testable 목록이 표시된다.

2. 스크롤 가능한 ResultSet

JDBC에서 Connection 오브젝트를 작성하거나 기존 오브젝트를 가져온 후 이 오브젝트를 사용하여 Statement를 작성하는 프로시저는 매우 일반적인 프로시저이다. Statement에서는 SQL SELECT를 사용하여 ResultSet를 리턴한다. 그런 다음 ResultSet는 왼쪽에서 오른쪽으로 한 번에 한 열씩 추출하는 본문으로 구성된 while 루프(Iterator와 유사함)를 통해 ResultSet가 비워질 때까지 제공된다.

이 전체 오퍼레이션은 대부분의 개발자가 당연하게 생각할 정도로 일반적으로 수행되고 있으며 지금까지 그렇게 해왔다는 이유만으로 습관적으로 이루어지고 있다. 하지만 이는 완전히 불필요한 조작이다.

스크롤 가능한 ResultSet 소개

JDBC의 새 버전 번호와 릴리스에 개선 사항이 반영되어 있음에도 불구하고 JDBC가 지난 여러 해 동안 상당히 개선되었다는 것을 모르는 개발자가 많다. 첫 번째 주요 개선 사항인 JDBC 2.0은 JDK 1.2와 비슷한 시기에 개발되었다. 이 기사의 작성 시점을 기준으로 JDBC의 최신 버전은 4.0이다.

JDBC 2.0의 흥미로운(그러면서도 자주 무시되는) 개선 사항 중 하나는 ResultSet를 "스크롤"하는 기능이다. 이는 필요에 따라 앞으로, 뒤로 또는 앞뒤로 이동할 수 있다는 것을 의미한다. 하지만 이 작업을 수행하려면 미리 생각해서 처리해야 하는 작업이 있다. 즉, JDBC 호출에서 Statement가 작성되는 시점에 스크롤 가능한 ResultSet를 사용할 것임을 지정해야 한다.

ResultSet 유형 확인하기

DatabaseMetaData에 드라이버가 스크롤 가능한ResultSet를 지원한다고 표시되어 있음에도 불구하고 실제로 지원하는지 의심스러운 경우 getType()을 호출하여ResultSet 유형을 확인할 수 있다. 물론 사용자가 편집증적이라면 getType()의 리턴값도 믿지 못할 것이다. 설마하니 getType()이 리턴된 ResultSet에 대해 거짓말을 하겠는가?

기본 JDBC 드라이버가 스크롤을 지원하는 경우에는 스크롤 가능한ResultSet가 해당 Statement에서 리턴된다. 하지만 스크롤 가능한ResultSet를 요청하기 전에 드라이버가 스크롤 기능을 지원하는지 먼저 확인하는 것이 좋다. 앞에서 설명한 대로 Connection에서 가져올 수 있는 DatabaseMetaData 오브젝트를 통해 스크롤 기능을 확인할 수 있다.

DatabaseMetaData 오브젝트를 작성한 후에는getJDBCMajorVersion()을 호출하여 드라이버가 적어도 JDBC 2.0 스펙을 지원하는지 여부를 확인한다. 물론 드라이버가 지정된 스펙에 대한 지원 레벨을 잘못 알려줄 수도 있으므로 특별히 안전하게 실행하기 위해 원하는 ResultSet 유형을 사용하여 supportsResultSetType() 메소드를 호출한다. (유형은 ResultSet 클래스에 상수로 정의되어 있으며 잠시 후 각 유형의 값에 대해 살펴본다.)


Listing 3. 스크롤 가능한가?

int JDBCVersion = dbmd.getJDBCMajorVersion();
boolean srs = dbmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
if (JDBCVersion > 2 || srs == true)
{
    // scroll, baby, scroll!
}

스크롤 가능한 ResultSet 요청하기

드라이버가 예라고 응답한 것으로 가정하면(그렇지 않은 경우에는 새 드라이버 또는 데이터베이스가 필요함)Connection.createStatement() 호출에 두 매개변수를 전달하여 스크롤 가능한 ResultSet를 요청할 수 있다(Listing 4 참조).


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는 스크롤 방향에 상관 없이 작동하지만 방향을 미리 알고 있으면 데이터 검색을 최적화할 수 있다.)

3. 업데이트 가능한 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()를 호출하여 보류 중인 모든 업데이트를 취소할 수 있다.


Listing 6. 더 나은 방법

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() 메소드에 전달).

4. Rowset

거의 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에서 참조 구현을 제공하고 있다. 참고자료에 소개된 링크를 참조하기 바란다.)

5. 일괄처리 업데이트

높은 유용성에도 불구하고 Rowset가 사용자의 모든 요구를 충족하지 못하는 경우가 있으며, 결과적으로 사용자가 SQL문을 직접 작성해야 하는 상황이 발생할 수 있다. 이러한 상황에서 특히, 많은 작업을 수행해야 할 때 데이터베이스에 대한 둘 이상의 SQL문을 하나의 네트워크 라운드트립의 일부로 실행하는 일괄처리 업데이트가 큰 도움이 될 것이다.

JDBC 드라이버가 일괄처리 업데이트를 지원하는지 확인하기 위해 DatabaseMetaData.supportsBatchUpdates()를 호출하면 결과를 알려 주는 부울 값이 표시된다. 일괄처리 업데이트가 지원된다고 간주하고(비SELECT로 지정됨) Listing 8과 같이 호출을 큐에 저장한 후 한꺼번에 해제할 수 있다.


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 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자. 

제품 및 기술 얻기

토론

  • My developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다. 

필자소개

Ted Neward는 글로벌 컨설팅 업체인 ThoughtWorks의 컨설턴트이자 Neward & Associates의 회장으로 Java, .NET, XML 서비스 및 기타 플랫폼에 대한 컨설팅, 조언, 교육 및 강연을 한다. 워싱턴 주의 시애틀 근교에 살고 있다.