JDBC에서 Connection, Statement,ResultSet의 close

이미 많이 알려진 내용이지만, 아직도 문제를 많이 일으키는 주제입니다. 그래서 보다 이 주제를 검색엔진에서 쉽게 찾을 수 있었으면 하는 마음에서 이 글을 정리해봤습니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  ResultSet rs = null; // <---- !!!
  try{
     conn = ...<getConnection()>...;
     pstmt = conn.prepareStatement("select .....");
     rs = pstmt.executeQuery(); // <----- !!!
     while(rs.next()){
       ......
     }
  }  catch(Exception e){
     ....
  }  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( pstmt != null ) try{pstmt.close();}catch(Exception e){}
     if ( conn != null ) try{conn.close();}catch(Exception e){}

 }

이것이 JDBC API 사용시에 권장되는 코딩방식입니다. 코드는 참조자료에 있는 이원영님의 글에서 인용했습니다.

JDBC 스펙을 찾아보면 Statement가 닫힐 때 ResultSet은 닫히고, Connection이 닫히면 Statement도 닫힌다고 되어 있습니다. 하지만 Staement close 시에 Exception이 발생한다면 이것이 따로 Exception을 catch되지 않고서는 뒤에 Connection을 닫는 코드가 실행되지 않습니다. 그리고 Connection pool에서 얻어온 Connection객체는 connection.close()로 처리하는 것이 pool로의 반환을 의미하는 것이지 실제로 connetion을 close하는 것이 아니기 때문에 Statement까지 닫아준다고 장담할 수 없습니다. ResultSet의 경우도 WAS에도 제공하는 Statement cache 기능 때문에 명시적으로 close해주는 것이 확실한 자원해제를 보장할 수 있습니다.

DBMS에서 "maximum open cursor exceed !" 나 "Limit on number of statements exceeded " 에러를 내고 있다면 위와 같이 코딩했는지 한번 확인해보시기 바랍니다.

각 벤더별 드라이버의 구현이나 WAS의 Connection Pool의 구현등에 따라서 저 정도까지 안 해도 문제가 안 생길 수도 있습니다. 그리고 독립적으로 돌아가는 배치프로그램이나 커넥션풀을 쓰지 않는 경우에는 보다 덜 엄격해도 될 때도 있습니다. 그래도 어떠한 경우에도 안심하고 있을만한 코드는 위와 같은 구조입니다.

javaservice.net에서 이원영님이 처음에 이 문제에 대한 글을 쓰신것이 2000년 9월입니다. 그래서 많은 분들이 알고 계시지만 그래도 정말 반복적으로 만나게 되는 문제입니다. 저의 경험이 편향된지도 모르겠지만, 지금까지 제가 만났던 JDBC AP를 그대로 쓰는 개발팀은 세 팀이였었는데, 모두 이렇게 코딩하지 않을 경우 문제가 생길 가능성이 있다는 것을 모르고 있었습니다. 결국 그 중 한 팀은 시스템 전체를 몇 일동안 매시간마다 재부팅시키게 만들게 했었습니다.

미국의 모 대형항공사의 예약시스템을 3시간동안 멈춘 코드도 위와 같은 방식을 따르지 않았었습니다. finally절이 다음과 같았다고 합니다.

} finally{
    if (stmt!=null) stmt.close();
    if (conn!=null) conn.close();
}

그 예약 시스템은 이중화된 DB로 구성되어 있었고, 그 DB들은 가상IP주소로 어플리케이션과 연결되어 있었습니다. 정기 점검을 위해 DB중 하나를 수동 fail-over 시키는 순간 내려간 DB의 JDBC연결에서 나온 statement객체의 close문장은 Exception을 일으켰습니다. 이 문장은 별도로 catch 되지 않았기 때문에 그 다음의 conn.close()는 실행되지 않았습니다. 결국 이 때문에 반환되지 않은 Connection 자원들로 인해 리소스 풀은 곧 바닥이 났습니다. 그 후에 새로 Connection을 얻고자 하는 다른 프로그램들은 블록되어서 전체 시스템을 멈추었습니다.

아마도 JDBC API를 쓰는 곳에는 언제나 생길 수 있는 문제일 것입니다. 좋은 API는 문서를 안 보고 자연스럽게 써도 사용하기 쉽고 문제를 안 일으키는 것일텐데, JDBC는 제대로 사용하기가 오히려 더 어려운 API입니다. 위의 항공사 사건 같이 전 세계에서 JDBC로 인해 야기된 장애,생산성 저하를 다 따져본다면, 가히 이 API가 인류에게 끼친 해악이 엄청나다는 생각까지도 듭니다. 요즘은 Framework 기반 개발로 JDBC를 직접 안 쓰는 것이 이런 점에서는 다행입니다.

JDBC API에서 대표적으로 지적받는 문제점은 Checked Exception을 남발했다는 것입니다. catch 절에서 아무 것도 하지 않는 것은 바람직하지 않은 코딩이지만 JDBC API에서는 정말 할 것이 없습니다. 그래서 이런 문제점을 알고서 그 후에 나온 JDBC를 활용한 API들, Spring의 JdbcTemplet, HibernateQuery 인터페이스, JPAQuery 인터페이스, JDOQuery 인터페이스에서는 Checked Exception인 SqlException을 볼 수 없게 설계되어 있습니다.

그리고 Java6 이전의 JDBC에서는 접속에러, 쿼리에러, 제약조건 에러 등 다양한 원인으로 생기는 Exception을 SqlException 1개로 다 때우는 문제도 있었습니다. Spring에서는 이것을 더 섬세하게 구분한 Exception들을 정의를 하고 있습니다. DataAccessException의 하위 클래스를 보면 CleanupFailureDataAccessException, DataIntegrityViolationException, DataRetrievalFailureException 등이 보입니다. Java6에 포함된 JDBC 4.0에서는 SQLNonTransientException, SQLRecoverableException, SQLTransientException 등의 하위 클래스가 생겼고, ,Spring에서는 이런 클래스도 잘 인식해서 적절한 DataAccessException의 하위 클래스로 변환해줍니다.

참고자료

미국 항공사 장애 사건 관련

Digester를 이용한 Naver Open API Java Client 모듈

네이버 Open API Cafe에서 검색 API의 Java Client 모듈을 보게 되었습니다.

위의 모듈을 참고해서 같은 역할을 하는 모듈을 다르게 구현해봤습니다.

특징은 아래와 같습니다.

  • RSS를 파싱하는 부분을 XML parsing API로 널리 알려진 Digester(http://commons.apache.org/digester/)를 사용했습니다. Digester의 예제 코드로 제공되는 RSS파싱모듈을 그대로 써서 짧은 코드로 파싱이 가능했습니다.

  • 요청 파라미터를 담는 클래스를 따로 뺐습니다. (RequestParameter.java) 이중 Target 값은 (blog, news 등 검색할 컨텐츠 유형을 선택하는 파라미터입니다.) enum으로 해서 정해진 값이 아닐 경우 compile이 안 되게 했습니다.

  • open API key값은 필수값이므로 OpenApiClient클래스의 생성자의 파라미터로 받았습니다. 대신 키 값이 없이 이 객체가 생성될 수 없도록 default 생성자는 private으로 돌려놨습니다.

첨부한 파일은 이클립스에서 Dynamic Web Project로 생성한 폴더를 압축한 것입니다. 테스트 실행 서버는 Tomcat 5.5를 사용했습니다. Eclipse WTP가 설치되어 있는 환경이면 실행이 가능합니다. 그리고 enum을 썼기에 Java5이상이어야 합니다.

Open API에 대한 자세한 사용법은 http://openapi.naver.com/index.nhn 를 참조하시면 됩니다.

소스코드

NaverSearchClient.java
package openapiclient;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;

import org.apache.commons.digester.rss.Channel;
import org.apache.commons.digester.rss.RSSDigester;

public class NaverSearchClient {

    private static final String OPEN_API_URL = "http://openapi.naver.com/search";
    private String key;

    @SuppressWarnings("unused")
    private NaverSearchClient(){};

    public NaverSearchClient(String key){
    this.key = key;
    }
    public Channel search(RequestParameter param) throws Exception{
        RSSDigester digester = new RSSDigester();
        URL requestUrl = getRequestUrl(param);
        InputStream is = requestUrl.openConnection().getInputStream();
     return (Channel) digester.parse(is);
    }

    private URL getRequestUrl(RequestParameter param) throws UnsupportedEncodingException, MalformedURLException {
        StringBuffer serverUrl = new StringBuffer(OPEN_API_URL);
        serverUrl.append("?target=" + param.getTarget());
        serverUrl.append("&key=" + key);
        serverUrl.append("&start=" + param.getStart());
        serverUrl.append("&display=" + param.getDisplay());
        serverUrl.append("&query=" + URLEncoder.encode(param.getQuery(), "UTF-8"));
        if(param.getSort()!=null) serverUrl.append("&sort=" + param.getSort());
        return new URL(serverUrl.toString());
    }
}
RequestParameter.java
package openapiclient;

public class RequestParameter {

 public enum Category{
  KIN,BLOG,CAFE,DOC,WEBKR,BOOK, SHOP, ENCYC,
  KRDIC, JPDIC, ENDIC, NEWS, LOCAL, VIDEO,IMAGE;
  public String toString(){
   return super.toString().toLowerCase();
  }
 }

 private Category target;
 private String sort;
 private int start;
 private int display;
 private String query;

 // getter and setters 생략
 }

JSP에서 사용한 예제

JSTL을 함께 사용해서 찍어본 예제입니다. http://openapi.naver.com/index.nhn 에 가셔서 API key를 발급 받으시고 소스 중간에 밑줄로 표시된 부분에 그 값을 넣으시고 돌려주시면 됩니다.

<%@ page language="java" contentType="text/html; charset=EUC-KR"   pageEncoding="EUC-KR"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page import="openapiclient.RequestParameter" %>
<%@ page import="openapiclient.NaverSearchClient" %>
<%@ page import="org.apache.commons.digester.rss.Channel" %>
<%
 String KEY = "????"; // Open API key값을 넣으세요
 NaverSearchClient client = new NaverSearchClient(KEY);
 RequestParameter param = new RequestParameter();
 param.setDisplay(10);
 param.setStart(1);
 param.setQuery("미역국");
 param.setTarget(RequestParameter.Category.NEWS);
 Channel result = client.search(param);
 result.render(System.out); // 콘솔에 받아온 내용을 확인삼아 찍어봄
 request.setAttribute("result", result);
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Naver Open API를 이용한 검색</title>
</head>
<body>
<c:forEach var='item' items='${result.items}'>
  <p>
  <a href="${item.link}"> ${item.title} </a> <br/>
  ${item.description}
  </p>
</c:forEach>
</body>
</html>

실행결과화면

openApiClient.JPG

Batch job에서의 바람직한 트랜잭션 처리 방식

일반적인 온라인 어플리케이션에서는 사용자가 보내는 event 한번에 하나의 트랜잭션을 묶어서 처리합니다. 하지만 배치처리에서는 하나의 처리가 하나의 트랜잭션으로 관리되는 것은 때로는 문제가 될 여지가 있습니다.

몇 만 건 이상의 행과 같이 대용량 데이터를 다루는 경우에는 roll back segment 같은 공간의 부족현상이 생길 위험이 있습니다. 그리고 상대적으로 긴 처리 시간동안 DB에 lock을 걸리게 해서 전체 시스템에 병목을 만들지도 모릅니다. 사용자가 많은 웹어플리케이션이 동시에 접근하는 DB에서 배치처리를 돌릴 때는 이를 더 염두에 두어야 할 것입니다.

또, 데이터가 건마다 독립성을 가지고 있는 경우라면 한 두건의 처리실패 때문에 앞에 성공한 건들도 다 Roll back이 되어버린다면 더 많은 건의 데이터가 의도하지 않은 상태로 오래 유지됩니다. 배치처리의 에러가 발생 즉시 해결되지 않을 수도 있기 때문에 때문에 업무 규칙상으로 가능한 경우라면 부분적으로 성공한 건이라도 DB에 반영되는 것이 좋습니다. 그렇지 않을 경우에 재시도시에 다시 모든 데이터를 처리해야 하니 그 처리시간이 더 길어지게 됩니다.

그렇다고 해서 데이터 매 건마다 따로 트랜잭션 처리를 하는 것도 성능상 좋지 못합니다. 트랜잭션을 시작하고 끝낼 때 드는 비용도 감안해야 되기 때문입니다.

따라서 일정한 행 단위를 묶어서 처리를 하는 것이 배치처리에서는 더 바람직한 방식입니다.

WebSphere XD Compute Grid에서는 check-point algorithm을 time-based와 record-based 두 가지 방식으로 제공해서 이런 문제에 대한 해결책을 제시하고 있습니다. Spring batch에서는 SimpleStepFactoryBean 클래스에서 commitInterval이라는 속성으로 주기적인 commit을 설정할 수 있습니다. 데이터 특성이나 업무규칙에 따라서 적절한 값을 트랜잭션 단위로 묶이는 건수를 지정할 수 있는 것입니다.

참고자료 1

세번째 페이지의 Job Partitioning and Chunking 단락에서 아래와 같이 설명하고 있습니다.

Running your job as one long-lived transaction isn’t a good idea. Long-lived transactions are more fragile than short-lived transactions. They are more susceptible to errors and DBMS locking problems because the locks are held longer. Smaller chunks are more reliable and less likely to exceed your connection and session timeouts.

참고자료2

Oracle의 경우를 설명한 글입니다. DBA들이 보통 긴 transaction을 유지하는 batch job에 전용 role back segment를 할당하는데 실제로는 이것이 별도움이 못 된다고 말합니다. 결국 다른 session들이 사용할 수 있는 roll back segment를 사용하는 것은 똑같기 때문에 ORA-01555 에러를 유발할 수 있다고 합니다. 결국 보다 작은 transaction 단위를 가지고 가는 것이 근본적인 해결책입니다.

그리고 실패시 재시도를 위해서 지난 번 처리한 영역까지 표시하거나 앞에 성공한 건들을 다 지워줘야 하는 경우 때문에 큰 transaction을 가지고 가는 문제가 .생기는데, 재시도를 위한 기능을 트랜잭션에서 제공받기 전에 응용프로그램에서 적절한 로직으로 처리하는 것이 필요하다고 나와 있습니다.

The problem that needs to be addressed in cases such as these is the design of the batch processes that require such a huge transaction. Transactions in Oracle should normally be kept fairly short. While it is undesirable to commit for every row processed (which will cause excessive redolog buffer flushing and high waits on “log file sync”), it makes sense to have batch processes commit for every few hundred rows processed.

Often the greatest barrier to changing batch jobs to commit continuously is failure tolerance. If a batch job that commits continuously fails part way through, then there must be a way to restart that batch job where it left off, or clean up from the first attempt so that the job can be started over. Whereas before this restart capability was provided by rolling back the large transaction, the proposed rollback-friendly model requires that the appropriate application logic be built into the batch processing software.

If you allow your semi-frequently committing batch jobs to randomly select rollback segments like all the rest of the transactions in your system, you will be less likely to overwrite recently committed changes, since the burden of the batch transactions is spread around, rather than concentrated in a single rollback segment

참고자료3

WebSphere XD에서 제공하고 있는 check-point algorithm에 대한 설명을 볼 수 있습니다.

참고자료4

Spring batch에서 주기적인 commit을 설정하는 방법입니다.