달력

52025  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

http://www-128.ibm.com/developerworks/kr/library/j-ajax1/

Ajax를 이용한 웹 애플리케이션 구현

난이도 : 중급

Philip McCarthy, 소프트웨어 개발 컨설턴트 , Independent Consultant

2005 년 9 월 20 일

페이지 리로드 사이클은 웹 애플리케이션 개발에 있어서 가장 큰 사용 장애이자 자바 개발자들에게는 심각한 도전 과제이다. Philip McCarthy가 혁신적인 동적 웹 애플리케이션 구현 방법을 소개한다. Ajax (Asynchronous JavaScript and XML)는 자바, XML, JavaScript가 혼합된 프로그래밍 기술이다. 자바 기반 웹 애플리케이션의 페이지 리로드 패러다임을 과감히 바꾼다.

Ajax (Asynchronous JavaScript and XML)는 클라이언트 측 스크립팅을 사용하는 웹 애플리케이션 개발 방식으로서 데이터를 웹 서버와 교환한다. 따라서 웹 페이지는 동적으로 업데이트 될 수 있다. 전체 페이지를 리프레시 하여 인터랙션 플로우에 영향을 끼치지도 않는다. Ajax를 사용하여 보다 풍부하고 동적인 웹 애플리케이션 사용자 인터페이스를 만들 수 있다. 원시 데스크탑 애플리케이션의 가용성에 접근한 풍부하고 동적인 웹 애플리케이션 사용자 인터페이스이다.

Ajax는 기술이 아니다. 오히려 패턴에 더 가깝다. Ajax는 많은 개발자들에게는 생소할지 모르지만, Ajax 애플리케이션을 구현한 모든 컴포넌트들은 여러 해 동안 존재했다. 2004년과 2005년에 Ajax 기술에 기반한 동적 웹 UI가 생겨나면서 주목을 받게 되었다. 대표적인 예로 Google의 Gmail과 Maps 애플리케이션 그리고 사진 공유 사이트인 Flickr을 들 수 있다. 이러한 UI들은 몇몇 웹 개발자들에 의해 "Web 2.0"으로 불릴 만큼 혁신적이었다. 이에 따라 Ajax 애플리케이션에 대한 인기도 하늘로 치솟았다.

이 시리즈를 통해 Ajax를 사용하여 애플리케이션을 개발할 때 필요한 모든 툴들을 소개하겠다. 우선 이 첫 번째 글에서는 Ajax의 개념을 설명하고, 자바 기반 웹 애플리케이션에 Ajax 인터페이스를 구현하는 기본적인 단계들을 설명하겠다. 코드 예제를 사용하여 서버측 자바 코드와 클라이언트측 JavaScript를 설명하겠다. 이것이 바로 동적인 Ajax 애플리케이션의 핵심축을 구성한다. 마지막으로 Ajax 방식의 단점과 Ajax 애플리케이션을 구현할 때, 반드시 고려해야 할 가용성 및 접근성 문제들도 설명하도록 하겠다.

더 나은 쇼핑 카트

Ajax를 사용하여 전통적인 웹 애플리케이션을 만들 수 있다. 페이지 부하를 줄여서 인터랙션을 간소하게 할 수 있다. 아이템들이 추가될 때 마다 동적으로 업데이트 되는 쇼핑 카트 예제를 설명하겠다. 온라인 스토어와 결합된 이 방식을 사용하면, 사용자들은 전체 페이지가 업데이트 될 때까지 기다리지 않고도 카트에 아이템들을 검색 및 추가할 수 있다. 이 글에 소개한 몇몇 코드는 이 쇼핑 카트 예제에만 국한된 것이지만, 여기에 사용된 기술들은 Ajax의 어떤 애플리케이션에나 적용될 수 있다. Listing 1은 쇼핑 카트 예제에서 사용되는 HTML 코드이다.


Listing 1. 쇼핑 카트 예제
<!-- Table of products from store`s catalog, one row per item -->
<th>Name</th> <th>Description</th> <th>Price</th> <th></th>
...
<tr>
  <!-- Item details -->
  <td>Hat</td> <td>Stylish bowler hat</td> <td>$19.99</td>
  <td>
    <!-- Click button to add item to cart via Ajax request -->
    <button onclick="addToCart(`hat001`)">Add to Cart</button>
  </td>
</tr>
...

<!-- Representation of shopping cart, updated asynchronously -->
<ul id="cart-contents">

  <!-- List-items will be added here for each item in the cart -->
 
</ul>

<!-- Total cost of items in cart displayed inside span element -->
Total cost: <span id="total">$0.00</span>




위로


Ajax 작동

Ajax 인터랙션은 XMLHttpRequest라고 하는 JavaScript 객체로 시작한다. 이름만 보아도 알겠지만 이것은 클라이언트측 스크립트가 HTTP 요청을 수행하도록 하고 XML 서버 응답을 파싱한다. Ajax 연구의 첫 번째 단계는 XMLHttpRequest 인스턴스를 만드는 것으로 시작한다. 이 요청(GET 또는 POST)에 사용되는 HTTP 메소드와 도착지 URL은 XMLHttpRequest 객체에서 설정된다.

이제 Ajax의 첫 번째 단어 a가 비동기식(asynchronous)을 의미한다는 것을 기억하라. HTTP 요청을 보내면 서버가 응답할 때 까지 기다릴 것을 브라우저에 요청하지 않아도 된다. 대신 사용자와 페이지 간 인터랙션에 지속적으로 반응하고, 서버의 응답이 도착하면 이를 다루도록 요청한다. 이를 위해서 XMLHttpRequest로 콜백 함수를 등록하고, XMLHttpRequest를 비동기식으로 실행한다. 컨트롤이 브라우저에 리턴되지만, 콜백 함수는 서버 응답이 도착하면 호출될 것이다.

자바 웹 서버에 요청은 다른 HttpServletRequest와 마찬가지로 도착한다. 요청 매개변수를 파싱한 후에 서블릿은 필요한 애플리케이션 로직을 호출하고, 응답을 XML로 직렬화 하고, 이를 HttpServletResponse에 작성한다.

다시 클라이언트로 돌아가서, XMLHttpRequest에 등록된 콜백 함수가 서버에서 리턴된 XML 문서를 처리하도록 호출된다. 마지막으로, 사용자 인터페이스는 서버로부터 온 데이터에 대한 응답으로 업데이트된다. 이때 JavaScript를 사용하여 페이지의 HTML DOM을 조작한다. 그림 1은 Ajax의 시퀀스 다이어그램이다.


그림 1. Ajax 시퀀스 다이어그램 - Sequence diagram of the Ajax roundtrip

위 그림은 고급 시퀀스 다이어그램이다. 각 단계를 자세히 설명하도록 하겠다. 그림 1에서 보면 Ajax의 비동기식 방식 때문에 시퀀스가 단순하지 않다.




위로


XMLHttpRequest 실행

Ajax 시퀀스의 시작 부분부터 설명하겠다. 브라우저에서 XMLHttpRequest를 생성 및 실행하는 Ajax 시퀀스의 시작 부분부터 설명하겠다. 불행히도 XMLHttpRequest를 만드는 방식은 브라우저마다 다르다. Listing 2의 JavaScript 함수는 부드럽게 진행되면서 현재 브라우저에 맞는 정확한 방식을 찾고, 사용 준비가 된 XMLHttpRequest를 리턴한다. 이것을 JavaScript 라이브러리에 복사하여 XMLHttpRequest가 필요할 때 사용하도록 한다.


Listing 2. 크로스 브라우저 XMLHttpRequest 구현하기

/*
 * Returns a new XMLHttpRequest object, or false if this browser
 * doesn`t support it
 */
function newXMLHttpRequest() {

  var xmlreq = false;

  if (window.XMLHttpRequest) {

    // Create XMLHttpRequest object in non-Microsoft browsers
    xmlreq = new XMLHttpRequest();

  } else if (window.ActiveXObject) {

    // Create XMLHttpRequest via MS ActiveX
    try {
      // Try to create XMLHttpRequest in later versions
      // of Internet Explorer

      xmlreq = new ActiveXObject("Msxml2.XMLHTTP");

    } catch (e1) {

      // Failed to create required ActiveXObject

      try {
        // Try version supported by older versions
        // of Internet Explorer

        xmlreq = new ActiveXObject("Microsoft.XMLHTTP");

      } catch (e2) {

        // Unable to create an XMLHttpRequest with ActiveX
      }
    }
  }

  return xmlreq;
}
  

나중에 XMLHttpRequest를 지원하지 않는 브라우저를 다루는 방법을 설명하겠다. 지금은 Listing 2의 새로운 newXMLHttpRequest가 언제나 XMLHttpRequest를 리턴하는 것으로 간주한다.

쇼핑 카트 시나리오 예제로 돌아가서, 사용자가 해당 카탈로그 아이템에서 addToCart() 버튼을 누를 때 마다 Ajax 인터랙션을 호출하도록 할 것이다. addToCart()라는 onclick 핸들러 함수는 Ajax 호출 시 카트의 상태를 업데이트 한다.(Listing 1). Listing 3에서 보듯 addToCart()가 해야 하는 첫 번째 일은 Listing 2에서 newXMLHttpRequest()함수를 호출하여 XMLHttpRequest의 인스턴스를 획득하는 것이다. 다음에는 콜백 함수를 등록하여 서버의 응답을 받는 것이다. (Listing 6).

요청으로 인해 서버상의 상태가 변경되기 때문에 HTTP POST가 이 일을 수행하도록 할 것이다. POST 를 통해 데이터를 보내려면 세 단계를 거쳐야 한다. 우선, 통신하고 있는 서버 리소스에 대한 POST에 연결한다. 이 경우 URL cart.do에 매핑된 서블릿이다. 그런 다음 XMLHttpRequest에 헤더를 설정한다. 이때 요청 내용은 폼 인코딩(form-encoded) 데이터라는 것을 언급한다. 마지막으로 폼 인코딩 데이터를 가진 요청을 바디(body)로서 보낸다.

Listing 3은 그 세 단계를 나타낸다.


Listing 3. Add to Cart XMLHttpRequest 실행

/*
 * Adds an item, identified by its product code, 
 * to the shopping cart
 * itemCode - product code of the item to add.
 */
function addToCart(itemCode) {

  // Obtain an XMLHttpRequest instance
  var req = newXMLHttpRequest();

  // Set the handler function to receive callback notifications
  // from the request object
  var handlerFunction = getReadyStateHandler(req, updateCart);
  req.onreadystatechange = handlerFunction;
  
  // Open an HTTP POST connection to the shopping cart servlet.
  // Third parameter specifies request is asynchronous.
  req.open("POST", "cart.do", true);

  // Specify that the body of the request contains form data
  req.setRequestHeader("Content-Type", 
                       "application/x-www-form-urlencoded");

  // Send form encoded data stating that I want to add the 
  // specified item to the cart.
  req.send("action=add&item="+itemCode);
}


지금까지 Ajax 설정의 첫 번째 부분을 보았다. 주로 HTTP 요청을 클라이언트에서 생성하여 실행하는 것이다. 다음에는 이 요청을 핸들할 때 사용하는 자바 서블릿 코드이다.




위로


서블릿 요청 핸들링

서블릿으로 XMLHttpRequest를 핸들하는 것은 브라우저에서 일반적인 HTTP 요청을 핸들하는 것과 비슷하다. 포스트 바디(body)에 보내진 폼 인코딩 데이터는 HttpServletRequest.getParameter() 호출로 얻을 수 있다. Ajax 요청은 애플리케이션의 일반 웹 요청과 같은 HttpSession에서 발생한다. 이것은 쇼핑 카트 시나리오 예제에 유용하다. 왜냐하면 사용자의 쇼핑 카트 상태를 JavaBean에 캡슐화 하고, 그 상태를 요청들 간 세션에 지속시키기 때문이다.

Listing 4는 Ajax 요청을 핸들하여 쇼핑 카트를 업데이트 하는 간단한 서블릿의 일부이다. Cart빈은 사용자 세션에서 검색되고, 상태는 요청 매개변수에 따라 업데이트 된다. Cart는 XML로 직렬화 되고, 그 XML은 ServletResponse에 작성된다. 응답 내용 유형을 application/xml로 설정하는 것이 중요하다. 그렇지 않으면 XMLHttpRequest는 응답 내용을 XML DOM으로 파싱할 것이다.


Listing 4. Ajax 요청을 핸들하는 서블릿 코드

public void doPost(HttpServletRequest req, 
  HttpServletResponse res)
      throws java.io.IOException {

  Cart cart = getCartFromSession(req);

  String action = req.getParameter("action");
  String item = req.getParameter("item");
  
  if ((action != null)&&(item != null)) {

    // Add or remove items from the Cart
    if ("add".equals(action)) {
      cart.addItem(item);

    } else if ("remove".equals(action)) {
      cart.removeItems(item);

    }
  }

  // Serialize the Cart`s state to XML
  String cartXml = cart.toXml();

  // Write XML to response.
  res.setContentType("application/xml");
  res.getWriter().write(cartXml);
}


Listing 5는 Cart.toXml() 메소드에서 만들어진 XML 예제이다. 매우 단순하다. cart 엘리먼트의 generated 애트리뷰트에 주목하라. System.currentTimeMillis()에서 만들어진 타임스템프이다.


Listing 5. Cart 객체의 XML 직렬화 예제
<?xml version="1.0"?>
<cart generated="1123969988414" total="$171.95">
  <item code="hat001">
    <name>Hat</name>
    <quantity>2</quantity>
  </item>
  <item code="cha001">
    <name>Chair</name>
    <quantity>1</quantity>
  </item>
  <item code="dog001">
    <name>Dog</name>
    <quantity>1</quantity>
  </item>
</cart>

다운로드 섹션의, 애플리케이션 소스 코드에서 Cart.java를 보았다면, 스트링(strings)들을 함께 붙여서 XML이 만들어 졌다는 것을 알 수 있을 것이다. 이 예제에서는 가능하지만, 자바 코드에서 XML을 만드는 방법 중 최악의 방법이다. 다음 글에서는 더 나은 방법을 소개하겠다.

이제 CartServletXMLHttpRequest에 어떻게 응답하는지 알았다. 이제 클라이언트측으로 리턴할 차례 이다. 여기에서 XML 응답이 페이지 상태의 업데이트에 어떻게 사용되는지를 보게 된다.




위로


JavaScript를 이용한 응답 핸들링

XMLHttpRequestreadyState 속성은 요청의 주기 상태를 부여하는 숫자 값이다. "초기화되지 않은 것"에는 0 부터 "완료된 것"에는 4 까지 붙을 수 있다. readyState가 변경될 때 마다 readystatechange 이벤트가 실행되고, onreadystatechange 속성을 통해 붙은 핸들러 함수가 호출된다.

Listing 3에서, getReadyStateHandler()함수가 호출되어 핸들러 함수를 만드는 방법을 보았다. 이 핸들러 함수는 onreadystatechange속성으로 할당된다. getReadyStateHandler()는 함수들이 JavaScript의 첫 번째 객체라는 것을 활용한다. 다시 말해서 함수들은 다른 함수들에 대한 매개변수가 될 수 있고, 다른 함수들을 생성 및 리턴할 수 있다. XMLHttpRequest가 완료되었는지 여부를 검사하여 그 핸들러 함수에 대한 XML 응답으로 전달하는 함수를 리턴하는 것은 getReadyStateHandler()의 역할이다. Listing 6은 getReadyStateHandler()의 코드이다.


Listing 6. getReadyStateHandler() 함수

/*
 * Returns a function that waits for the specified XMLHttpRequest
 * to complete, then passes its XML response
 * to the given handler function.
 * req - The XMLHttpRequest whose state is changing
 * responseXmlHandler - Function to pass the XML response to
 */
function getReadyStateHandler(req, responseXmlHandler) {

  // Return an anonymous function that listens to the 
  // XMLHttpRequest instance
  return function () {

    // If the request`s status is "complete"
    if (req.readyState == 4) {
      
      // Check that a successful server response was received
      if (req.status == 200) {

        // Pass the XML payload of the response to the 
        // handler function
        responseXmlHandler(req.responseXML);

      } else {

        // An HTTP problem has occurred
        alert("HTTP error: "+req.status);
      }
    }
  }
}

HTTP 상태 코드

Listing 6에서, XMLHttpRequest의 상태 속성(status property)은 요청이 성공적으로 완료되었는지를 보기 위해 테스트된다. status에는 서버 응답에 대한 HTTP 상태 코드가 포함되어 있다. GETPOST 요청을 수행할 때 200(OK)이 아닌 어떤 코드도 에러이다. 서버가 리다이렉트 응답(예를 들어, 301 또는 302)을 보내면 브라우저는 리다이렉트를 따라 새로운 위치에서 리소스를 가져온다. XMLHttpRequest는 리다이렉트 상태 코드를 볼 수 없다. 또한 브라우저(Cache-Control: no-cache)는 헤더를 모든 XMLHttpRequest에 추가하여 클라이언트 코드가 (변경되지 않은) 304 서버 응답을 다루지 않도록 한다.

getReadyStateHandler()에 대하여

특히 JavaScript 읽기에 익숙하지 않다면 getReadyStateHandler()는 비교적 복잡한 코드이다. 대신 여러분의 JavaScript 라이브러리에 이 하수를 추가하여 XMLHttpRequest의 내부를 다루지 않고도 Ajax 서버 응답들을 핸들할 수 있다. 중요한 것은 자신의 코드에서 getReadyStateHandler()를 사용하는 방법을 이해하는 것이다.

Listing 3에서 getReadyStateHandler() 는 다음과 같이 호출된다. handlerFunction = getReadyStateHandler(req,updateCart). 이 경우 getReadyStateHandler()에 의해 리턴된 함수는 변수 req에 있는 XMLHttpRequest가 완료되었는지를 확인한 다음 응답 XML이 있는 updateCart 함수를 호출한다.

카트 데이터 추출하기

Listing 7은 updateCart()의 코드이다. 이 함수는 DOM 호출들을 사용하여 쇼핑 카트 XML 문서를 조사하여 새로운 카트 내용을 반영하여 웹 페이지를 업데이트 한다. (Listing 1) XML DOM에서 데이터를 추출하는데 사용된 호출에 집중해보자. cart 엘리먼트에 대한, generated 애트리뷰트는 Cart가 XML로 직렬화 되었을 때 만들어진 타임스템프이다. 이것은 새로운 카트 데이터가 옛 카트 데이터에 반영되지 않았는지를 확인한다. Ajax 요청은 근본적으로 비동기식이라서 시퀀스에서 도착하는 서버 응답에 대한 세이프가드를 체크한다.


Listing 7. 카트 XML 문서를 반영하여 페이지 업데이트하기

function updateCart(cartXML) {

 // Get the root "cart" element from the document
 var cart = cartXML.getElementsByTagName("cart")[0];

 // Check that a more recent cart document hasn`t been processed
 // already
 var generated = cart.getAttribute("generated");
 if (generated > lastCartUpdate) {
   lastCartUpdate = generated;

   // Clear the HTML list used to display the cart contents
   var contents = document.getElementById("cart-contents");
   contents.innerHTML = "";

   // Loop over the items in the cart
   var items = cart.getElementsByTagName("item");
   for (var I = 0 ; I < items.length ; I++) {

     var item = items[I];

     // Extract the text nodes from the name and quantity elements
     var name = item.getElementsByTagName("name")[0]
                                               .firstChild.nodeValue;
                                               
     var quantity = item.getElementsByTagName("quantity")[0]
                                               .firstChild.nodeValue;

     // Create and add a list item HTML element for this cart item
     var li = document.createElement("li");
     li.appendChild(document.createTextNode(name+" x "+quantity));
     contents.appendChild(li);
   }
 }

 // Update the cart`s total using the value from the cart document
 document.getElementById("total").innerHTML = 
                                          cart.getAttribute("total");
}


이것으로 Ajax 실행이 완료되었다. 물론 여러분은 웹 애플리케이션이 실행되는 것을 보고 싶을 것이다.(다운로드 참조). 이 예제는 매우 간단하고 환경 범위도 다양하다. 예를 들어 서버측 코드를 추가하여 카트에서 아이템을 제거했지만, UI에서 접근하는 방법은 없다. 애플리케이션의 기존 JavaScript 코드에 이러한 기능을 구현하기 바란다.




위로


Ajax를 사용할 때의 문제점

다른 기술과 마찬가지로 Ajax도 허점이 많이 있다 . 최근에는 쉬운 솔루션이 없다는 것이 문제되고 있지만 Ajax가 성숙해 가면서 개선될 전망이다. 개발자 커뮤니티가 Ajax 애플리케이션 개발에 경험을 많이 쌓고 최선의 방법과 가이드라인이 문서화 될 것이다.

XMLHttpRequest의 기능

Ajax 개발자들이 직면한 가장 큰 문제들 중 하나는 XMLHttpRequest를 사용할 수 없을 경우이다. 대다수의 브라우저들은 XMLHttpRequest를 지원하지만, 어떤 사람들은 그러한 브라우저를 사용하지 않거나 브라우저 보안 설정 때문에 XMLHttpRequest를 사용할 수 없는 경우도 있다. 기업 인트라넷에 전개할 웹 애플리케이션을 개발한다면 어떤 브라우저가 지원되는지를 지정하고, XMLHttpRequest를 언제나 사용할 수 있도록 해야 한다. 하지만 공용 웹상에 전개한다면 XMLHttpRequest를 사용할 수 있겠지만 오래된 브라우저, 장애인용 브라우저, 핸드헬드용 경량 브라우저에는 사용할 수 없다.

따라서 애플리케이션을 "부드럽게 강등시켜서" XMLHttpRequest지원이 안되는 브라우저에서 작동하도록 해야 한다. 이 쇼핑 카트 예제에서 애플리케이션을 강등시키는 최선의 방법은 Add to Cart 버튼이 폼 제출을 수행하도록 하는 것이다. 페이지를 리프레시하여 카트의 업데이트를 반영한다. 페이지가 로딩되면 Ajax 작동이 JavaScript를 통해 페이지에 추가될 수 있다. JavaScript 핸들러 함수를 각 Add to Cart 버튼에 붙인다. 이는 어디까지나 XMLHttpRequest가 가능할 때이다. 또 다른 방법은 사용자가 로그인할 때 XMLHttpRequest를 탐지하는 것이다. 그리고 나서 Ajax 버전의 애플리케이션을 실행하든지 아니면 폼 기반 버전을 실행한다.

가용성 문제

Ajax 애플리케이션에 관련한 몇 가지 가용성 문제들이 있다. 예를 들어, 인풋이 등록되었다는 것을 사용자에게 알리는 것이 중요할 수 있다. 왜냐하면 보통의 피드백 방식인 모래시계 커서와 돌아가는 브라우저 "쓰로버(throbber)"가 XMLHttpRequest에 적용되지 않기 때문이다. 한 가지 방법은 Submit 버튼을 "Now updating..."메시지로 대체시키는 것이다. 이렇게 해서 사용자들이 응답을 기다리는 동안 버튼을 반복적으로 클릭하지 않도록 한다.

또 다른 문제는 사용자가 그들이 보고 있는 페이지의 일부가 업데이트 되었다는 것을 인식하지 못하는 것이다. 다양한 시각 기술을 사용하여 업데이트된 페이지 부분을 그려서 이 문제를 해결할 수 있다. 페이지 업데이트와 관련한 또 다른 문제로는 브라우저의 뒤로가기(back) 버튼이 "비활성화(breaking)"되는 것, 주소 바의 URL이 페이지의 전체 상태를 반영하지 않는 문제, 북마킹이 안되는 경우 등이 있다. (참고자료)

서버 부하

폼 기반의 Ajax UI를 구현하면 서버에서 요청 수가 상당히 많이 늘어난다. 예를 들어, 사용자가 검색 폼을 제출할 때 Google 웹 검색이 서버에 한번의 히트를 일으킨다고 해보자. 하지만 사용자의 검색어를 자동 완성하는 Google Suggest는 서버에 여러 요청들을 보낸다. Ajax 애플리케이션을 개발할 때 서버에 얼마나 많은 요청들을 보낼 것인지, 그리고 서버 부하가 어떻게 될 것인지를 알아야 한다. 또한 클라이언트에 요청을 버퍼링하고 클라이언트에 서버 응답을 캐싱하여 서버 부하를 완화시킬 수 있다. 또한 Ajax 웹 애플리케이션을 설계하여 가능한 많은 로직들이 굳이 서버에 연결되지 않고도 클라이언트에서 수행될 수 있도록 한다.

비동기식 다루기

XMLHttpRequest가 발송되는 순서대로 완료될 것이라는 보장이 없다. 실제로 그것까지 염두해 가며 애플리케이션을 설계하지 않는다. 쇼핑 카트 예제에서 마지막으로 업데이트 된 타임스탬프는 새로운 카트 데이터가 오래된 데이터에 적용되었는지를 확인할 떄 사용되었다. (Listing 7). 이것은 쇼핑 카트 시나리오에서 매우 기본적인 방식이지만 다른 경우에는 그렇지 않다. 설계할 때 비동기식 서버 응답을 어떻게 처리할 것인지를 생각하라.




위로


결론

Ajax의 기본 원리와 Ajax 인터랙션에 참여하는 클라이언트와 서버측 컴포넌트에 대해서 설명했다. 이들 모두 자바 기반 Ajax 웹 애플리케이션을 만드는 요소들이다. 또한 Ajax 접근방식에 따르는 고급 디자인 문제들도 이해해야 한다. 성공적인 Ajax 애플리케이션을 만들기 위해서는 UI 디자인부터 자바 스크립트 디자인, 그리고 서버측 아키텍쳐에 이르기까지 유기적인 접근 방식이 필요하다. 하지만 이제 여러분도 Ajax에 대한 기본적인 지식이 있으니 문제없다.

이 글에서 설명한 기술들을 사용하여 대형 Ajax 애플리케이션을 작성할 때의 복잡함에 압도될지 모르겠다. 하지만 좋은 소식이 있다. Struts, Spring, Hibernate 같은 프레임웍이 저급의 Servlet API와 JDBC에서 탈피하여 추상 웹 애플리케이션으로 진화했기 때문에 툴킷들을 사용하면 Ajax 개발이 쉬워진다. 이들 중 몇몇은 클라이언트 측에만 해당된다. 페이지에 시각 효과를 쉽게 추가할 수 있고, XMLHttpRequest의 사용법도 간단해진다. 서버측 코드에서 Ajax 인터페이스를 자동으로 생성하는 방법도 제공한다. Ajax 개발 방식이 고급화 되었다. .

Ajax 커뮤니티는 빠르게 변화하고 있고 정보도 풍부하다. 이 글을 다 읽었다면 참고자료 섹션을 참조하기 바란다. Ajax 또는 클라이언트측 개발이 처음인 사람들에게 권한다. 또한 시간을 내어 샘플 코드도 공부하기 바란다.

다음에는 XMLHttpRequest API를 보다 자세히 설명하고 JavaBean에서 XML을 쉽게 생성하는 방법도 설명하겠다. JSON (JavaScript Object Notation)도 소개할 테니 기대하라.



필자소개

Philip McCarthy: Independent Consultant 소프트웨어 개발 컨설턴트

Posted by 알 수 없는 사용자
|

JSON-PRC-java 적용하기

java 2006. 2. 25. 22:35

http://kingori.egloos.com/1673534

ajax의 광풍이 몰아닥칠때, XMLHttpRequest 와 함께 이름이 여기저기서 보이던 JSON-RPC , 그 중에서도 JSON-RPC-java 를 직접 사용해보았다.

ajax 는 분명히 손이 많이 가지만 한번 적용해보고 싶었고, 또한 적용하지 않으면 상당히 우울한 부분이 마침 눈에 띄어서 옳다꾸나 하고 냉큼 써먹어봤다.

사용한 부분은 대량의 입력을 받아 submit 하기 직전에 대략 3개~4개의 column 들에 대한 validation 이었다. 당연히 server-side 에서의 validation 이다.
보통 이럴경우 struts의 form validation 을 쓰는데, 이런 경우 form 이 dynamic 하게 늘어나고 parameter 도 array 로 날라다니고 해서 아주 골치아프다.
사용자 입장에서는 대략 40개의 입력값을 줬는데, submit 했다가 validation에서 뻑나면 다시 일일이 입력해줘야 하고, 이걸 막기 위해선 개발자가 40개의 입력값을 받아다가 어디에 저장해놓고, validation 해서 뻑나면 다시 redirecting 시켜준 후 저장해 놓은 입력값을 다시 form 에 일일이 박아다 주고. 관두고 말지!
이때 ajax 를 통한 validation 을 쓰면 아주 깔끔하게 처리할 수 있다. 물론 iframe 을 사용해서 매우 아름답지 못한 방법으로 할 수도 있지만 느리고, 또 꼬이기 십상이다. 내 경우 단순결과를 얻기위해 iframe으로 10번정도의 request를 날려야하니 이것또한 우울하다.

XMLHttpRequest 를 쓸 수도 있는데 굳이 JSON-RPC 를 사용한 이유는, 단순 validation 이기 때문에 값만 받아오면 되는데 ( true|false ) , 요것때문에 xml 페이지 처리하고 값 받아오는것을 만드는것이 귀찮고, 또 귀파는데 삽자루 들이미는 격이라서 아얘 객체로 왔다리 갔다리 하는 JSON-RPC 로 눈을 돌렸다.

홈페이지에 나와있는 설명을 번역하자면

  • JSON (JavaScript Object Notation) 은 C, C++, C#, Java, JavaScript, Perl, TCL 등의 언어와 호환가능한 lightweight 한 데이타교환 format이다.
  • JSON-RPC 는 XML-RPC 과 유사한 단순한 remote procedure call protocol 이지만, XML 대신 JSON format 을 사용한다.
  • 페이지 reloading 없이 원격 method 호출을 위해 XMLHttpRequest object (IE의 경우는 MSXML ActiveX ) 가 쓰인다.
  • JSON-RPC-Java 는 JSON-RPC protocol 을 Java로 작성한 것이다

결국 XMLHttpRequest 위에 올라가서, 원격 메소드 호출 후 결과값을 JSON 으로 가져다 주는 것이니 나의 경우와 같이 결과값만 받는 경우 XMLHttpRequest 직접 구현보다 훨씬 편하다.

간단히 실제 적용내용을 정리하면
  1. web application 에 사용할 directory 를 만든다. ( 기존 application 에 적용할 때는 필요없음).
  2. web application root 밑에 WEB-INF 디렉토리를 만들고 아래와 같이 web.xml 을 작성.

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
    <web-app>
    <directory-servlet>none</directory-servlet>
    <display-name>jsonrpc</display-name>
    <!-- Action Servlet Configuration -->
    <servlet>
    <servlet-name>JSONRPCServlet</servlet-name>
    <servlet-class>com.metaparadigm.jsonrpc.JSONRPCServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>JSONRPCServlet</servlet-name>
    <url-pattern>/json</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    </web-app>

  3. library 디렉토리에 json.jar 복사
  4. js 위치한 곳에 jsonrpc.js 복사
  5. 환경설정끝! jsp 작성


내가 만든 테스트 페이지의 소스와 jsp 는
<%@ page contentType="text/html;charset=EUC-KR"
import
="kingori.MessageGetter" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<jsp:useBean id="JSONRPCBridge"
class="com.metaparadigm.jsonrpc.JSONRPCBridge" scope="session"/>
<%
JSONRPCBridge.registerObject("messageGetter", new MessageGetter() );
%>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>JSONRPCJAVA test</title>
<script type="text/javascript" src="jsonrpc.js"></script>
<script type="text/javascript">
jsonrpc = new JSONRpcClient("json");

function getMessage()
{
document.getElementById("messageSpan").innerHTML =
jsonrpc.messageGetter.getMessage();
}
</script>
</head>
<body>
<p>Please tell me something:
<input type="button" onclick="getMessage();" value="click"/></p>
<p>message:<span id="messageSpan"></span></p>
</body>
</html>

package kingori;

public class MessageGetter
{
public String getMessage()
{
return "hohoho!";
}
}


위와 같다.

jsp 에서의 호출부분이 매우 단순하다. 만약 object에 뭔가를 넣어줘야 할 경우 javascript 단에서 처리하면 된다. 위의 코드라면
jsonrpc.messageGetter.getMessage( value);

이런 식이 될 듯.

한가지 문제는 validator 자체가 http request , http response 를 거치지 않고 JSONRPC 가 그냥 불러버리기 때문에 "로긴한 사용자 이름을 가져와서 세팅하기" 등등의 부분에서 꼼수가 필요하지 않을까 생각한다. 이것은 jsp 단 코드에서 해결해줘야 하지 않을까 생각한다. (아직 해보진 않았지만.)

꽤 깔끔하고, XMLHttpRequest 일일이 구현하는것보다 간편하다. 대신 만약 xml document 전체를 뿌려준다거나 할 경우는 차라리 XMLHttpRequest 로 구현하는게 더 편하지 않을까 싶다.

내 결론은
  • 달랑 값만 필요 => JSONRPC-JAVA
  • 웹단 HTML 도 같이 가져오고싶다 => XMLHttpRequest


정도. 뭐가 되었던간에 예전같이 iframe 으로 장난하던 것보다 비할 데 없이 편하고, 보기도 좋다.
Posted by 알 수 없는 사용자
|

http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=968522077

서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-
[JDBC Connection Pooling]

최근수정일자 : 2001.01.19
최근수정일자 : 2001.03.20(샘플예제추가)
최근수정일자 : 2001.10.22(디버깅을 위한 로직추가)
최근수정일자 : 2001.10.29(Oracle JDBC2.0 샘플추가)
최근수정일자 : 2001.11.08(윤한성님 도움 OracleConnectionCacheImpl 소스수정)
최근수정일자 : 2001.11.09(Trace/Debugging을 위한 장문의 사족을 담)

5. JDBC Connection Pooling 을 왜 사용해야 하는가 ?

Pooling 이란 용어는 일반적인 용어입니다. Socket Connection Pooling, Thread
Pooling, Resource Pooling 등 "어떤 자원을 미리 Pool 에 준비해두고 요청시 Pool에
있는 자원을 곧바로 꺼내어 제공하는 기능"인 거죠.

JDBC Connection Pooling 은 JDBC를 이용하여 자바에서 DB연결을 할 때, 미리 Pool에
물리적인 DB 연결을 일정개수 유지하여 두었다가 어플리케이션에서 요구할 때 곧바로
제공해주는 기능을 일컫는 용어입니다. JDBC 연결시에, (DB 종류마다, 그리고 JDBC
Driver의 타입에 따라 약간씩 다르긴 하지만 ) 대략 200-400 ms 가 소요 됩니다. 기껏
0.2 초 0.4 초 밖에 안되는 데 무슨 문제냐 라고 반문할수도 있습니다.

하지만, 위 시간은 하나의 연결을 시도할 때 그러하고, 100 - 200개를 동시에 연결을
시도하면 얘기가 완전히 달라집니다.

아래는 직접 JDBC 드라이버를 이용하여 연결할 때와 JDBC Connection Pooling 을 사용
할 때의 성능 비교 결과입니다.


------------------------------------------------------------------
테스트 환경
LG-IBM 570E Notebook(CPU:???MHz , MEM:320MB)
Windows NT 4.0 Service Pack 6
IBM WebSphere 3.0.2.1 + e-Fixes
IBM HTTP Server 1.3.6.2
IBM UDB DB2 6.1

아래에 첨부한 파일는 자료를 만들때 사용한 JSP소스입니다. (첨부파일참조)

[HttpConn.java] 간단한 Stress Test 프로그램


아래의 수치는 이 문서 이외에는 다른 용도로 사용하시면 안됩니다. 테스트를 저의
개인 노트북에서 측정한 것이고, 또한 테스트 프로그램 역시 직접 만들어한 것인
만큼, 공정성이나 수치에 대한 신뢰를 부여할 수는 없습니다.
그러나 JDBC Connection Pooling 적용 여부에 따른 상대적인 차이를 설명하기에는
충분할 것 같습니다.

테스트 결과

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
매번 직접 JDBC Driver 연결하는 경우
java HttpConn http://localhost/db_jdbc.jsp -c <동시유저수> -n <호출횟수> -s 0

TOTAL( 1,10)  iteration=10 ,  average=249.40 (ms),   TPS=3.93
TOTAL(50,10)  iteration=500 , average=9,149.84 (ms), TPS=4.83
TOTAL(100,10)  iteration=1000 , average=17,550.76 (ms), TPS=5.27
TOTAL(200,10)  iteration=2000 , average=38,479.03 (ms), TPS=4.89
TOTAL(300,10)  iteration=3000 , average=56,601.89 (ms), TPS=5.01

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DB Connection Pooing 을 사용하는 경우
java HttpConn http://localhost/db_pool_cache.jsp -c <동시유저수> -n <호출횟수> -s 0

TOTAL(1,10)  iteration=10 , average=39.00 (ms), TPS=23.26
TOTAL(1,10)  iteration=10 , average=37.10 (ms), TPS=24.33
TOTAL(50,10)  iteration=500 , average=767.36 (ms), TPS=45.27
TOTAL(50,10)  iteration=500 , average=568.76 (ms), TPS=61.26
TOTAL(50,10)  iteration=500 , average=586.51 (ms), TPS=59.79
TOTAL(50,10)  iteration=500 , average=463.78 (ms), TPS=67.02
TOTAL(100,10)  iteration=1000 , average=1,250.07 (ms), TPS=57.32
TOTAL(100,10)  iteration=1000 , average=1,022.75 (ms), TPS=61.22
TOTAL(200,10)  iteration=1462 , average=1,875.68 (ms), TPS=61.99
TOTAL(300,10)  iteration=1824 , average=2,345.42 (ms), TPS=61.51

NOTE: average:평균수행시간, TPS:초당 처리건수
------------------------------------------------------------------

즉, JDBC Driver 를 이용하여 직접 DB연결을 하는 구조는 기껏 1초에 5개의 요청을
처리할 수 있는 능력이 있는 반면, DB Connection Pooling 을 사용할 경우는 초당
60여개의 요청을 처리할 수 있는 것으로 나타났습니다.
12배의 성능향상을 가져온거죠. (이 수치는 H/W기종과 측정방법, 그리고 어플리케이션에
따라 다르게 나오니 이 수치자체에 너무 큰 의미를 두진 마세요)


주의: 흔히 "성능(Performance)"을 나타낼 때, 응답시간을 가지고 얘기하는 경향이
  있습니다. 그러나 응답시간이라는 것은 ActiveUser수가 증가하면 당연히 그에 따라
  느려지게 됩니다. 반면 "단위시간당 처리건수"인 TPS(Transaction Per Second) 혹은
  RPS(Request Per Second)는 ActiveUser를 지속적으로 끌어올려 임계점을 넘어서면,
  특정수치 이상을 올라가지 않습니다. 따라서 성능을 얘기할 땐 평균응답시간이 아니라
  "단위시간당 최대처리건수"를 이야기 하셔야 합니다.
  성능(Performance)의 정의(Definition)은 "단위시간당 최대처리건수"임을 주지하세요.
  성능에 관련한 이론은 아래의 문서를 통해, 함께 연구하시지요.
  [강좌]웹기반시스템하에서의 성능에 대한 이론적 고찰
  http://www.javaservice.net/~java/bbs/read.cgi?m=resource&b=consult&c=r_p&n=1008701211

PS: IBM WebSphere V3 의 경우, Connection 을 가져올 때, JNDI를 사용하게 되는데
   이때 사용되는 DataSource 객체를 매번 initialContext.lookup() 을 통해 가져오게
   되면, 급격한 성능저하가 일어납니다.(NOTE: V4부터는 내부적으로 cache를 사용하여
   성능이 보다 향상되었습니다)
   DataSource를 매 요청시마다 lookup 할 경우 다음과 같은 결과를 가져왔습니다.

   java HttpConn http://localhost/db_pool.jsp -c <동시유저수> -n <호출횟수> -s 0

   TOTAL(1,10)  iteration=10 , average=80.00 (ms), TPS=11.61
   TOTAL(50,10)  iteration=500 , average=2,468.30 (ms), TPS=16.98
   TOTAL(50,10)  iteration=500 , average=2,010.43 (ms), TPS=18.18
   TOTAL(100,10)  iteration=1000 , average=4,377.24 (ms), TPS=18.16
   TOTAL(200,10)  iteration=1937 , average=8,991.89 (ms), TPS=18.12

   TPS 가 18 이니까 DataSource Cache 를 사용할 때 보다 1/3 성능밖에 나오지 않는 거죠.



6. JDBC Connection Pooling 을 사용하여 코딩할 때 고려사항

JDBC Connecting Pooling은 JDBC 1.0 스펙상에 언급되어 있지 않았습니다. 그러다보니
BEA WebLogic, IBM WebSphere, Oracle OAS, Inprise Server, Sun iPlanet 등 어플리케
이션 서버라고 불리는 제품들마다 그 구현방식이 달랐습니다.
인터넷에서 돌아다니는 Hans Bergsten 이 만든 DBConnectionManager.java 도 그렇고,
JDF 에 포함되어 있는 패키지도 그렇고 각자 독특한 방식으로 개발이 되어 있습니다.

JDBC를 이용하여 DB연결하는 대표적인 코딩 예를 들면 다음과 같습니다.

------------------------------------------------------------------
[BEA WebLogic Application Server]

    import java.sql.*;

    // Driver loading needed.
    static {
      try {
        Class.forName("weblogic.jdbc.pool.Driver").newInstance();         
      }
      catch (Exception e) {
        ...
      }
    }

    ......

    Connection conn = null;
    Statement stmt = null;
    try {
      conn = DriverManager.getConnection("jdbc:weblogic:pool:<pool_name>", null);
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{conn.close();}catch(Exception e){}
    }


------------------------------------------------------------------
IBM WebSphere Application Server 3.0.2.x / 3.5.x

    /* IBM WebSphere 3.0.2.x for JDK 1.1.8 */
    //import java.sql.*;
    //import javax.naming.*;
    //import com.ibm.ejs.dbm.jdbcext.*;
    //import com.ibm.db2.jdbc.app.stdext.javax.sql.*;

    /* IBM WebSphere 3.5.x for JDK 1.2.2 */
    import java.sql.*;
    import javax.sql.*;
    import javax.naming.*;

    // DataSource Cache 사용을 위한 ds 객체 static 초기화
    private static DataSource ds = null;
    static {
      try {
        java.util.Hashtable props = new java.util.Hashtable();
        props.put(Context.INITIAL_CONTEXT_FACTORY,
                  "com.ibm.ejs.ns.jndi.CNInitialContextFactory");
        Context ctx = null;
        try {
          ctx = new InitialContext(props);
          ds = (DataSource)ctx.lookup("jdbc/<data_source_name>");
        }
        finally {
          if ( ctx != null ) ctx.close();
        }
      }
      catch (Exception e) {
       ....
      }
    }

    .....  

    Connection conn = null;
    Statement stmt = null;
    try {
      conn = ds.getConnection("userid", "password");
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{conn.close();}catch(Exception e){}
    }

------------------------------------------------------------------
IBM WebSphere Application Server 2.0.x
 
    import java.sql.*;
    import com.ibm.servlet.connmgr.*;


    // ConnMgr Cache 사용을 위한 connMgr 객체 static 초기화

    private static IBMConnMgr connMgr = null;
    private static IBMConnSpec spec = null;
    static {
      try {
        String poolName = "JdbcDb2";  // defined in WebSphere Admin Console
        spec = new IBMJdbcConnSpec(poolName, false, 
              "com.ibm.db2.jdbc.app.DB2Driver",
              "jdbc:db2:<db_name>",   // "jdbc:db2://ip_address:6789/<db_name>",
              "userid","password");
        connMgr = IBMConnMgrUtil.getIBMConnMgr();
      }
      catch(Exception e){
        .....
      }
    }

    .....

    IBMJdbcConn cmConn = null; // "cm" maybe stands for Connection Manager.
    Statement stmt = null;
    try {
      cmConn = (IBMJdbcConn)connMgr.getIBMConnection(spec);   
      Connection conn = jdbcConn.getJdbcConnection();
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( cmConn != null ) try{cmConn.releaseIBMConnection();}catch(Exception e){}
    }
    // NOTE: DO NOT "conn.close();" !!


------------------------------------------------------------------
Oracle OSDK(Oracle Servlet Development Kit)

    import java.sql.*;
    import oracle.ec.ctx.*;


    .....
   
    oracle.ec.ctx.Trx trx = null;
    Connection conn = null;
    Statement stmt = null;
    try {
      oracle.ec.ctx.TrxCtx ctx = oracle.ec.ctx.TrxCtx.getTrxCtx();
      trx = ctx.getTrx();
      conn = trx.getConnection("<pool_name>");
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{ trx.close(conn,"<pool_name>");}catch(Exception e){}
    }
    // NOTE: DO NOT "conn.close();" !!


------------------------------------------------------------------
Hans Bergsten 의 DBConnectionManager.java

    import java.sql.*;

    .....
   
    db.DBConnectionManager connMgr = null;
    Connection conn = null;
    Statement stmt = null;
    try {
      connMgr = db.DBConnectionManager.getInstance();
      conn = connMgr.getConnection("<pool_name>");
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) connMgr.freeConnection("<pool_name>", conn);
    }
    // NOTE: DO NOT "conn.close();" !!

------------------------------------------------------------------
JDF 의 DB Connection Pool Framework

    import java.sql.*;
    import com.lgeds.jdf.*;
    import com.lgeds.jdf.db.*;
    import com.lgeds.jdf.db.pool.*;


    private static com.lgeds.jdf.db.pool.JdbcConnSpec spec = null;
    static {
      try {
        com.lgeds.jdf.Config conf = new com.lgeds.jdf.Configuration();
        spec =  new com.lgeds.jdf.db.pool.JdbcConnSpec(
             conf.get("gov.mpb.pbf.db.emp.driver"),
             conf.get("gov.mpb.pbf.db.emp.url"),
             conf.get("gov.mpb.pbf.db.emp.user"),
             conf.get("gov.mpb.pbf.db.emp.password")
          );
      }
      catch(Exception e){
        .....
      }
    }

    .....

    PoolConnection poolConn = null;
    Statement stmt = null;
    try {
      ConnMgr mgr = ConnMgrUtil.getConnMgr();
      poolConn = mgr.getPoolConnection(spec);
      Connection conn = poolConnection.getConnection();
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( poolConn != null ) poolConn.release();
    }
    // NOTE: DO NOT "conn.close();" !!

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


여기서 하고픈 얘기는 DB Connection Pool 을 구현하는 방식에 따라서 개발자의 소스도
전부 제 각기 다른 API를 사용해야 한다는 것입니다.
프로젝트를 이곳 저곳 뛰어 본 분은 아시겠지만, 매 프로젝트마나 어플리케이션 서버가
다르고 지난 프로젝트에서 사용된 소스를 새 프로젝트에 그대로 적용하지 못하게 됩니다.
JDBC 관련 API가 다르기 때문이죠.
같은 제품일지라도 버전업이 되면서 API가 변해버리는 경우도 있습니다. 예를 들면,
IBM WebSphere 버전 2.0.x에서 버전 3.0.x로의 전환할 때, DB 연결을 위한 API가
변해버려 기존에 개발해둔 400 여개의 소스를 다 뜯어 고쳐야 하는 것과 같은 상황이
벌어질 수도 있습니다.
닷컴업체에서 특정 패키지 제품을 만들때도 마찬가지 입니다. 자사의 제품이 어떠한
어플리케이션 서버에서 동작하도록 해야 하느냐에 따라 코딩할 API가 달라지니 소스를
매번 변경해야만 하겠고, 특정 어플리케이션 서버에만 동작하게 하려니 마켓시장이
좁아지게 됩니다.
IBM WebSphere, BEA WebLogic 뿐만 아니라 Apache JServ 나 JRun, 혹은 Tomcat 에서도
쉽게 포팅하길 원할 것입니다.

이것을 해결하는 방법은 우리들 "SE(System Engineer)"만의 고유한 "Connection Adapter
클래스"를 만들어서 사용하는 것입니다.

예를 들어 개발자의 소스는 이제 항상 다음과 같은 유형으로 코딩되면 어떻겠습니까 ?


    -----------------------------------------------------------
    .....
    ConnectionResource resource = null;
    Statement stmt = null;
    try {
      resource = new ConnectionResource();
      Connection conn = resource.getConnection();

      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( resource != null ) resource.release();
    }
    // NOTE: DO NOT "conn.close();" !!
    -----------------------------------------------------------



이 때 ConnectionResource 는 다음과 같은 형식으로 누군가 한분이 만들어 두면 되겠죠.

    -----------------------------------------------------------
    import java.sql.*;
    public class ConnectionResource
    {
      private java.sql.Connection conn = null;
      public ConnectionResource() throws Exception {
         ......
         conn =  ..... GET Connection by Connection Pooling API
      }
      public Connection getConnection() throws Exception {
         return conn;
      }
      public void release(){
         // release conn into "Connection Pool"
         .......
      }
    }
    -----------------------------------------------------------



  예를 들어 IBM WebSphere Version 3.0.2.x 의 경우를 든다면 다음과 같이 될 겁니다.

  -----------------------------------------------------------
  package org.jsn.connpool;
  /*
   * ConnectionResource V 1.0
   * JDBC Connection Pool Adapter for IBM WebSphere V 3.0.x/3.5.x
   * Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
   * Last Modified : 2000.11.10
   * NOTICS: You can re-distribute or copy this source code freely,
   *         you can NOT remove the above subscriptions.
  */

  /* IBM WebSphere 3.0.2.x for JDK 1.1.8 */
  //import java.sql.*;
  //import javax.naming.*;
  //import com.ibm.ejs.dbm.jdbcext.*;
  //import com.ibm.db2.jdbc.app.stdext.javax.sql.*;

  /* IBM WebSphere 3.5.x for JDK 1.2.2 */
  import java.sql.*;
  import javax.sql.*;
  import javax.naming.*;

  public class ConnectionResource
  {
     private static final String userid = "userid";
     private static final String password = "password"
     private static final String datasource = "jdbc/<data_source_name>";
     private static DataSource ds = null;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
       synchronized ( ConnectionResource.class ) {
         if ( ds == null ) {
           java.util.Hashtable props = new java.util.Hashtable();
           props.put(Context.INITIAL_CONTEXT_FACTORY,
              "com.ibm.ejs.ns.jndi.CNInitialContextFactory");
           Context ctx = null;
           try {
             ctx = new InitialContext(props);
             ds = (DataSource)ctx.lookup("jdbc/<data_source_name>");
           }
           finally {
             if ( ctx != null ) ctx.close();
           }
         }
       }
       conn =  ds.getConnection( userid, password );
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        // release conn into "Connection Pool"
        if ( conn != null ) try { conn.close(); }catch(Excepton e){}
        conn = null;
     }
   }
   -----------------------------------------------------------
         


 
  만약 Hans Bersten 의 DBConnectionManager.java 를 이용한 Connection Pool 이라면
  다음과 같이 만들어 주면 됩니다.

   -----------------------------------------------------------
   package org.jsn.connpool;
   import java.sql.*;
   public class ConnectionResource
   {
      private String poolname = "<pool_name>";
      private Connection conn = null;
      private db.DBConnectionManager connMgr = null;

      public ConnectionResource() throws Exception {
         connMgr = db.DBConnectionManager.getInstance();
         conn =  connMgr.getConnection(poolname);
      }
      public Connection getConnection() throws Exception {
         if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
         return conn;
      }
      public void release(){
         if ( conn != null ) {
           // Dirty Transaction을 rollback시키는 부분인데, 생략하셔도 됩니다.
           boolean autoCommit = true;
           try{ autoCommit = conn.getAutoCommit(); }catch(Exception e){}
           if ( autoCommit == false ) {
             try { conn.rollback(); }catch(Exception e){}
             try { conn.setAutoCommit(true); }catch(Exception e){}
           }

           connMgr.freeConnection(poolname, conn);
           conn = null;
         }
      }
   }
   -----------------------------------------------------------


  또, Resin 1.2.x 의 경우라면 다음과 같이 될 겁니다.
  -----------------------------------------------------------
  package org.jsn.connpool;
  /*
   * ConnectionResource V 1.0
   * JDBC Connection Pool Adapter for Resin 1.2.x
   * Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
   * Last Modified : 2000.10.18
   * NOTICS: You can re-distribute or copy this source code freely,
   *         you can NOT remove the above subscriptions.
  */
  import java.sql.*;
  import javax.sql.*;
  import javax.naming.*;
  public class ConnectionResource
  {
     private static final String datasource = "jdbc/<data_source_name>";
     private static final String userid = "userid";
     private static final String password = "password"
     private static DataSource ds = null;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
        synchronized ( ConnectionResource.class ) {
          if ( ds == null ) {
            Context env = (Context) new InitialContext().lookup("java:comp/env");
            ds = (DataSource) env.lookup(datasource);
          }
        }   
        conn =  ds.getConnection( userid, password );
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        // release conn into "Connection Pool"
        if ( conn != null ) try { conn.close(); }catch(Excepton e){}
        conn = null;
     }
   }
   -----------------------------------------------------------


  Oracle 8i(8.1.6이상)에서 Oracle의 JDBC 2.0 Driver 자체가 제공하는 Connection
  Pool을 이용한다면 다음과 같이 될 겁니다.
  -----------------------------------------------------------
  package org.jsn.connpool;
  /*
   * ConnectionResource V 1.0
   * JDBC Connection Pool Adapter for Oracle JDBC 2.0
   * Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
   * Last Modified : 2001.10.29
   * NOTICS: You can re-distribute or copy this source code freely,
   *         but you can NOT remove the above subscriptions.
  */
  import java.sql.*;
  import javax.sql.*;
  import oracle.jdbc.driver.*;
  import oracle.jdbc.pool.*;

  public class ConnectionResource
  {
     private static final String dbUrl = "jdbc:oracle:thin@192.168.0.1:1521:ORCL";
     private static final String userid = "userid";
     private static final String password = "password"
     private static OracleConnectionCacheImpl oraclePool = null;
     private static boolean initialized = false;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
        synchronized ( ConnectionResource.class ) {
          if ( initialized == false ) {
            DriverManager.registerDriver (new oracle.jdbc.driver.OracleDriver());
            oraclePool = new OracleConnectionCacheImpl();
            oraclePool.setURL(dbUrl);
            oraclePool.setUser(userid);
            oraclePool.setPassword(password);
            oraclePool.setMinLimit(10);
            oraclePool.setMaxLimit(50);
            //oraclePool.setCacheScheme(OracleConnectionCacheImpl.FIXED_WAIT_SCHEME);
            // FIXED_WAIT_SCHEME(default), DYNAMIC_SCHEME, FIXED_RETURN_NULL_SCHEME
            //oraclePool.setStmtCacheSize(0); //default is 0

            initialized = true;
          }
        }   
        conn = oraclePool.getConnection();
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        // release conn into "Connection Pool"
        if ( conn != null ) try { conn.close(); }catch(Exception e){}
        conn = null;
     }
   }
   -----------------------------------------------------------

  또한 설령 Connection Pool 기능을 지금을 사용치 않더라도 향후에 적용할 계획이
  있다면, 우선은 다음과 같은 Connection Adapter 클래스를 미리 만들어 두고 이를
  사용하는 것이 효과적입니다. 향후에 이 클래스만 고쳐주면 되니까요.
  Oracle Thin Driver를 사용하는 경우입니다.

  -----------------------------------------------------------
  import java.sql.*;
  public class ConnectionResource
  {
     private static final String userid = "userid";
     private static final String password = "password"
     private static final String driver = "oracle.jdbc.driver.OracleDriver"
     private static final String url = "jdbc:oracle:thin@192.168.0.1:1521:ORCL";
     private static boolean initialized = false;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
        synchronized ( ConnectionResource.class ) {
          if ( initialized == false ) {
              Class.forName(driver);
              initialized = true;
          }
        }
        conn = DriverManager.getConnection( url, userid, password );
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        if ( conn != null ) try { conn.close(); }catch(Excepton e){}
        conn = null;
     }
   }
   -----------------------------------------------------------


프로그램의 유형이나, 클래스 이름이야 뭐든 상관없습니다. 위처럼 우리들만의 고유한
"Connection Adapter 클래스"를 만들어서 사용한다는 것이 중요하고, 만약 어플리케이션
서버가 변경된다거나, DB Connection Pooling 방식이 달라지면 해당 ConnectionResource
클래스 내용만 살짝 고쳐주면 개발자의 소스는 전혀 고치지 않아도 될 것입니다.

NOTE: ConnectionResource 클래스를 만들때 주의할 것은 절대 Exception 을 클래스 내부에서
   가로채어 무시하게 하지 말라는 것입니다. 그냥 그대로 throw 가 일어나게 구현하세요.
   이렇게 하셔야만 개발자의 소스에서 "Exception 처리"를 할 수 있게 됩니다.

NOTE2: ConnectionResource 클래스를 만들때 반드시 package 를 선언하도록 하세요.
   IBM WebSphere 3.0.x 의 경우 JSP에서 "서블렛클래스패스"에 걸려 있는 클래스를
   참조할 때, 그 클래스가 default package 즉 package 가 없는 클래스일 경우 참조하지
   못하는 버그가 있습니다. 통상 "Bean"이라 불리는 클래스 역시 조건에 따라 인식하지
   않을 수도 있습니다.
   클래스 다지인 및 설계 상으로 보더라도 package 를 선언하는 것이 바람직합니다.

NOTE3: 위에서  "userid", "password" 등과 같이 명시적으로 프로그램에 박아 넣지
   않고, 파일로 관리하기를 원한다면, 그렇게 하셔도 됩니다.
   JDF의 Configuration Framework 을 참조하세요.


PS: 혹자는 왜 ConnectionResource 의 release() 메소드가 필요하냐고 반문할 수도 있습
   니다. 예를 들어 개발자의 소스가 다음처럼 되도록 해도 되지 않느냐라는 거죠.

    -----------------------------------------------------------
    .....
    Connection conn = null;
    Statement stmt = null;
    try {
      conn = ConnectionResource.getConnection(); // <---- !!!

      stmt = conn.createStatement();
      .....
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{conn.close();}catch(Exception e){} // <---- !!!
    }
    -----------------------------------------------------------

  이렇게 하셔도 큰 무리는 없습니다. 그러나, JDBC 2.0 을 지원하는 제품에서만
  conn.close() 를 통해 해당 Connection 을 실제 close() 시키는 것이 아니라 DB Pool에
  반환하게 됩니다. BEA WebLogic 이나 IBM WebSphere 3.0.2.x, 3.5.x 등이 그렇습니다.
  그러나, 자체제작된 대부분의 DB Connection Pool 기능은 Connection 을 DB Pool에
  반환하는 고유한 API를 가지고 있습니다. WebSphere Version 2.0.x 에서는
  cmConn.releaseIBMConnection(), Oracle OSDK 에서는 trx.close(conn, "<pool_name">);
  Hans Bersten 의 DBConnectionManager 의 경우는
  connMgr.freeConnection(poolname, conn); 등등 서로 다릅니다. 이러한 제품들까지
  모두 지원하려면 "release()" 라는 우리들(!)만의 예약된 메소드가 꼭 필요하게 됩니다.

  물론, java.sql.Connection Interface를 implements 한 별도의 MyConnection 을 만들어
  두고, 실제 java.sql.Connection 을 얻은 후 MyConnection의 생성자에 그 reference를
  넣어준 후, 이를 return 시에 넘기도록 하게 할 수 있습니다. 이때, MyConnection의
  close() 함수를 약간 개조하여 DB Connection Pool로 돌아가게 할 수 있으니까요.


PS: 하나 이상의 DB 를 필요로 한다면, 다음과 같은 생성자를 추가로 만들어서 구분케
  할 수도 있습니다.

  -----------------------------------------------------------
  ....
  private String poolname = "default_pool_name";
  private String userid = "scott";
  private String password = "tiger";
  public ConnectionResource() throws Exception {
    initialize();
  }
  public ConnectionResource(String poolname) throws Exception {
    this.poolname = poolname;
    initialize();
  }
  public ConnectionResource(String poolname,String userid, String passwrod)
  throws Exception
  {
    this.poolname = poolname;
    this.userid = userid;
    this.password = password;
    initialize();
  }
  private void initialize() throws Exception {
    ....
  }
  ...
  -----------------------------------------------------------



-------------------------------------------------------------------------------
2001.03.20 추가
2001.10.22 수정
2001.11.09 수정(아래 설명을 추가함)

실 운영 사이트의 장애진단 및 튜닝을 다녀보면, 장애의 원인이 DataBase 연결개수가
지속적으로 증가하고, Connection Pool 에서 더이상 가용한 연결이 남아 있지 않아
발생하는 문제가 의외로 많습니다. 이 상황의 십중팔구는 개발자의 코드에서 Pool로
부터 가져온 DB연결을 사용하고 난 후, 이를 여하한의 Exception 상황에서도 다시
Pool로 돌려보내야 한다는 Rule 를 지키지 않아서 발생한 문제가 태반입니다.
이름만 말하면 누구나 알법한 큼직한 금융/뱅킹사이트의 프로그램소스에서도 마찬가지
입니다.
문제는 분명히 어떤 특정 응용프로그램에서 DB Connection 을 제대로 반환하지 않은
것은 분명한데, 그 "어떤 특정 응용프로그램"이 꼭집어 뭐냐 라는 것을 찾아내기란
정말 쉽지 않습니다. 정말 쉽지 않아요. 1초당 수십개씩의 Request 가 다양하게 들어
오는 상황에서, "netstat -n"으로 보이는 TCP/IP 레벨에서의 DB연결수는 분명히 증가
하고 있는데, 그 수십개 중 어떤 것이 문제를 야기하느냐를 도저히 못찾겠다는 것이지요.
사용자가 아무도 없는 새벽에 하나씩 컨텐츠를 꼭꼭 눌러 본들, 그 문제의 상황은
대부분 정상적인 로직 flow 에서는 나타나지 않고, 어떤 특별한 조건, 혹은 어떤
Exception 이 발생할 때만 나타날 수 있기 때문에, 이런 방법으로는 손발과 눈만 아프게
되곤 합니다.

따라서, 애초 부터, 이러한 상황을 고려하여, 만약, 개발자의 코드에서 실수로 DB연결을
제대로 Pool 에 반환하지 않았을 때, 그 개발자 프로그램 소스의 클래스 이름과 함께
Warnning 성 메세지를 남겨 놓으면, 되지 않겠느냐는 겁니다.

Java 에서는 java.lang.Object 의 finalize() 라는 기막힌 메소드가 있습니다. 해당
Object instance의 reference 가 더이상 그 어떤 Thread 에도 남아 있지 않을 경우
JVM의 GC가 일어날 때 그 Object 의 finalize() 메소드를 꼭 한번 불러주니까요.

계속 반복되는 얘기인데, 아래 글에서 또한번 관련 의미를 찾을 수 있을 것입니다.
Re: DB Connection Pool: Orphan and Idle Timeout
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=1005294960

아래는 이것을 응용하여, Hans Bergsten 의 DBConnectionManager 를 사용하는
ConnectionAdapter 클래스를 만들어 본 샘플입니다. Trace/Debuging 을 위한 몇가지
기능이 첨가되어 있으며, 다른 ConnectionPool을 위한 Connection Adapter 를 만들때도
약간만 수정/응용하여 사용하실 수 있을 것입니다.


/**
* Author : Lee WonYoung, javaservice@hanmail.net
* Date   : 2001.03.20, 2001.10.22
*/

import java.util.*;
import java.sql.*;
import java.io.*;
import java.text.SimpleDateFormat;

public class ConnectionResource
{
    private boolean DEBUG_MODE = true;
    private String DEFAULT_DATASOURCE = "idb";  // default db name
    private long GET_CONNECTION_TIMEOUT = 2000; // wait only 2 seconds if no
                                                // avaiable connection in the pool
    private long WARNNING_MAX_ELAPSED_TIME = 3000;

    private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd/HHmmss");
    private db.DBConnectionManager manager = null;
    private Connection conn = null;
    private String datasource = DEFAULT_DATASOURCE;
    private String caller = "unknown";
    private long starttime = 0;

    private static int used_conn_count = 0;
    private static Object lock = new Object();

    // detault constructor
    public ConnectionResource()
    {
        manager = db.DBConnectionManager.getInstance();
    }

    // For debugging, get the caller object reference
    public ConnectionResource(Object caller_obj)
    {
        this();
        if ( caller_obj != null )
            caller = caller_obj.getClass().getName();
    }

    public Connection getConnection()  throws Exception
    {
        return getConnection(DEFAULT_DATASOURCE);
    }

    public Connection getConnection(String datasource)  throws Exception
    {
        if ( conn != null ) throw new Exception
            ("You must release the connection first to get connection again !!");

        this.datasource = datasource;
        // CONNECTION_TIMEOUT is very important factor for performance tuning
        conn = manager.getConnection(datasource, GET_CONNECTION_TIMEOUT);
        synchronized( lock ) { ++used_conn_count; }
        starttime = System.currentTimeMillis();
        return conn;
    }

    // you don't have to get "connection reference" as parameter,
    // because we already have the referece as the privae member variable.
    public synchronized void release() throws Exception { 
        if ( conn == null ) return;
        // The following is needed for some DB connection pool.
        boolean mode = true;
        try{
            mode = conn.getAutoCommit();
        }catch(Exception e){}
        if ( mode == false ) {
            try{conn.rollback();}catch(Exception e){}
            try{conn.setAutoCommit(true);}catch(Exception e){}
        }
        manager.freeConnection(datasource, conn);
        conn = null;
        int count = 0;
        synchronized( lock ) { count = --used_conn_count; }
        if ( DEBUG_MODE ) {
            long endtime = System.currentTimeMillis();
            if ( (endtime-starttime) > WARNNING_MAX_ELAPSED_TIME ) {
                System.err.println(df.format(new java.util.Date()) +
                    ":POOL:WARNNING:" + count +
                    ":(" + (endtime-starttime) + "):" +
                    "\t" + caller
                );
            }
        }
    }
    // finalize() method will be called when JVM's GC time.
    public void finalize(){
        // if "conn" is not null, this means developer did not release the "conn".
        if ( conn != null ) {
            System.err.println(df.format(new java.util.Date()) +
                ":POOL:ERROR connection was not released:" +
                used_conn_count + ":\t" + caller
            );
            release();
        }
    }
}

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

위의 Connection Adaptor 클래스를 Servlet 이나 JSP에서 사용할 때는 다음과 같이
사용할 수 있습니다.


사용법: DB Transaction 처리가 필요치 않을 때...


ConnectionResource resource = null;
Connection conn = null;
Statement stmt = null;
try{
    // For debugging and tracing, I recommand to send caller's reference to
    // the adapter's constructor.
    resource = new ConnectionResource(this); // <--- !!!
    //resource = new ConnectionResource();

    conn = resource.getConnection();
    // or you can use another database name
    //conn = resource.getConnection("other_db");

    stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("select ...");
    while(rs.next()){
        .....
    }
    rs.close();

}
//catch(Exception e){
//    // error handling if you want
//    ....
//}
finally{
    if ( stmt != null ) try{stmt.close();}catch(Exception e){}
    if ( conn != null ) resource.release(); // <--- !!!
}

-------------------------------------------------------------------------------
사용법: Transaction 처리가 필요할 때.

ConnectionResource resource = null;
Connection conn = null;
Statement stmt = null;
try{
    // For debugging and tracing, I recommand to send caller's reference to
    // the adapter's constructor.
    resource = new ConnectionResource(this);
    //resource = new ConnectionResource();

    conn = resource.getConnection();
    // or you can use another database name
    //conn = resource.getConnection("other_db");

    conn.setAutoCommit(false); // <--- !!!

    stmt = conn.createStatement();
    stmt.executeUpdate("update ...");
    stmt.executeUpdate("insert ...");
    stmt.executeUpdate("update ...");
    stmt.executeUpdate("insert ...");
    stmt.executeUpdate("update ...");
   
    int affected = stmt.executeUpdate("update....");
    // depends on your business logic,
    if ( affected == 0 )
        throw new Exception("NoAffectedException");
    else if ( affected > 1 )
        throw new Exception("TooManyAffectedException:" + affected);

    conn.commit(); //<<-- commit() must locate at the last position in the "try{}"
}
catch(Exception e){
    // if error, you MUST rollback
    if ( conn != null ) try{conn.rollback();}catch(Exception e){}

    // another error handling if you want
    ....
    throw e; // <--- throw this exception if you want
}
finally{
    if ( stmt != null ) try{stmt.close();}catch(Exception e){}
    if ( conn != null ) resource.release(); // <-- NOTE: autocommit mode will be
                                            //           initialized
}
-----------------------------------------------------------------

PS: 더 깊이있는 내용을 원하시면 다음 문서들을 참조 하세요...
    JDF 제4탄 - DB Connection Pool
    http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945156790

    JDF 제5탄 - DB Connection Resource Framework
    http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945335633

    JDF 제6탄 - Transactional Connection Resource Framework
    http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945490586


PS: IBM WebSphere의 JDBC Connection Pool 에 관심이 있다면 다음 문서도 꼭 참조
하세요...
    Websphere V3 Connection Pool 사용법
    http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=970209527
    WebSphere V3 DB Connection Recovery 기능 고찰
    http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=967473008

------------------------
NOTE: 2004.04.07 추가
그러나, 2004년 중반을 넘어서는 이 시점에서는, ConnectionResource와 같은 Wapper의
중요성이 별로 높이 평가되지 않는데, 그 이유는 Tomcat 5.0을 비롯한 대부분의 WAS(Web
Application Server)가 자체의 Connection Poolinig기능을 제공하며, 그 사용법은 다음과
같이 JDBC 2.0에 준하여 모두 동일한 형태를 띠고 있기 때문입니다.

InitialContext ctx = new InitialContext();
DataSource ds = (DataSoruce)ctx.lookup("java:comp/env/jdbc/ds");
Connection conn = ds.getConnection();
...
conn.close();

결국 ConnectionResource의 release()류의 메소드는 Vendor별로, JDBC Connection Pool의
종류가 난무하던 3-4년 전에는 어플리케이션코드의 Vendor종속성을 탈피하기 위해 의미를
가졌으나, 지금은 굳이 필요가 없다고 보여 집니다.

단지, caller와 callee의 정보, 즉, 응답시간을 추적한다거나, close() 하지 않은
어플리케이션을 추적하는 의미에서의 가치만 남을 수 있습니다.  (그러나, 이것도,
IBM WebSphere v5의 경우, 개발자가 conn.close() 조차 하지 않을 지라도 자동을 해당
Thread의 request ending시점에 "자동반환"이 일어나게 됩니다.)

---------------------
2005.01.21 추가
JDBC Connection/Statement/ResultSet을 close하지 않은 소스의 위치를 잡아내는 것은
실제 시스템 운영 중에 찾기란 정말 모래사장에서 바늘찾기 같은 것이었습니다.
그러나, 제니퍼(Jennifer2.0)과 같은 APM을 제품을 적용하시면, 운영 중에, 어느 소스의
어느 위치에서 제대로 반환시키지 않았는지를 정확하게 찾아줍니다.
뿐만 아니라, 모든 SQL의 수행통계 및 현재 수행하고 있는 어플리케이션이 어떤 SQL을
수행중인지 실시간으로 확인되니, 성능저하를 보이는 SQL을 튜닝하는 것 등, 많은 부분들이
명확해져 가고 있습니다. 이젠 더이상 위 글과 같은 문서가 필요없는 세상을 기대해 봅니다.

------------------------------------------------------- 
  본 문서는 자유롭게 배포/복사 할 수 있으나 반드시
  이 문서의 저자에 대한 언급을 삭제하시면 안됩니다
================================================
  자바서비스넷 이원영
  E-mail: javaservice@hanmail.net
  PCS:011-898-7904
================================================





Posted by 알 수 없는 사용자
|

최초작성일자: 2000/09/05 16:19:47 
최근 수정일 : 2001.01.27
최근 수정일 : 2001.03.12(nested sql query issue)
최근 수정일 : 2001.03.13(transaction)
최근 수정일 : 2001.03.20(instance variables in JSP)
최근 수정일 : 2001.04.03(문맥수정)
최근 수정일 : 2002.02.06("close 할 땐 제대로..." 추가사항첨가)
최근 수정일 : 2002.02.25("transaction관련 추가")
최근 수정일 : 2002.06.11(PreparedStatement에 의한 ResultSet close 이슈)
최근 수정일 : 2002.06.18(PreparedStatement관련 추가)
최근 수정일 : 2002.12.30(Instance Variable 공유 1.2 추가)


다들 아실법한 단순한 얘깁니다만, 아직 많은 분들이 모르시는 것 같아 다시한번 
정리합니다. 아래의 각각의 예제는 잘못 사용하고 계시는 전형적인 예들입니다.

1. 서블렛에서 instance variable 의 공유

1.1 서블렛에서 instance variable 의 공유 - PrintWriter - 

  다음과 같은 코드를 생각해 보겠습니다.

 import java.io.*;
 import javax.servlet.*;
 import javax.servlet.http.*;

 public class CountServlet extends HttpServlet {
     private PrintWriter out = null; // <-------------- (1)
 
     public void doGet(HttpServletRequest req, HttpServletResponse res)  
         throws ServletException, IOException
     {
         res.setContentType("text/html");
         out = res.getWriter();
         for(int i=0;i<20;i++){
             out.println("count= " + (i+1) + "<br>");  // <---- (2)
             out.flush();
             try{Thread.sleep(1000);}catch(Exception e){}
         }
    }
  }

 위의 CountServlet.java 를 컴파일하여 돌려 보면, 1초간격으로 일련의 숫자가 올라가는
 것이 보일 겁니다.(서블렛엔진의 구현방식에 따라 Buffering 이 되어 20초가 모두 지난
 후에서 퍽 나올 수도 있습니다.)
 혼자서 단일 Request 를 날려 보면, 아무런 문제가 없겠지만, 이제 브라우져 창을 두개 이상
 띄우시고 10초의 시간 차를 두시면서 동시에 호출해 보세요... 이상한 증상이 나타날
 겁니다. 먼저 호출한 창에는 10 까지 정도만 나타나고, 10초 뒤에 호출한 창에서는 먼저
 호출한 창에서 나타나야할 내용들까지 덤으로 나타나는 것을 목격할 수 있을 겁니다.

 이는 서블렛의 각 호출은 Thread 로 동작하여, 따라서, 각 호출은 위의 (1) 에서 선언한
 instance variable 들을 공유하기 때문에 나타나는 문제입니다.

 위 부분은 다음과 같이 고쳐져야 합니다.

  public class CountServlet extends HttpServlet {
     //private PrintWriter out = null;
 
     public void doGet(HttpServletRequest req, HttpServletResponse res)  
         throws ServletException, IOException
     {
         PrintWriter out = null; // <--- 이 쪽으로 와야죠 !!!
         res.setContentType("text/html");
         out = res.getWriter();
         for(int i=0;i<20;i++){
             out.println("count= " + (i+1) + "<br>");  // <---- (2)
             out.flush();
             try{Thread.sleep(1000);}catch(Exception e){}
         }
    }
  }

 국내 몇몇 Servlet 관련 서적의 일부 예제들이 위와 같은 잘못된 형태로 설명한 
 소스코드들이 눈에 띕니다. 빠른 시일에 바로 잡아야 할 것입니다.

 실제 프로젝트 환경에서 개발된 실무시스템에서도, 그러한 책을 통해 공부하신듯, 동일한
 잘못된 코딩을 하고 있는 개발자들이 있습니다. 결과적으로 테스트 환경에서는 나타나지
 않더니만, 막상 시스템을 오픈하고나니 고객으로 부터 다음과 같은 소리를 듣습니다. 
 "내 데이타가 아닌데 남의 데이타가 내 화면에 간혹 나타나요. refresh 를 누르면 또, 
 제대로 되구요" .....
 

1.2 서블렛에서 instance variable 의 공유

 앞서의 경우와 의미를 같이하는데, 다음과 같이 하면 안된다는 얘기지요.

  public class BadServlet extends HttpServlet {
     private String userid = null;
     private String username = null;
     private int hitcount = 0;
 
     public void doGet(HttpServletRequest req, HttpServletResponse res)  
         throws ServletException, IOException
     {
         res.setContentType("text/html");
         PrintWriter out = res.getWriter();
         userid = request.getParameter("userid");
         username = request.getParameter("username");
         hitcount = hitcount + 1;
         ....
    }
  }

 새로운 매 HTTP 요청마다 userid/username변수는 새롭게 할당됩니다. 문제는 그것이 특정
 사용자에 한하여 그러한 것이 아니라, BadServlet의 인스턴스(instance)는 해당
 웹컨테이너(Web Container)에 상에서 (예외경우가 있지만) 단 하나만 존재하고, 서로 다른
 모든 사용자들의 서로 다른 모든 요청들에 대해서 동일한 userid/username 및 count 변수를
 접근하게 됩니다. 따라서, 다음과 같이 메소드 안으로 끌어들여 사용하여야 함을 강조합니다.

  public class BadServlet extends HttpServlet {
     //private String userid = null; // <---- !!
     //private String username = null; // <---- !!
     private int hitcount = 0;
 
     public void doGet(HttpServletRequest req, HttpServletResponse res)  
         throws ServletException, IOException
     {
         res.setContentType("text/html");
         PrintWriter out = res.getWriter();
         String userid = request.getParameter("userid"); // <---- !!
         String username = request.getParameter("username"); // <---- !!

         //또한, instance 변수에 대한 접근은 적어도 아래처럼 동기화를 고려해야...
         synchronized(this){ hitcount = hitcount + 1; } 
         ....
    }
  }


1.3 서블렛에서 instance variable 의 공유  - DataBase Connection -

 public class TestServlet extends HttpServlet {
     private final static String drv = "oracle.jdbc.driver.OracleDriver";
     private final static String url = "jdbc:orache:thin@210.220.251.96:1521:ORA8i";
     private final static String user = "scott";
     private final static String password = "tiger";

     private ServletContext context;
     private Connection conn = null;  <--- !!!
     private Statement stmt = null; <------ !!!
     private ResultSet rs = null; <------ !!!
 
     public void init(ServletConfig config) throws ServletException {
         super.init(config);
         context = config.getServletContext();
         try {
             Class.forName(drv);
         }
         catch (ClassNotFoundException e) {
             throw new ServletException("Unable to load JDBC driver:"+ e.toString());
         }
     }
     public void doGet(HttpServletRequest req, HttpServletResponse res)  
         throws ServletException, IOException, SQLException
     {
         String id = req.getParameter("id");
         conn = DriverManager.getConnection(url,user,password);   ---- (1)
         stmt = conn.createStatement();  ---------- (2)
         rs = stmt.executeQuery("select .... where id = '" + id + "'"); ----- (3)
         while(rs.next()) { ----------- (4)
            ......  --------- (5)
         }  
         rs.close();  -------- (6)
         stmt.close();  ---- (7)
         conn.close();  --- (8)
         .....
    }
  }

  위에서 뭐가 잘못되었죠? 여러가지가 있겠지만, 그 중에 하나가 java.sql.Connection과
  java.sql.Statment, java.sql.ResultSet을 instance variable 로 사용하고 있다는 것입니다.

  이 서블렛은 사용자가 혼자일 경우는 아무런 문제를 야기하지 않습니다. 그러나 여러사람이
  동시에 이 서블렛을 같이 호출해 보면, 이상한 증상이 나타날 것입니다.
  그 이유는 conn, stmt, rs 등과 같은 reference 들을 instance 변수로 선언하여 두었기 
  때문에 발생합니다.  서블렛은 Thread로 동작하며 위처럼 instance 변수 영역에 선언해 둔
  reference 들은 doGet(), doPost() 를 수행하면서 각각의 요청들이 동시에 공유하게 됩니다.

  예를 들어, 두개의 요청이 약간의 시간차를 두고 비슷한 순간에 doGet() 안으로 들어왔다고
  가정해 보겠습니다.
  A 라는 요청이 순차적으로 (1), (2), (3) 까지 수행했을 때, B 라는 요청이 곧바로 doGet()
  안으로 들어올 수 있습니다. B 역시 (1), (2), (3) 을 수행하겠죠...
  이제 요청 A 는 (4) 번과 (5) 번을 수행하려 하는데, 가만히 생각해 보면, 요청B 로 인해
  요청A에 의해 할당되었던 conn, stmt, rs 의 reference 들은 바뀌어 버렸습니다.
  결국, 요청 A 는  요청 B 의 결과를 가지고 작업을 하게 됩니다. 반면, 요청 B 는
  요청 A 의 의해 rs.next() 를 이미 수행 해 버렸으므로, rs.next() 의 결과가 이미 close 
  되었다는 엉뚱한 결과를 낳고 마는 거죠...
  다른 쉬운 얘기로 설명해 보면, A, B 두사람이 식탁에 앉아서 각자 자신이 준비해 온 사과를
  하나씩 깎아서 식탁 위의 접시에 올려 놓고 나중에 먹어려 하는 것과 동일합니다. A 라는
  사람이 열심히 사과를 깎아 접시에 담아둘 때, B 라는 사람이 들어와서 A가 깎아둔 사과를
  버리고 자신이 깎은 사과를 대신 접시에 담아 둡니다. 이제 A라는 사람은 자신이 깎아서
  담아 두었다고 생각하는 그 사과를 접시에서 먹어버립니다. 곧이어 B라는 사람이 자신의
  사과를 접시에서 먹어려 하니 이미 A 가 먹고 난 후 였습니다. 이는 접시를 두 사람이
  공유하기 때문에 발생하는 문제잖습니까.
  마찬가지로 서블렛의 각 Thread는 instance variable 를 공유하기 때문에 동일한 문제들을
  발생하게 됩니다.

  따라서 최소한 다음처럼 고쳐져야 합니다.

 public class TestServlet extends HttpServlet {
     private final static String drv = "...";
     private final static String url = "....";
     private final static String user = "...";
     private final static String password = "...";

     private ServletContext context;
 
     public void init(ServletConfig config) throws ServletException {
         super.init(config);
         context = config.getServletContext();
         try {
             Class.forName(drv);
         }
         catch (ClassNotFoundException e) {
             throw new ServletException("Unable to load JDBC driver:"+ e.toString());
         }
     }
     public void doGet(HttpServletRequest req, HttpServletResponse res)  
         throws ServletException, IOException, SQLException
     {
         Connection conn = null;  <----- 이곳으로 와야죠..
         Statement stmt = null; <-------
         ResultSet rs = null; <---------

         String id = req.getParameter("id");
         conn = DriverManager.getConnection(url,user,password); 
         stmt = conn.createStatement();
         rs = stmt.executeQuery("select ..... where id = '" + id + "'");
         while(rs.next()) {
            ......  
         }  
         rs.close();
         stmt.close();
         conn.close();
         .....
     }
  }


1.4 JSP에서 Instance Variable 공유

 JSP에서 아래처럼 사용하는 경우가 위의 경우와 동일한 instance 변수를 공유하는 경우가
 됩니다.
   ---------------------------------------------------------
   <%@ page session=.... import=.... contentType=........ %>
   <%! 
       Connection conn = null;
       Statement stmt = null;
       ResultSet rs = null;
       String userid = null;
   %>
   <html><head></head><body>
   <%
       ........
       conn = ...
       stmt = .....

       uesrid = ......
   %>
   </body></html>
   ---------------------------------------------------------

   마찬가지로 위험천만한 일이며, 여러 Thread 에 의해 그 값이 변할 수 있는 변수들은
   <%! ... %> 를 이용하여 선언하시면 안됩니다. 이처럼 instance 변수로 사용할 것은
   다음 처럼, 그 값이 변하지 않는 값이거나, 혹은 공유변수에 대한 특별한 관리를
   하신 상태에서 하셔야 합니다.

   <%!  private static final String USERID = "scott";
        private static final String PASSWORD = "tiger";
   %>

   JSP에서의 이와 같은 잘못된 유형도, 앞선 서블렛의 경우처럼 일부 국내 JSP관련 
   서적에서 발견됩니다.  해당 서적의 저자는 가능한 빨리 개정판을 내셔서 시정하셔야
   할 것입니다. 해당 책은 출판사나 책의 유형, 그리고 글자체로 추정건데, 초보자가
   쉽게 선택할 법한 책인 만큼 그 파급력과 영향력이 너무 큰 듯 합니다.
 
   이와 같은 부분이 실 프로젝트에서 존재할 경우, 대부분 시스템 오픈 첫날 쯤에
   문제를 인식하게 됩니다. Connection reference 가 엎어쳐지므로 Pool 에 반환이
   일어나지 않게 되고, 이는 "connection pool"의 가용한 자원이 부하가 얼마 없음에도
   불구하고 모자라는 현상으로 나타나며, 때론 사용자의 화면에서는 엉뚱한 다른
   사람의 데이타가 나타나거나, SQLException 이 나타납니다.

   NOTE: 어떻게하란 말입니까? 각 호출간에 공유되어서는 안될 변수들은 <%! ...%> 가
   아니라, <% ... %> 내에서 선언하여 사용하란 얘깁니다. JSP가 Pre-compile되어
   Servlet형태로 변환될 때, <%! ... %>는 서블렛의 instance variable로 구성되는 반면,
   <% ... %>는 _jspService() 메소드 내의 method variable로 구성됩니다.



2. 하나의 Connection을 init()에서 미리 연결해 두고 사용하는 경우.

 public class TestServlet extends HttpServlet {
     private final static String drv = "oracle.jdbc.driver.OracleDriver";
     private final static String url = "jdbc:orache:thin@210.220.251.96:1521:ORA8i";
     private final static String user = "scott";
     private final static String password = "tiger";

     private ServletContext context;
     private Connection conn = null;  <--- !!!
 
     public void init(ServletConfig config) throws ServletException {
         super.init(config);
         context = config.getServletContext();
         try {
             Class.forName(drv);
             conn = DriverManager.getConnection(url,user,password);
         }
         catch (ClassNotFoundException e) {
             throw new ServletException("Unable to load JDBC driver:"+ e.toString());
         }
         catch (SQLException e) {
             throw new ServletException("Unable to connect to database:"+ e.toString());
         }
     }
     public void doGet(HttpServletRequest req, HttpServletResponse res)  
         throws ServletException, IOException, SQLException
     {
         Statement stmt = null;
         ResultSet rs = null;
         String id = req.getParameter("id");
         stmt = conn.createStatement();
         rs = stmt.executeQuery("select ..... where id = '" + id + "'");
         while(rs.next()) {
            ......  
         }  
         rs.close(); 
         stmt.close();
         .....
     }
     public void destroy() {
         if ( conn != null ) try {conn.close();}catch(Exception e){}
     }     
  }

 위는 뭐가 잘못되었을 까요?  서블렛당 하나씩 java.sql.Connection 을 init()에서 미리
 맺어 두고 사용하는 구조 입니다.

 얼핏 생각하면 아무런 문제가 없을 듯도 합니다. doGet() 내에서 별도의 Statement와 
 ResultSet 을 사용하고 있으니, 각 Thread는 자신만의 Reference를 갖고 사용하게 되니까요.

 이 구조는 크게 세가지의 문제를 안고 있습니다. 하나는 DB 연결자원의 낭비를 가져오며,
 두번째로 수많은 동시사용자에 대한 처리한계를 가져오고, 또 마지막으로 insert, update,
 delete 와 같이 하나 이상의 SQL문장을 수행하면서 단일의 Transaction 처리를 보장받을
 수 없다는 것입니다.

 1) DB 자원의 낭비

   위의 구조는 서블렛당 하나씩 java.sql.Connection 을 점유하고 있습니다. 실 프로젝트에서
   보통 서블렛이 몇개나 될까요? 최소한 100 개에서 400개가 넘어 갈 때도 있겠죠?
   그럼 java.sql.Connection에 할당 되어야 할 "DB연결갯수"도 서블렛 갯수 많큼 필요하게
   됩니다. DB 연결 자원은 DB 에서 설정하기 나름이지만, 통상 maximum 을 셋팅하기
   마련입니다. 그러나 아무런 요청이 없을 때도 400 여개의 DB연결이 연결되어 있어야 한다는
   것은 자원의 낭비입니다.
    
 2) 대량의 동시 사용자 처리 불가.

   또한, 같은 서블렛에 대해 동시에 100 혹은 그 이상의 요청이 들어온다고 가정해 보겠습
   니다. 그럼 같은 java.sql.Connection 에 대해서 각각의 요청이 conn.createStatement() 를
   호출하게 됩니다.
   문제는 하나의 Connection 에 대해 동시에 Open 할 수 있는 Statement 갯수는 ( 이 역시
   DB 에서 셋팅하기 나름이지만 ) maximum 제한이 있습니다. Oracle 의 경우 Default는 50
   입니다. 만약 이 수치 이상을 동시에 Open 하려고 하면 "maximum open cursor exceed !"
   혹은 "Limit on number of statements exceeded"라는 SQLExceptoin 을 발생하게 됩니다.

   예를 들어 다음과 같은 프로그램을 실행시켜 보세요.

   public class DbTest {
     public static void main(String[] args) throws Exception {
        Class.forName("jdbc driver...");
        Connection conn = DriverManager.getConnection("url...","id","password");
        int i=0;
        while(true) {
           Statement stmt = conn.createStatement();
           System.out.println( (++i) + "- stmt created");
        }
     }
   }

   과연 몇개 까지 conn.createStement() 가 수행될 수 있을까요? 이는 DB에서 설정하기 나름
   입니다. 중요한 것은 그 한계가 있다는 것입니다.
   또한 conn.createStatement() 통해 만들어진 stmt 는 java.sql.Connection 의 자원이기
   때문에 위처럼 stmt 의 reference 가 없어졌다고 해도 GC(Garbage Collection)이 되지
   않습니다.


  3) Transaction 중복현상 발생

  예를 들어 다음과 같은 서비스가 있다고 가정해 보겠습니다.

  public void doGet(HttpServletRequest req, HttpServletResponse res)  
      throws ServletException, IOException, SQLException
  {
      Statement stmt = null;
      String id = req.getParameter("id");
      try {
          conn.setAutoCommit(false);
          stmt = conn.createStatement();
          stmt.executeUpdate("update into XXXX..... where id = " + id + "'");
          stmt.executeUpdate("delete from XXXX..... where id = " + id + "'");
          stmt.executeUpdate(".... where id = " + id + "'");
          stmt.executeUpdate(".... where id = " + id + "'");
          conn.commit();
      }
      catch(Exception e){
          try{conn.rollback();}catch(Exception e){}
      }
      finally {
         if ( stmt != null ) try{stmt.close();}catch(Exception e){}
         conn.setAutoCommit(true);
      }
      .....
  }

  아무런 문제가 없을 듯도 합니다. 그러나 위의 서비스를 동시에 요청하게 되면, 같은 
  java.sql.Connection 을 갖고 작업을 하고 있으니 Transaction 이 중첩되게 됩니다.
  왜냐면, conn.commit(), conn.rollback() 과 같이 conn 이라는 Connection 에 대해서
  Transaction 이 관리되기 때문입니다. 요청 A 가 총 4개의 SQL문장 중 3개를 정상적으로
  수행하고 마지막 4번째의 SQL문장을 수행하려 합니다. 이 때 요청 B가 뒤따라 들어와서
  2개의 SQL 문장들을 열심히 수행했습니다.  근데, 요청 A에 의한 마지막 SQL 문장
  수행중에 SQLException 이 발생했습니다. 그렇담 요청 A 는 catch(Exception e) 절로
  분기가 일어나고 conn.rollback() 을 수행하여 이미 수행한 3개의 SQL 수행들을 모두
  rollback 시킵니다. 근데,,, 문제는 요청 B 에 의해 수행된 2개의 SQL문장들도 같이
  싸잡아서 rollback() 되어 버립니다. 왜냐면 같은 conn 객체니까요. 결국, 요청B 는
  영문도 모르고 마지막 2개의 SQL문장만 수행한 결과를 낳고 맙니다.

  따라서 정리하면, Connection, Statement, ResultSet 는 doGet() , doPost() 내에서
  선언되고 사용되어져야 합니다.

  public void doGet(HttpServletRequest req, HttpServletResponse res)  
      throws ServletException, IOException, SQLException 
  {
      Connection conn = null;  <----- 이곳으로 와야죠..
      Statement stmt = null; <-------
      ResultSet rs = null; <---------
      .....
  }




 3. Exception 이 발생했을 때도 Connection 은 닫혀야 한다 !!

  public void doGet(HttpServletRequest req, HttpServletResponse res)  
       throws ServletException, IOException, SQLException
  {
      String id = req.getParameter("id");

      Connection conn = DriverManager.getConnection("url...","id","password");
      Statement stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("ssselect * from XXX where id = '" + id + "'");
      while(rs.next()) {
         ......  
      }  
      rs.close();
      stmt.close();
      conn.close();
      .....
  }

  위에선 뭐가 잘못되었을까요? 네, 사실 특별히 잘못된 것 없습니다. 단지 SQL문장에 오타가
  있다는 것을 제외하곤.... 근데, 과연 그럴까요?
  SQLException 이라는 것은 Runtime 시에 발생합니다. DB 의 조건이 맞지 않는다거나
  개발기간 중에 개발자의 실수로 SQL문장에 위처럼 오타를 적을 수도 있죠.
  문제는 Exception 이 발생하면 마지막 라인들 즉, rs.close(), stmt.close(), conn.close()
  가 수행되지 않는다는 것입니다.
  java.sql.Connection 은 reference 를 잃더라도 JVM(Java Virtual Machine)의 GC(Garbage 
  Collection) 대상이 아닙니다. 가뜩이나 모자라는 "DB연결자원"을 특정한 어플리케이션이
  점유하고 놓아 주지 않기 때문에 얼마안가 DB Connection 을 더이상 연결하지 못하는
  사태가 발생합니다.

  따라서 다음처럼 Exception 이 발생하든 발생하지 않든 반드시 java.sql.Connection 을 
  close() 하는 로직이 꼭(!) 들어가야 합니다.

  public void doGet(HttpServletRequest req, HttpServletResponse res)  
       throws ServletException, IOException, SQLException 
  {
      Connection conn = null;
      Statement stmt = null;
      ResultSet rs = null;
      String id = req.getParameter("id");
      try {
        conn = DriverManager.getConnection("url...","id","password");
        stmt = conn.createStatement();
        rs = stmt.executeQuery("sselect * from XXX where id = '" + id + "'");
        while(rs.next()) {
         ......  
        }
        rs.close();
        stmt.close();
      }
      finally {
         if ( conn != null ) try {conn.close();}catch(Exception e){}
      }
      .....
  }

  참고로, 실프로젝트의 진단 및 튜닝을 나가보면 처음에는 적절한 응답속도가 나오다가
  일정한 횟수 이상을 호출하고 난 뒤부터 엄청 응답시간이 느려진다면, 십중팔구는 위처럼
  java.sql.Connection 을 닫지 않아서 생기는 문제입니다.
  가용한 모든 Connection 을 연결하여 더이상 연결시킬 Connection 자원을 할당할 수 없을
  때, 대부분 timewait 이 걸리기 때문입니다. 일단 DB관련한 작업이 들어오는 족족
  timewait에 빠질 경우, "어플리케이션서버"에서 동시에 처리할 수 있는 최대 갯수만큼
  호출이 차곡차곡 쌓이는 건 불과 몇분 걸리지 않습니다. 그 뒤부터는 애궂은 dummy.jsp
  조차 호출이 되지 않게 되고,   누군가는 "시스템 또 죽었네요"라며 묘한 웃음을 짓곤
  하겠죠....



4. Connection 뿐만 아니라 Statement, ResultSet 도 반드시 닫혀야 한다 !!

4.1  3번의 예제에서 Connection 의 close() 만 고려하였지 Statement 나 ResultSet 에 대한
  close는 전혀 고려 하지 않고 있습니다. 무슨 문제가 있을까요? Statement 를 닫지 않아도
  Connection 을 닫았으니 Statement 나 ResultSet 은 자동으로 따라서 닫히는 것 아니냐구요?
  천만의 말씀, 만만의 콩깎지입니다.

  만약, DB Connection Pooling 을 사용하지 않고 직접 JDBC Driver 를 이용하여 매번 DB
  연결을 하였다가 끊는 구조라면 문제가 없습니다.
  그러나 DB Connection Pooling 은 이젠 보편화되어 누구나 DB Connection Pooling 을 사용
  해야한다는 것을 알고 있습니다. 그것이 어플리케이션 서버가 제공해 주든, 혹은 작은
  서블렛엔진에서 운영하고 있다면 직접 만들거나, 인터넷으로 돌아다니는 남의 소스를 가져다
  사용하고 있을 겁니다.

  이처럼 DB Connection Pooling 을 사용하고 있을 경우는 Conneciton 이 실제 close()되는
  것이 아니라 Pool에 반환되어 지게 되는데, 결국 reference가 사리지지 않기 때문에 GC시점에
  자동 close되지 않게 됩니다.
  특정 Connection 에서 열어둔 Statement 를  close() 하지 않은채 그냥 반환시켜 놓게 되면,
  언젠가는 그 Connection 은 다음과 같은   SQLException 을 야기할 가능성을 내포하게 됩니다.

  Oracle :
    java.sql.SQLException : ORA-01000: maximum open cursor exceeded !!
                            (최대열기 커서수를 초과했습니다)
  UDB DB2 :
    COM.ibm.db2.jdbc.DB2Exception: [IBM][JDBC 드라이버] CLI0601E  유효하지 않은
      명령문 핸들 또는 명령문이 닫혔습니다. SQLSTATE=S1000
    COM.ibm.db2.jdbc.DB2Exception: [IBM][CLI Driver] CLI0129E  핸들(handle)이 
      더이상 없습니다. SQLSTATE=HY014        
    COM.ibm.db2.jdbc.DB2Exception: [IBM][CLI Driver][DB2/NT] SQL0954C  응용프로그램
      힙(heap)에 명령문을 처리하기 위해 사용 가능한 저장영역이 충분하지 않습니다.
      SQLSTATE=57011

  이유는 앞 2)번글에서 이미 언급드렸습니다. 보다 자세한 기술적 내용은 아래의 글을
  참고하세요.
  Connection/Statement 최대 동시 Open 수
  http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=jdbc&c=r_p&n=972287002

  따라서 또다시 3번의 소스는 다음과 같은 유형으로 고쳐져야 합니다.

  public void doGet(HttpServletRequest req, HttpServletResponse res)  
       throws ServletException, IOException, SQLException
  {
      Connection conn = null;
      Statement stmt = null;
      ResultSet rs = null;
      String id = req.getParameter("id");
      try {
        conn = ...<getConnection()>...; // (편의상 생략합니다.)
        stmt = conn.createStatement();
        rs = stmt.executeQuery("sselect * from XXX where id = '" + id + "'");
        while(rs.next()) {
         ......  
        }
        // rs.close();
        // stmt.close();
      }
      finally {
        if ( rs != null ) try {rs.close();}catch(Exception e){}
        if ( stmt != null ) try {stmt.close();}catch(Exception e){} // <-- !!!!
        if ( conn != null ) ...<releaseConnection()>...; // (편의상 생략)

      }
      .....
  }

 4.2 사실 위와 같은 구조에서, java.sql.Statement의 쿼리에 의한 ResultSet의 close()에
  대한 것은 그리 중요한 것은 아닙니다. ResultSet은 Statement 가 close() 될 때 함께
  자원이 해제됩니다. 따라서 다음과 같이 하셔도 됩니다.

  public void doGet(HttpServletRequest req, HttpServletResponse res)  
       throws ServletException, IOException, SQLException
  {
      Connection conn = null;
      Statement stmt = null;
      String id = req.getParameter("id");
      try {
        conn = ...<getConnection()>...;
        stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("sselect * from XXX where id = '" + id + "'");
        while(rs.next()) {
         ......  
        }
        rs.close(); //<--- !!!
      }
      finally {
        // if ( rs != null ) try {rs.close();}catch(Exception e){}
        if ( stmt != null ) try {stmt.close();}catch(Exception e){}
        if ( conn != null ) ...<releaseConnection()>...;
      }
      .....
  }


 4.3  가장 대표적으로 잘못 프로그래밍하고 있는 예를 들라면 다음과 같은 유형입니다.

  Connection conn = null;
  try {
      conn = ...<getConnection()>....;
      Statement stmt = conn.createStatement();
      stmt.executeUpdate("....");  <--- 여기서 SQLException 이 일어나면...
      .....
      .....
      .....  <-- 만약,이쯤에서 NullPointerException 이 일어나면 ?
      .....
      stmt.close(); <-- 이것을 타지 않음 !!!
  }
  finally{
     if ( conn != null ) ...<releaseConnection()>...;
  }


 4.4  Statement 가 close() 되지 않는 또 하나의 경우가 다음과 같은 경우입니다.

  Connection conn = null;
  Statement stmt = null;
  try {
    conn = .....
    stmt = conn.createStatement(); // ....(1)
    rs = stmt.executeQuery("select a from ...");
    .....
    rs.close();

    stmt = conn.createStatement(); // ....(2)
    rs = stmt.executeQuery("select b from ...");
    ....
  }
  finally{
    if ( rs != null ) try {rs.close();}catch(Exception e){}
    if ( stmt != null ) try{stmt.close();}catch(Exception e){}
    if ( conn != null ) ....
  }

  즉, 두번 stmt = conn.createStatement() 를 수행함으로써, 먼저 생성된 stmt 는 
  (2)번에 의해 그 reference 가 엎어쳐버리게 되어 영원히 close() 되지 않은채 
  남아 있게 됩니다.
  이 경우, (2)번 문장을 주석처리하여야 겠지요. 한번 생성된 Statement 로 여러번
  Query 를 수행하면 됩니다.

  Connection conn = null;
  Statement stmt = null;
  try {
    conn = .....
    stmt = conn.createStatement(); // ....(1)
    rs = stmt.executeQuery("select a from ...");
    .....
    rs.close();

    // stmt = conn.createStatement(); // <--- (2) !!!
    rs = stmt.executeQuery("select b from ...");
    ....
  }
  finally{
    if ( rs != null ) try {rs.close();}catch(Exception e){}
    if ( stmt != null ) try{stmt.close();}catch(Exception e){}
    if ( conn != null ) ....
  }


 4.5  Statement 뿐만 아니라 PreparedStatement 를 사용할 때로 마찬가지 입니다.
  ....
  PreparedStatement pstmt = conn.prepareStatement("select ....");
  ....
  이렇게 만들어진  pstmt 도 반드시 pstmt.close() 되어야 합니다.

  예를 들면, 다음과 같은 코드를 생각할 수 있습니다.

  Connection conn = null;
  try {
    conn = ...<getConnection()>...;
    PreparedStatement pstmt = conn.prepareStatement("select .... ?...?");
    pstmt.setString(1,"xxxx");
    pstmt.setString(2,"yyyy");
    ResultSet rs = pstmt.executeQuery(); <--- 여기서 SQLException 이 일어나면
    while(rs.next()){
      ....
    }
    rs.close();
    pstmt.close();  <-- 이것을 타지 않음 !!!
  }
  finally{
     if ( conn != null ) ...<releaseConnection()>...;
  }

  따라서 같은 맥락으로 다음과 같이 고쳐져야 합니다.
  
  Connection conn = null;
  PreparedStatement pstmt = null; // <-------- !!
  ResultSet rs = null;
  try {
    conn = ...<getConnection()>...;
    pstmt = conn.prepareStatement("select .... ?...?");
    pstmt.setString(1,"xxxx");
    pstmt.setString(2,"yyyy");
    rs = pstmt.executeQuery(); <--- 여기서 SQLException 이 일어나더라도...
    while(rs.next()){
      ....
    }
    //rs.close();
    //pstmt.close(); 
  }
  finally{
    if ( rs != null ) try {rs.close();}catch(Exception e){}
    if ( pstmt != null ) try {pstmt.close();}catch(Exception e){} // <-- !!!!
    if ( conn != null ) ...<releaseConnection()>...;
  }


 4.6  PreparedStatement 에 관련해서 다음과 같은 경우도 생각할 수 있습니다.

 4.6.1 앞서의 4.4에서 Statement를 createStatement() 연거푸 두번 생성하는 것과
  동일하게 PreparedStatement에서도 주의하셔야 합니다. 예를 들면 다음과 같습니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  ResultSet rs = null;
  try {
    conn = .....
    pstmt = conn.prepareStatement("select a from ..."); // ....(1)
    rs = pstmt.executeQuery();
    .....
    rs.close();

    pstmt = conn.prepareStatement("select b from ..."); // <--- (2) !!!
    rs = pstmt.executeQuery();
    ....
  }
  finally{
    if ( rs != null ) try {rs.close();}catch(Exception e){}
    if ( pstmt != null ) try{pstmt.close();}catch(Exception e){} // <--- (3)
    if ( conn != null ) ...<releaseConnection()>...;
  }

  Statement의 인스턴스는 conn.createStatement() 시에 할당되어 지는 반면, 
  PreparedStatement는 conn.prepareStatement("..."); 시에 할당되어 집니다. 위의 경우에서
  설령 마지막 finally 절에서 pstmt.close() 를 하고 있기는 하지만, (1)번에서 할당되어진
  pstmt 는 (2)에서 엎어쳤기 때문에 영원히 close() 되지 않게 됩니다. 따라서, 다음과
  같이 코딩되어야 한다는 것은 자명합니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  ResultSet rs = null;
  try {
    conn = .....
    pstmt = conn.prepareStatement("select a from ..."); // ....(1)
    rs = pstmt.executeQuery();
    .....
    rs.close();
    pstmt.close(); // <------- !!!!! 이처럼 여기서 먼저 close() 해야지요.

    pstmt = conn.prepareStatement("select b from ..."); // <--- (2)
    rs = pstmt.executeQuery();
    ....
  }
  finally{
    if ( rs != null ) try {rs.close();}catch(Exception e){}
    if ( pstmt != null ) try{pstmt.close();}catch(Exception e){} // <--- (3)
    if ( conn != null ) ...<releaseConnection()>...;
  }

  혹은 다음과 같이 서로 다른 두개의 PreparedStatement를 이용할 수도 있습니다.
  
  Connection conn = null;
  PreparedStatement pstmt1 = null;
  ResultSet rs1 = null;
  PreparedStatement pstmt2 = null;
  ResultSet rs2 = null;
  try {
    conn = .....
    pstmt1 = conn.prepareStatement("select a from ..."); // ....(1)
    rs1 = pstmt1.executeQuery();
    .....
    pstmt2 = conn.prepareStatement("select b from ..."); // <--- (2)
    rs2 = pstmt2.executeQuery();
    ....
  }
  finally{
    if ( rs1 != null ) try {rs1.close();}catch(Exception e){}
    if ( pstmt1 != null ) try{pstmt1.close();}catch(Exception e){} // <--- (3)
    if ( rs2 != null ) try {rs2.close();}catch(Exception e){}
    if ( pstmt2 != null ) try{pstmt2.close();}catch(Exception e){} // <--- (4)
    if ( conn != null ) ...<releaseConnection()>...;
  }


 4.6.2 아래는 앞서의 4.6.1과 같은 맥락인데, for loop 안에서 사용된 경우입니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  try {
    conn = ...<getConnection()>...;
    for(int i=0;i<10;i++){
      pstmt = conn.prepareStatement("update .... ?... where id = ?"); //... (1)
      pstmt.setString(1,"xxxx");
      pstmt.setString(2,"id"+(i+1) );
      int affected = pstmt.executeUpdate();
      if ( affected == 0 ) throw new Exception("NoAffected");
      else if ( affedted > 1 ) throw new Exception("TooManyAffected");
    }
  }
  finally{
     if ( pstmt != null ) try {pstmt.close();}catch(Exception e){} // ...(2)
     if ( conn != null ) ...<releaseConnection()>...;
  }

  이 경우가 실제 프로젝트 performace 튜닝을 가 보면 종종 발견되는 잘못된
  유형 중의 하나입니다. 핵심은 pstmt.prepareStatement("update..."); 문장을
  수행할 때 마다 내부적으로 pstmt 가 새로 할당된다는 것에 있습니다.
  결국, (1)번 문장이 for loop 을 돌면서 9개는 pstmt.close()가 되지 않고, 마지막
  하나의 pstmt 만  finally 절의 (2)번에서 close 되지요...
  따라서 다음처럼 고쳐져야 합니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  try {
    conn = ...<getConnection()>...;
    pstmt = conn.prepareStatement("update .... ?... where id = ?");
    for(int i=0;i<10;i++){
      pstmt.clearParameters();      
      pstmt.setString(1,"xxxx");
      pstmt.setString(2,"id"+(i+1) );
      int affected = pstmt.executeUpdate();
      if ( affected == 0 ) throw new Exception("NoAffected");
      else if ( affedted > 1 ) throw new Exception("TooManyAffected");
    }
  }
  finally{
     if ( pstmt != null ) try {pstmt.close();}catch(Exception e){}
     if ( conn != null ) ...<releaseConnection()>...;
  }

  PreparedStatement 라는 것이, 한번 파싱하여 동일한 SQL문장을 곧바로 Execution할 수
  있는 장점이 있는 것이고, 궁극적으로 위와 같은 경우에 효과를 극대화 할 수 있는
  것이지요.

  어느 개발자의 소스에서는 위의 경우를 다음과 같이 for loop 안에서 매번
  conn.prepareStatement(...)를 하는 경우를 보았습니다.

  Connection conn = null;
  
  try {
    conn = ...<getConnection()>...;
    for(int i=0;i<10;i++) {
        PreparedStatement pstmt = null;
        try{
            pstmt = conn.prepareStatement("update .... ?... where id = ?"); 
            pstmt.clearParameters();      
            pstmt.setString(1,"xxxx");
            pstmt.setString(2,"id"+(i+1) );
            int affected = pstmt.executeUpdate();
            if ( affected == 0 ) throw new Exception("NoAffected");
            else if ( affedted > 1 ) throw new Exception("TooManyAffected");
        }
        finally{
            if ( pstmt != null ) try {pstmt.close();}catch(Exception e){}
        }
    }
  }
  finally{
     if ( conn != null ) ...<releaseConnection()>...;
  }

  위 경우는 장애관점에서 보면 사실 별 문제는 없습니다. 적어도 닫을 건 모두 잘 닫고
  있으니까요. 단지 효율성의 문제가 대두될 수 있을 뿐입니다.

 4.7 생각해 보면, Statement 나 PreparedStatement 가 close() 되지 않는 유형은
  여러가지가 있습니다. 그러나 열려진 Statement는 반드시 close()되어야 한다라는
  단순한 사실에 조금만 신경쓰시면 어떻게 프로그래밍이 되어야 하는지 쉽게
  감이 오실 겁니다. 단지 신경을 안쓰시니 문제가 되는 것이지요...

  Statement 를 닫지 않는 실수를 한 코드가 400-1000여개의 전 어플리케이션을 통털어
  한두군데에 숨어 있는 경우가 제일 찾아내기 어렵습니다. 한두번 Statement 를
  close 하지 않는다고 하여 곧바로 문제로 나타나지 않기 때문입니다.
  하루나 이틀, 혹은 며칠지나서야, Oracle 의 경우, "maximum open cursor exceed"
  에러를 내게 됩니다. oracle 의 경우, 위와 같은 에러를 conn.createStatement()시에
  발생하므로 (경우에 따라) 큰 문제로 이어지지는 않습니다. 찾아서 고치면 되니까요.

  반면, DB2 의 경우는 메모리가 허용하는 한도까지 지속적인 메모리 증가를 야기합니다.
  급기야 "핸들(handle)이 더이상 없습니다", "응용프로그램 힙(heap)에 명령문을 
  처리하기 위해 사용 가능한 저장영역이 충분하지 않습니다", "유효하지 않은 명령문
  핸들 또는 명령문이 닫혔습니다" 등과 같은 에러를 내지만, 여기서 그치는 것이 아니라
  새로운 connection 을 맺는 시점에 SIGSEGV 를 내면 crashing 이 일어납니다.
  java.lang.OutOfMemoryError 가 발생하기 때문이지요.

  Oracle 의 경우도 사실 상황에 따라 심각할 수 있습니다. 예를 들어, 어떤 개발자가
  "maximum open cursor exceed"라는 Oracle SQL 에러 메세지를 만나고는 자신이
  코딩을 잘못한 것은 염두에 두지 않고, 무조건 DBA에게 oraXXX.ini 파일에서
  OPEN_CURSORS 값을 올려달라고 요청하고, DBA는 그 조언(?)을 충실히 받아들여 
  Default 50 에서 이를 3000 으로 조정합니다. 여기서 문제는 깊숙히(?) 숨겨집니다.
  close() 안된 Statement 가 3000 회에 도달하지 전까진 아무도 문제를 인식하지
  못하기 때문이죠. 그러나, Statement가 하나씩 close()되지 않은 갯수에 비례하여
  Oracle 엔진의 메모리 사용은 자꾸만 증가하고, 전체적인 성능저하를 야기하지만, 
  이를 인식하기란 막상 시스템을 오픈하고 나서 3-4일이 지난 후에 "DB성능이 이상하네,
  응답속도가 느리네" 하면서 또 한번의 "maximum open cursor exceed" 메세지를
  확인하고 난 뒤의 일이 되곤 합니다.

  에러가 없는 정상적인 로직 flow 에서는 대부분 Statement가 잘 닫힐 겁니다. 그러나,
  어떤 이유에서건 아주 드물게 Runtime Exception 이 발생하여 exception throwing으로
  인해 "stmt.close()"를 타지 않는 경우가 제일 무섭지요. 정말 무섭지요...

  Statement, PreparedStatement, CallableStatement 모두 마찬가지 입니다.



5. close() 를 할 땐 제대로 해야 한다!!

 5.1 다음과 같은 프로그램 형식을 생각할 수 있습니다.

  Connection conn = null;
  Statement stmt = null;
  ResultSet rs = null;
  try{
     conn = ...<getConnection()>...; //.......(1)
     stmt = conn.createStatement();    //.............(2)
     rs = stmt.executeQuery("select .....");  // .....(3)
     while(rs.next()){
       ......
     }
  }
  finally {
     try {
        rs.close(); //........(4)
        stmt.close(); //......(5)
        ...<releaseConneciton()>...; //......(6)
     }catch(Exception e){}
  }

  위에선 뭐가 잘못되었을까요? 다 제대로 한듯 한데....
  finally 절에서 rs, stmt, conn 을 null check 없이, 그리고 동일한 try{}catch 절로
  실행하고 있습니다.
  예를 들어, (1), (2) 번을 거치면서 conn 과 stmt 객체까지는 제대로 수행되었으나
  (3)번 Query문장을 수행하는 도중에 SQLException 이 발생할 수 있습니다. 그러면, 
  finally  절에서 (4) 번 rs.close()를 수행하려 합니다. 그러나, executeQuery()가
  실패 했기 때문에 rs 의 reference 는 null 이므로 rs.close() 시에 
  NullPointerException 이 발생합니다.  결국 정작 반드시 수행되어야할 (5)번, (6)번이
  실행되지 않습니다. 따라서 반드시(!) 다음과 같은 형식의 코딩이 되어야 합니다.

  Connection conn = null;
  Statement stmt = null;
  ResultSet rs = null;
  try{
     conn = ...<getConnection()>...;
     stmt = conn.createStatement();
     rs = stmt.executeQuery("select .....");
     while(rs.next()){
       ......
     }
  }
  catch(Exception e){
     ....
  }
  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     if ( conn != null ) ...<releaseConnection()>...;
  }

 같은 맥락으로 PreparedStatement 를 사용할 경우도, 다음과 같은 코딩은 잘못되었습니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  try{
     conn = ...<getConnection()>...; //.......(1)
     pstmt = conn.prepareStatement("ddelete from EMP where empno=7942"); //...(2)
     int k = pstmt.executeUpdate();  // .....(3)
     ......
  }
  finally {
     try {
        pstmt.close(); //......(4)
        ...<releaseConneciton()>...; //......(5)
     }catch(Exception e){}
  }
  왜냐면, SQL문장에 오타가 있었고, 이는 prepareStatement("ddelete..")시점에 에러가
  발생하여 pstmt 가 여전히 null 인 상태로 finally 절로 분기되기 때문인 것이죠.

 5.2.0 앞서 4.2에서도 언급했지만, java.sql.Statement의 executeQuery()에 의한 ResultSet은
  Statement 가 close 될때 자원이 같이 해제되므로 다음과 같이 하여도 그리 문제가 되진
  않습니다. 
     
  Connection conn = null;
  Statement stmt = null;
  //ResultSet rs = null;
  try{
     conn = ...<getConnection()>...;
     stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("select .....");
     while(rs.next()){
       ......
     }
     rs.close(); // <---- !!!
  }
  catch(Exception e){
     ....
  }
  finally {
     //if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     if ( conn != null ) ...<releaseConnection()>...;
  }


5.2.1  그러나 PreparedStatement에 의한 ResultSet close()는 얘기가 다를 수 있습니다.
  (2002.06.11 추가 사항)
  
  많은 분들이, "아니 설령 ResultSet를 close하지 않았더라도 Statement/PreparedStatement를
  close하면 함께 ResultSet로 close되는 것 아니냐, JDBC 가이드에서도 그렇다고
  나와 있다, 무슨 개뿔같은 소리냐?" 라구요.

  그러나, 한가지 더 이해하셔야 할 부분은, 웹스피어, 웹로직과 같은 상용 웹어플리케이션
  서버들은 성능향상을 위해 PreparedStatement 및 심지어 Statement에 대한 캐싱기능을
  제공하고  있습니다. pstmt.close() 를 하였다고 하여 정말 해당 PreparedStatement가
  close되는 것이 아니라, 해당 PreparedeStatement가 생겨난 java.sql.Connection당 지정된
  개수까지 웹어플리케이션서버 내부에서 reference가 사라지지 않고 캐시로 남아 있게 됩니다.
  결국, 연관된 ResultSet 역시 close()가 되지 않은 채 남아있게 되는 것이지요. 명시적인
  rs.close()를 타지 않으면, 웹어플리케이션서버의 JVM내부 힙영역 뿐만아니라, 쿼리의 결과로
  데이타베이스에서 임시로 만들어진 메모리데이타가 사라지지 않는 결과를 낳게 됩니다. 
  특히 ResultSet이 닫히지 않으면 DB에서의 CURSOR가 사라지지 않습니다. 
  또한, CURSOR를 자동으로 닫거나 닫지 않는 등의 옵션은 WAS마다 WAS의 DataSource설정에서
  CORSOR 핸들링에 대한 별도의 옵션을 제공하게 되며 특히 아래 [항목6]에서 다시 언급할
  nested sql query 처리시에 민감한 반응을 하게 됩니다.

  따라서, 앞서의 코딩 방법을 반드시 다음처럼 ResultSet도 close하시기를 다시금 정정하여
  권장 드립니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  ResultSet rs = null; // <---- !!!
  try{
     conn = ...<getConnection()>...;
     pstmt = conn.prepareStatement("select .....");
     rs = pstmt.executeQuery(); // <----- !!!
     while(rs.next()){
       ......
     }
     //rs.close(); // <---- !!!
  }
  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 ) ...<releaseConnection()>...;
  }

  PS: 웹어플리케이션서버에서의 PreparedStatement 캐싱기능에 관한 부분은 아래의 글들을
  참조하세요.
  Connection Pool & PreparedStatement Cache Size
  http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=995572195
  WebLogic에서의 PreparedStatement Cache -서정희-
  http://www.javaservice.net/~java/bbs/read.cgi?m=dbms&b=jdbc&c=r_p&n=1023286823



 5.3 간혹, 다음과 같은 코딩은 문제를 야기하지 않을 것이라고 생각하는 분들이 많습니다.

 finally{
    try{
      if ( stmt != null ) stmt.close();
      if ( conn != null ) conn.close();
    }catch(Exception e){}
 }
 저명한 많은 책에서도 위처럼 코딩되어 있는 경우를 종종봅니다. 맞습니다. 특별히 문제성이
 있어보이지는 않습니다. 그러나, 간혹 웹어플리케이션서버의 Connection Pool과 연계하여
 사용할 땐 때론 문제가 될 때도 있습니다. 즉, 만약, stmt.close()시에 Exception이 throw
 되면 어떻게 되겠습니까? 
 아니 무슨 null 체크까지 했는데, 무슨 Exception이 발생하느냐고 반문할 수도 있지만,
 오랜 시간이 걸리는 SQL JOB에 빠져 있다가 Connection Pool의 "Orphan Timeout"이 지나
 자동으로 해당 Connection을 Pool에 돌려보내거나 혹은 특별한 marking처리를 해 둘 수
 있습니다. 이 경우라면 stmt.close()시에 해당 웹어플리케이션서버에 특화된 Exception이
 발생하게 됩니다. 그렇게 되면 conn.close()를 타지 못하게 되는 사태가 벌어집니다.
 따라서, 앞서 하라고 권장한 형식으로 코딩하세요.


6. Nested (Statemet) SQL Query Issue !!

 6.1 아래와 같은 코딩을 생각해 보겠습니다.

  Connection conn = null;
  Statement stmt = null;
  try{
     conn = ...<getConnection()>...;
     stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("select deptno ...where id ='"+ id +"'");
     while(rs.next()){
       String deptno = rs.getString("deptno");
       stmt.executeUpdate(
           "update set dept_name = ... where deptno = '"+ deptno +"'"
       );
       ......
     }
     rs.close();
  }
  catch(Exception e){
     ....
  }
  finally {
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     if ( conn != null ) ...<releaseConnection()>...;
  }

  위 코드는 사실 실행하자 말자 다음과 같은 에러를 만나게 됩니다.

  DB2 : -99999: [IBM][CLI Driver] CLI0115E 커서 상태가 유효하지 않습니다. 
        SQLSTATE=24000 
  Oracle : 

  에러는 두번째 while(rs.next()) 시에 발생합니다. 왜냐면, Statement가 nested하게
  실행된 executeUpdate() 에 의해 엎어쳐 버렸기 때문입니다. ResultSet 은 
  Statement와 밀접하게 관련이 있습니다. ResultSet은 자료를 저장하고 있는 객체가
  아니라, 데이타베이스와 step by step으로 상호 통신하는 Interface 일 뿐이기 때문입니다.
  따라서 위 코드는 다음처럼 바뀌어져야 합니다.

  Connection conn = null;
  Statement stmt1 = null;
  Statement stmt2 = null;
  try{
     conn = ...<getConnection()>...;
     stmt1 = conn.createStatement();
     stmt2 = conn.createStatement();
     ResultSet rs = stmt1.executeQuery("select deptno ...where id ='"+ id +"'");
     while(rs.next()){
       String deptno = rs.getString("deptno");
       stmt2.executeUpdate(
           "update set dept_name = ... where deptno = '"+ deptno +"'"
       );
       ......
     }
     rs.close();
  }
  catch(Exception e){
     ....
  }
  finally {
     if ( stmt1 != null ) try{stmt1.close();}catch(Exception e){}
     if ( stmt2 != null ) try{stmt2.close();}catch(Exception e){}
     if ( conn != null ) ...<releaseConnection()>...;
  }

  즉, ResultSet에 대한 fetch 작업이 아직 남아 있는 상태에서 관련된 Statement를
  또다시 executeQuery()/executeUpdate() 를 하면 안된다는 것입니다.

  PS: IBM WebSphere 환경일 경우, 아래의 글들을 추가로 확인하시기 바랍니다.
  349   Re: Function sequence error  (Version 3.x)
  http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=991154615
  486   WAS4.0x: Function sequence error 해결  
  http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=1015345459



7. executeUpdate() 의 결과를 비즈니스 로직에 맞게 적절히 활용하라.

 7.1 아래와 같은 코딩을 생각해 보겠습니다.

  public void someMethod(String empno) throws Exception {
      Connection conn = null;
      Statement stmt = null;
      try{
         conn = ...<getConnection()>...;
         stmt = conn.createStatement();
         stmt.executeUpdate(
             "UPDATE emp SET name='이원영' WHERE empno = '" + empno + "'"
         );
      }
      finally {
         if ( stmt != null ) try{stmt.close();}catch(Exception e){}
         if ( conn != null ) ...<releaseConnection()>...;
      }
  }

  사실 흔히들 이렇게 하십니다. 별로 잘못된 것도 없어보입니다. 근데, 만약 
  DB TABLE에 해당 empno 값이 없으면 어떻게 될까요? SQLException 이 발생
  하나요? 그렇지 않죠? 아무런 에러없이 그냥 흘러 내려 옵니다. 그러면, Update를
  하러 들어 왔는데, DB에 Update할 것이 없었다면 어떻게 해야 합니까? 그냥 무시하면
  되나요? 안되죠..  따라서, 다음 처럼, 이러한 상황을 이 메소드를 부른 곳으로
  알려 줘야 합니다.

  public void someMethod(String empno) throws Exception {
      Connection conn = null;
      Statement stmt = null;
      try{
         conn = ...<getConnection()>...;
         stmt = conn.createStatement();
         int affected = stmt.executeUpdate(
             "UPDATE emp SET name='이원영' WHERE empno = '" + empno + "'"
         );
         if ( affected == 0 ) throw new Exception("NoAffectedException");
         else if ( affected > 1 ) throw new Exception("TooManyAffectedException");
      }
      finally {
         if ( stmt != null ) try{stmt.close();}catch(Exception e){}
         if ( conn != null ) ...<releaseConnection()>...;
      }
  }

  물론 이러한 부분들은 해당 비즈니스로직이 뭐냐에 따라서 다릅니다. 그것을 무시케
  하는 것이 비즈니스 로직이었다면 그냥 무시하시면 되지만, MIS 성 어플리케이션의
  대부분은 이처럼 update 나 delete 쿼리의 결과에 따라 적절한 처리를 해 주어야
  할 것입니다.



8. Transaction 처리를 할 땐 세심하게 해야 한다.

 단, 아래는 웹어플리케이션서버의 JTA(Java Transaction API)기반의 트렌젝션처리가 아닌,
 java.sql.Connection에서의 명시적인 conn.setAutoCommit(false) 모드를 통한 트렌젝션처리에
 대해서 언급 하고 있습니다.

 8.1 Non-XA JDBC Transaction(명시적인 java.sql.Connection/commit/rollback)

  Connection conn = null;
  Statement stmt = null;
  try{
     conn = ...<getConnection()>...;
     stmt = conn.createStatement();
     stmt.executeUpdate("UPDATE ...."); // -------- (1)
     stmt.executeUpdate("DELETE ...."); // -------- (2)
     stmt.executeUpdate("INSERT ...."); // -------- (3)
  }
  finally {
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     if ( conn != null ) ...<releaseConnection()>...;
  }

  위와 같은 코딩은 아무리 비즈니스적 요구가 간소하더라도, 실 프로젝트에서는
  있을 수가 없는 코딩입니다.(JTS/JTA가 아닌 명시적인 Transaction 처리시에..)

  다들 아시겠지만, (1), (2) 번까지는 정상적으로 잘 수행되었는데, (3)번문장을
  수행하면서 SQLException 이 발생하면 어떻게 되나요? (1),(2)은 이미 DB에 반영된
  채로 남아 있게 됩니다. 대부분의 비즈니스로직은 그렇지 않았을 겁니다.
  
  "(1),(2),(3)번이 모두 정상적으로 수행되거나, 하나라도 잘못되면(?) 모두 취소
  되어야 한다" 
  
  가 일반적인 비즈니스적 요구사항이었을 겁니다. 따라서, 다음처럼 코딩 되어야
  합니다.

  Connection conn = null;
  Statement stmt = null;
  try{
     conn = ...<getConnection()>...;
     conn.setAutoCommit(false);
     stmt = conn.createStatement();
     stmt.executeUpdate("UPDATE ...."); // -------- (1)
     stmt.executeUpdate("DELETE ...."); // -------- (2)
     stmt.executeUpdate("INSERT ...."); // -------- (3)

     conn.commit(); // <-- 반드시 try{} 블럭의 마지막에 와야 합니다.
  }
  catch(Exception e){
     if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
     // error handling if you want
     
     throw e;  // <--- 필요한 경우, 호출한 곳으로 Exception상황을 알려줄
               //      수도 있습니다
  }
  finally {
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     // in some connection pool, you have to reset commit mode to "true"
     if ( conn != null ) ...<releaseConnection()>...;
  }


 8.2 auto commit mode 를 "false"로 셋팅하여 명시적인 Transaction 관리를 할 때,
  정말 조심해야 할 부분이 있습니다. 예를 들면 다음과 같은 경우입니다.

  Connection conn = null;
  Statement stmt = null;
  try{
     conn = ...<getConnection()>...;
     conn.setAutoCommit(false);
     stmt = conn.createStatement();
     stmt.executeUpdate("UPDATE ...."); // ----------------------- (1)
     ResultSet rs = stmt.executeQuery("SELECT ename ..."); // ---- (2)
     if ( rs.next() ) {
        conn.commit(); // ------------------- (3)
     }
     else {
        conn.rollback(); // ----------------- (4)
     }
  }
  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     // in some connection pool, you have to reset commit mode to "true"
     if ( conn != null ) ...<releaseConnection()>...;
  }

  코드가 왜 위처럼 됐는지는 저도 모르겠습니다.  단지 비즈니스가 그러했나보죠..

  문제는 만약, (2)번 executeQuery()문장을 수행하면서 SQLException 이나 기타의
  RuntimeException 이 발생할 때 입니다.
  commit() 이나 rollback()을 타지 않고, finally 절로 건너 뛰어 Statement를
  닫고, connection 은 반환됩니다. 이때, commit() 이나 rollback()이 되지 않은채
  (1)번 UPDATE 문이 수행된채로 남아 있게 됩니다.  이는 DB LOCK을 점유하게
  되고, 경우에 따라 다르겠지만, 다음번 요청시에 DB LOCK으로 인한 hang현상을
  초래할 수도 있습니다.
  일단 한두개의 어플리케이션이 어떠한 이유였든, DB Lock 을 발생시키면, 해당
  DB에 관련된 어플리케이션들이 전부 응답이 없게 됩니다. 이 상황이 조금만
  지속되면, 해당 waiting 을 유발하는 요청들이 어플리케이션서버의 최대 동시
  접속처리수치에 도달하게 되고, 이는 전체 시스템의 hang현상으로 이어지게
  되는 것이죠..


  따라서, 비즈니스 로직이 어떠했든, 반드시 지켜져야할 사항은 다음과 같습니다.

  "conn.setAutoComm(false); 상태에서 한번 실행된 Update성 SQL Query는 이유를
   막론하고 어떠한 경우에도 commit() 이나 rollback() 되어야 한다."

  위의 경우라면 다음처럼 catch 절에서 rollback 시켜주는 부분이 첨가되면 되겠네요.

  Connection conn = null;
  Statement stmt = null;
  try{
     conn = ...<getConnection()>...;
     conn.setAutoCommit(false);
     stmt = conn.createStatement();
     stmt.executeUpdate("UPDATE ...."); 
     ResultSet rs = stmt.executeQuery("SELECT ename ...");
     if ( rs.next() ) {
        conn.commit();
     }
     else {
        conn.rollback();
     }
  }
  catch(Exception e){  // <---- !!!!!
     if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
     throw e;
  }
  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     // in some connection pool, you have to reset commit mode to "true"
     if ( conn != null ) ...<releaseConnection()>...;
  }


 8.3 모든 경우의 수를 생각하라.

  다음과 같은 경우를 생각해 보겠습니다.

  Connection conn = null;
  Statement stmt = null;
  try{
     conn = ...<getConnection()>...;
     conn.setAutoCommit(false);
     stmt = conn.createStatement();
     stmt.executeUpdate("UPDATE ...."); 
     String idx = name.substring(3);
     ResultSet rs = stmt.executeQuery("SELECT ename ... where idx=" + idx);
     if ( rs.next() ) {
        .....
     }
     rs.close(); rs = null;

     stmt.executeUpdate("UPDATE ...."); 
     conn.commit();
  }
  catch(SQLException e){ 
     if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
     throw e;
  }
  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     // in some connection pool, you have to reset commit mode to "true"
     if ( conn != null ) ...<releaseConnection()>...;
  }

  잘 찾아 보세요. 어디가 잘못되었습니까? 잘 안보이시죠?

  Connection conn = null;
  Statement stmt = null;
  try{
     conn = ...<getConnection()>...;
     conn.setAutoCommit(false);
     stmt = conn.createStatement();
     stmt.executeUpdate("UPDATE ...."); //---- (1)
     String idx = name.substring(3); //<------ (2) NullPointerExceptoin ?
     ResultSet rs = stmt.executeQuery("SELECT ename ... where idx=" + idx);
     if ( rs.next() ) {
        .....
     }
     rs.close(); rs = null;

     stmt.executeUpdate("UPDATE ...."); 
     conn.commit();
  }
  catch(SQLException e){ //<------ (3) 이 부분을 탈까?
     if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
     throw e;
  }
  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( stmt != null ) try{stmt.close();}catch(Exception e){}
     // in some connection pool, you have to reset commit mode to "true"
     if ( conn != null ) ...<releaseConnection()>...;
  }

  위 코드를 보듯, 만약 (1)을 수행 후 (2)번 에서 NullPointerException 이나 
  ArrayIndexOutOfBoundException이라도 나면 어떻게 되죠? catch(SQLException ...)에는
  걸리지 않고 곧바로 finally 절로 건너띄어 버리네요. 그럼 (1)에서 update 된 것은
  commit()이나 rollback() 없이 connection 이 반환되네요... ;-)
  어떻게 해야 합니까? SQLException만 잡아서 되는 것이 아니라, catch(Exception ...)과
  같이 모든 Exception 을 catch해 주어야 합니다.


 8.4 위 주석문에서도 언급해 두었지만, Hans Bergsteins 의 DBConnectionManager.java
  와 같은 Connection Pool 을 사용할 경우에, 개발자의 코드에서 transaction auto
  commit mode 를 명시적으로 "false"로 한 후, 이를 그냥 pool 에 반환하시면,
  그 다음 사용자가 pool 에서 그 connection 을 사용할 경우, 여전히 transaction
  mode 가 "false"가 된다는 것은 주지하셔야 합니다. 따라서, DBConnectionManger의
  release method를 수정하시든지, 혹은 개발자가 명시적으로 초기화한 후 pool 에
  반환하셔야 합니다. 그렇지 않을 경우, DB Lock 이 발생할 수 있습니다.
  반면, IBM WebSphere 나 BEA WebLogic 과 같인 JDBC 2.0 스펙에 준하는 Connection
  Pool을 사용할 경우는 반환할 당시의 transaction mode 가 무엇이었든 간에,
  pool 에서 꺼내오는 connection 의 transaction mode는 항상 일정합니다.
  (default 값은 엔진의 설정에 따라 달라집니다.)
  그렇다고 commit 시키지 않은 실행된 쿼리가 자동으로 commit/rollback 되는 것은
  아닙니다. 단지 auto commit 모드만 자동으로 초기화 될 뿐입니다.

  PS:WAS의 JTS/JTA 트렌젝션 기반으로 운영될 경우는 명시적으로 commit/rollback되지
   않은 트렌젝션은 자동으로 rollback되거나 commit됩니다. default는 WAS 설정에 따라
   다를 수 있습니다.

  ---------------
  NOTE: 자바서비스컨설팅의 WAS성능관리/모니터링 툴인 제니퍼(Jennifer 2.0)를 적용하면,
  어플리케이션에서 명시적으로 commit/rollback시키지 않고 그대로 반환시킨 어플리케이션의
  소스 위치를 실시간으로 감지하여 알려줍니다. 이를 만약 수작업으로 한다면, 수많은 코드
  중 어디에서 DB lock을 유발 시키는 코드가 숨어있는지를 찾기가 경우에 따라 만만치 않은
  경우가 많습니다.

8.5 XA JDBC Driver, J2EE JTS/JTA
  JDBC 2.0, 표준 javax.sql.DataSource를 통한 JDBC Connection을 사용할 경우에,
  대부분의 상용WAS제품들은 J2EE의 표준 JTS(Java Transaction Service)/JTA(Java Transaction
  API)를 구현하고 있습니다. 특별히, 하나 이상의 데이타베이스에서 2 phase-commit과
  같은 XA Protocol를 지원하고 있지요(사실 WAS에서 2PC가 지원되기 시작한 것은 몇년
  되지 않습니다. 2PC를 사용하려면 반드시 XA-JDBC Driver가 WAS에 설치되어야 합니다)

  샘플 예제는 다음과 같습니다.

    ...
    javax.transaction.UserTransaction tx = null;
    java.sql.Connection conn1 = null;
    java.sql.Statement stmt1 = null;

    java.sql.Connection conn2 = null;
    java.sql.Statement stmt2 = null;
    java.sql.CallableStatement cstmt2 = null;
    
    try {
        javax.naming.InitialContext ctx = new javax.naming.InitialContext();
        tx = (javax.transaction.UserTransaction) ctx.lookup("java:comp/UserTransaction");
        // 트렌젝션 시작
        tx.begin();

        // -------------------------------------------------------------------------
        // A. UDB DB2 7.2 2PC(XA) Test
        // -------------------------------------------------------------------------
        javax.sql.DataSource ds1 = 
            (javax.sql.DataSource)ctx.lookup("java:comp/env/jdbc/DB2");
        conn1 = ds1.getConnection();

        stmt1 = conn1.createStatement();
        stmt1.executeUpdate(
            "insert into emp(empno,ename) values(" + empno + ",'Name" + empno + "')"
        );
        stmt1.executeUpdate(
            "update emp set ename = 'LWY" + count + "' where empno = 7934"
        );
        java.sql.ResultSet rs1 = stmt1.executeQuery("select empno,ename from emp");
        while(rs1.next()){
            ...
        }
        rs1.close();
        
        // -------------------------------------------------------------------------
        // B. Oracle 8.1.7 2PC(XA) Test
        // -------------------------------------------------------------------------
        javax.sql.DataSource ds2 =
            (javax.sql.DataSource)ctx.lookup("java:comp/env/jdbc/ORA8i");
        conn2 = ds2.getConnection();
        
        stmt2 = conn2.createStatement();
        stmt2.executeUpdate(
            "update emp set ename = 'LWY" + count + "' where empno = 7934"
        );
        java.sql.ResultSet rs2 = stmt2.executeQuery("select empno,ename from emp");
        while(rs2.next()){
            ...
        }
        rs2.close();


        // -------------------------------------------------------------------------
        // 트렌젝션 commit
        tx.commit();
    }
    catch(Exception e){
        // 트렌젝션 rollback
        if ( tx != null ) try{tx.rollback();}catch(Exception ee){}
        ...
    }
    finally {
        if ( stmt1 != null ) try { stmt1.close();}catch(Exception e){}
        if ( conn1 != null ) try { conn1.close();}catch(Exception e){}

        if ( stmt2 != null ) try { stmt2.close();}catch(Exception e){}
        if ( conn2 != null ) try { conn2.close();}catch(Exception e){}
    }

  

NOTE: 위에서 설명한 하나하나가 제 입장에서 보면 너무나 가슴깊이 다가오는 
  문제들입니다. 개발하시는 분의 입장에서 보면, 위의 가이드에 조금 어긋났다고
  뭐그리 문제겠느냐고 반문하실 수 있지만, 수백본의 소스코드 중에 위와 같은 규칙을
  준수하지 않은 코드가  단 하나라도 있다면, 잘 운영되던 시스템이 며칠에 한번씩
  에러를 야기하거나 응답이 느려지고 급기야 hang 현상을 초래하는 결과를 가져 옵니다.
  정말(!) 그렇습니다.

NOTE: 위에서 사용한 코딩 샘플들은 JDBC Connection Pooling 은 전혀 고려치 않고
  설명드렸습니다. 그러했기 때문에 <getConnection()>, <releaseConnection()> 이란
  Pseudo 코드로 설명했던 것입니다.
  반드시 "서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-" 을 읽어 보세요.
  http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=968522077


-------------------------------------------------------  
  본 문서는 자유롭게 배포/복사 할 수 있으나 반드시
  이 문서의 저자에 대한 언급을 삭제하시면 안됩니다
================================================
  자바서비스넷 이원영
  E-mail: javaservice@hanmail.net
  PCS:010-6239-6498
================================================

'java' 카테고리의 다른 글

JSON-PRC-java 적용하기  (0) 2006.02.25
서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-  (0) 2006.02.21
리소스 누수현상  (0) 2006.02.21
java.sql.SQLException: IO 예외 상황: Broken pipe - thread  (0) 2006.01.31
Jakarta POI - 쓰기  (0) 2005.11.30
Posted by 알 수 없는 사용자
|

리소스 누수현상

java 2006. 2. 21. 10:07

http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=consult&c=r_p&n=1106886851

제목 : 긴급! 리소스 누수 현상 ㅜ.ㅜ
글쓴이: 김영근(naucika) 2005/01/28 13:34:11 조회수:7258 줄수:41
안녕하세요. 
이번에 사내 SFA 서비스가 말썽이 생겨서 도움을 좀 부탁드립니다.
프로젝트가 끝난지는 2년이 다 되가는데요.
이번에 자주 웹서버가 다운되는 바람에 애를 먹고 있습니다.
도대체 원인을 모르겠고, 뭘 더 어떻게 해야할지 모르겠네요.

환경을 말씀드리면,
JBOSS 3.2.5 구요. 서버는 제온 프로세서 2, 메모리 1G 정도입니다. 윈도2003 서버구요.
두개의 서비스가 구동중인데요.
하나는 SFA(영업관리 시스템), 또 하나는 채용시스템입니다.

SFA는 초기모델이라 자체 프레임웍에 서블릿 JSP로 구성되어 있고, JBOSS에서 제공하고 
있는 DB POOL을 사용합니다.
채용시스템은 하이버네이트+스트럿츠+서블릿+JSP+스프링 프레임웍등으로 
구성되어 있구요 하이버네이트에 플러그인인 proxool 을 설정해서 쓰고 있습니다.
둘다 최소 10개 최대 30개 정도의 커넥션 풀로 구성되게 해놓았구요.

사용자층은 SFA는 사내 영업직원 약 300명 가량 동시접속자 2~30명정도구요.
채용은 몰릴때 일시적으로 몰리는데요 하루 400명정도 가량 접속을 합니다.

일단 로그를 확인해보면, 
어쩔땐 OutOfMemory 로 완전히 엔진이 맛이 간적도 있구요.
connection time out으로 pool에서 커넥션을 제대로 못가져오는 경우도 있구요.
getOutputStream() has already been called for this response -> 이에러도 많이 발생합니다.
정말 궁금한거 위에러는 열린거 또 열려고 할때 발생하는것 같은데,
이건 도대체 감을 잡을수가 없습니다. 

소스도 디테일하게 체크해봤지만, 에러가 발생한다고 해서 커넥션 반납에 문제가 있을
만한 내용은 없었습니다. finally 혹은 외부에서 exception을 처리해서 최대한 신경을
썻는데요.. 스트레스 테스트는 고사하고, 모니터링이라도 제대로 해보고 싶은데
튜닝쪽 정보나 실력이 영 모자랍니다.

일단, 전문가가 계시면 비용문제를 떠나서 좀 와서 봐주셨으면 좋겠구요.
아니면 이런 튜닝쪽을 의뢰할 방법이나 연락처라도 알려주시면 좋겠구요.
그것도 아니면, 힌트라도.. ㅜ.ㅜ 위에서 난리가 나서, 정말 갑갑합니다.
도와주세요~



온세상을 자바로~ -_-!!
http://cafe.naver.com/javalove
제목 : Re: 메모리 누수현상 원인 분석 방법
글쓴이: 이원영(javaservice) 2005/01/29 14:44:23 조회수:3042 줄수:53
언급하신 증상은 조금더 면밀한 조사가 필요하나 대부분 다음과 같은 원인에 의해 발생할
수 있습니다.

1) Statement/PreparedStatement/ResultSet을 정상적으로 닫지 않을 경우, native memory
 leak의 주원인이 되어, (JVM Heap메모리와 무관하게), OutOfMemoryError를 유발하게
 됩니다. 이 경우, 아래의 문서에 기술된 바와 같이 JDBC 코딩 가이드라인을 준수하지
 않은 것이 원인이라, 해당 어플리케이션을 수정하셔야만 해결이 됩니다.

 서블렛 + JDBC 연동시 코딩 고려사항 -제1탄- 
 http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=968185187

 문제는 어떤 어플리케이션에서 그러한 문제를 유발하고 있느냐를 찾기가 운영단계에서는
 쉽지가 않은데, 아래와 같은 실시간 모니터링 툴을 설치하시면 매우 쉽게 찾아집니다.


2) native memory가 아닌 Heap memory 가 정말 부족하게 설정되어 있을 수 있습니다.
 heap memory는 -Xms -Xmx로 설정가능하며, heap memory는 Runtime의 freeMemory()
 totalMemory()를 이용하여 추출가능합니다. 이 또한 위의 모니터링 툴을 통해 보다
 직관적인 실시간 그래프로 모니터링이 가능합니다.

 이 경우는, 극히 정상적으로 어플리케이션이 그렇게 많은 메모리를 사용하고 있는
 구조일 수 있고, 이 경우라면 -Xms -Xmx를 통해 시스템 전체 메모리를 고려하여 충분히
 잡아주는 것과, 혹은 그러한 어플리케이션을 보다 적게 메모리를 사용하는 구조로
 변경하는 것과 같은 노력이 필요합니다.

 혹은 ObjectOutputStream의 재사용과정에서 reset()를 주지 않았다거나,

 TCP/IP Socket ObjectOutputStream.reset() 
 http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=javatip&c=r_p&n=1058887057

 혹은 WAS의 bug에 기인한 heap memory leak이라거나, 혹은 어플리케이션에서 global
 변수인 vector나 hashtable에 지속적으로 저장하는 구조이거나, 어플리케이션 레벨에서의
 cache를 구현하는 과정에서 지속적으로 메모리를 점유하는 등의 원인이 있을 수 있습니다.

 heap memory leak의 추적은 크게 두가지 방법으로 나뉩니다.
 하나는 전용 JVM Profiling Tool을 통해 JVM Heap 메모리 누수를 찾아낸다거나,
 (HP-UX의 Enterprise Edtion의 OS전용분석기, Borland OptimizeIt 등),
 두번째는 OS가 만약 IBM의 AIX이거나 Windows,linux라면 IBM JDK의 자체기능인 heapdump를
 이용하여 이를 HeapRoot라는 툴로 분석하여 매우 쉽게 어떤 객체 어느 곳에서
 memory leak이 일어난지를 어렵지 않은 방법으로 찾아낼 수 있습니다.

 IBM HeapDump & HeapRoots
 http://www.javaservice.net/~java/bbs/read.cgi?m=dbms&b=jdbc&c=r_p&n=1083294657

결론적으로, 우선 Jennifer 전체적인 상황을 보다 면밀히 판단하고, memory leak의
형태에 따라 적절히 그 원인을 추적해 들어가시면 되겠습니다.


자바서비스컨설팅 이원영
e-mail : lwy@javaservice.com
MSN : javaservice@hanmail.net
Phone: 010-6239-6498
제목 : Re: 기본적인 것 부터 확인해 보세요.
글쓴이: 박철휘(guest) 2005/01/30 17:39:34 조회수:1452 줄수:58
이원영씨께서 답해주신 것이 옳은 방법이나 일단 문제가 생긴 시점에서 profilling 기법도
모르는데 그러한 정보로 해결을 한다는 것은 매우 어려운 일인 것 같습니다.

말씀하신대로 전문가의 도움을 받으시는 것이 비용대비 효율상 가장 좋을 것 같습니다.

다만 여의치 않으시다면 일반적인 요령 몇가지가 있습니다.
제 경험으론 이런게 사실 문제의 원인인 경우가 대부분이더군요.
그리고 그래도 안된다면 해결하는데 시간 많이 들어갑니다.

1. 웹서버 다운
서버자체가 먹통이 된다면 서버를 바꾸는 것이 좋을 것입니다.
OS와 하드웨어의 궁합으로 인한 문제가 간혹 있습니다. 
이것은 자바만의 문제가 아닌 모든 application에 해당되며 재수가 없는 것입니다.

application이 다운되는데 OOM(OutOfMemory)라면 십중팔구는 heap memory가 full 이 난
것입니다. 메모리 누수요인을 체크하는 것이 좋은데 이원영씨께서 말씀하신 부분이 원인인
경우가 대부분입니다. 그것이 요인이 아니라면 찾는 것이 어렵지는 않으나 시간이 꽤 걸립니다.
무엇을 실행시켰는데 heap에 메모리를 반환하지 않는지 일일히 확인해봐야 합니다.
Optimizeit으로 찾으려면 수작업이 조금 들어가구요. ServerTrace를 사용하면 매우 직관적이고
편리하게 이 문제를 확인할 수 있습니다.

application이 다운된 것은 아닌데 하얀화면만 나오는 이른바 hang 현상이라면 dead lock인
경우가 많고 간혹 WAS와 JVM의 bug로 인한 것이 있습니다.
이런건 지속적인 dump나 hang이 걸리는 시점에서 몇번의 dump 만으로도 문제의 원인을 찾을
수 있습니다. OOM보다 오히려 문제의 원인을 찾는데 시간이 적게 들어가는 경우가 많습니다.

2. POOL에서의 Connection Time Out
WAS가 허용하는 Connection 갯수 대비 connection pool 갯수가 현저히 적을 경우 발생하는
경우가 많습니다. connection pool 갯수를 늘리든지 WAS의 connection 갯수를 줄이든지 해서
경과를 지켜보시는게 좋을겁니다. 그렇지 않다면 db에서 허용하는 max connection num을
확인해 보세요.

3. getOutputStream()
외부 프레임워크를 쓰는데 이런 일이 발생하는 경우라면 ServletResponse를 직접 제어하는데
제대로 못써서 그런 경우이거나 프레임워크 자체를 잘 못 써서 그런 경우가 많습니다.
보통 파일 다운로드나 redirect를 해놓고 처리를 종료하는 것이 아니라 프레임워크에게 계속
처리가 넘어가는 경우 많이 생깁니다.

4. 하이버네이트+스트럿츠+서블릿+JSP+스프링 프레임웍
요즘 외국에서 인기리에 사용되는 방식인데 이것을 모두 도입하여 사용하실 정도인데 위의
문제가 발생하는 것이 조금 의문이네요.
하이버네이트 : 다 좋은데 가르치는데 시간이 많이 들더군요. 교육비용이 매우 많이 드는 것
같습니다. 이해의 부족으로 인한 결함 요인이 자꾸 생겨서 그때마다 확인해주는 것도 보통
일이 아니더군요. 그냥 포기하고 native sql을 사용하는 DAO로 전환시켰습니다.
스프링 : JBOSS를 사용하신다면 EJB를 사용하시는 것이 아닌가요. 그런데도 spring이 필요한
점이 의외네요.
스트러츠 : 이런거 쓸 때는 reponse를 직접 제어하는거 주의하시는게 좋습니다. 파일다운로드는
별도의 서블릿으로 사용하시는게 속편할 겁니다.
제 개인적인 견해로는 Hibernate를 쓰면서 spring과 결합이라면 transaction에 대한 부분이
매력이지만 critical한 것도 아닌 것이라면 없어도 무리는 없을 것 같은데요. 일반적으로
ThreadLocal을 사용하여 request당 Session 하나를 두는 것을 권장합니다. hibernate에서도
그것을 권장하는 편이구요.

이런 문제를 해결하면서 많은 것을 배우게 됩니다.
하지만 이것 때문에 내 목이 위태로운 상태라면 문제는 보통 심각한게 아니겠지요.
전문가에게 맡기는 방법이라면 프리랜서를 고용하는 방법이나 비싼 WAS 구매하면서 이 부분에
대한 분석을 요청하시면 될 겁니다. (이것도 운이 좀 필요하더군요. 이 문제를 곧바로
해결해주는 엔지니어를 만나신다면 행운입니다.)
제목 : Re: 좋은의견 주셔서 감사합니다.
글쓴이: 김영근(naucika) 2005/01/31 09:15:02 조회수:1133 줄수:28
저번주 금요일부터 나타나는 오류들을 하나하나씩 잡고, 주말에 추이를 좀 지켜보았는데요.
역시나, outofmemory가 떨어졌습니다.

로그확인결과, 
[net.sf.hibernate.collection.PersistentCollection] Failed to lazily initialize a collection
이란 하이버네이트 오류가 발생했구요. 
-> 공지사항 검색 로직인데, lazy 방식으로 로드합니다. 공지사항이 없을리는 만무하고,
특정구간에서 발생하는 오류인듯 싶습니다.

[org.jboss.web.localhost.Engine] StandardWrapperValve[jsp]: 
Servlet.service() for servlet jsp threw exception
java.lang.OutOfMemoryError
그외에 이렇게 oom 이 자주 일어나고, 종국엔 새벽경에 서버가 다운됬습니다.
한두시간정도 이런 oom이 계속 발생한거 같습니다.

철휘님이 말씀하신것중에 hibernate와 jboss 사용이유는 원래 ejb를 사용했던것인데요.
이번에 o/r mapping 으로 리뉴어링 했습니다. 굳이 서버를 변경할 필요까진 없어서
그대로 사용을 했던거구요, 이서버에 sfa(영업관리시스템)을 통합운영하게 된거죠.

그외에 퍼포먼스 가이드를 계속 살펴보고 있습니다.
로그분석도 이번주에 시작해 볼 예정입니다만, 솔직히 자신이 없네요. 
발견되는 내용은 족족 게시판에 업로드 하겠습니다..

수고하세요.


온세상을 자바로~ -_-!!
http://cafe.naver.com/javalove
제목 : Re: 하이버네이트
글쓴이: 손님(guest) 2005/02/01 14:07:32 조회수:1491 줄수:51
기억이 가물가물한데....항상 시간이 지나면 잊어버리니..쩝..
하이버네이트요...세션클로즈방식이 잘못쓰면 저런에러가 나왔던기억이...나네요..
종국에는 이원영님의 1번코멘트가 정답일겁니다.

제 가물가물한 기억으로는...1.1버전썼던것같은데 벌써3.0버전이되버렸네여..

Session session = _sessions.openSession();
        Transaction tx = null;
        List result = null;
        try {
            tx = session.beginTransaction();
            Query q = session.createQuery(
                "select blog.id, blog.name, count(blogItem) " +
                "from Blog as blog " +
                "left outer join blog.items as blogItem " +
                "group by blog.name, blog.id " +
                "order by max(blogItem.datetime)"
            );
            q.setMaxResults(max);
            result = q.list();
            tx.commit();
        }
        catch (HibernateException he) {
            if (tx!=null) tx.rollback();
            throw he;
        }
        finally {
            session.close();
        }
        return result;
    }

이와같이 준수하여 사용하셨는지 함 확인해보시기 바랍니다.

위의 메모리릭은 

1) Statement/PreparedStatement/ResultSet을 정상적으로 닫지 않을 경우, native memory
 leak의 주원인이 되어, (JVM Heap메모리와 무관하게), OutOfMemoryError를 유발하게
 됩니다. 이 경우, 아래의 문서에 기술된 바와 같이 JDBC 코딩 가이드라인을 준수하지
 않은 것이 원인이라, 해당 어플리케이션을 수정하셔야만 해결이 됩니다

요게 정답이라고 사료됩니다. 단 하이버네이트의 경우 세션이라는 개념을 사용합니다..
예전에..XX사는넘님이 이거 담당이였는데...ㅎㅎ
XX사는넘님 글보시면 코멘트부탁드립니다...케케
근데 HQL(Hibernate Query Language) 이거 제대로 쓸려니 머리좀 아프더군여..^^;





제목 : Re: 링크걸어드리져.
글쓴이: 손님(guest) 2005/02/01 14:23:53 조회수:1092 줄수:1
http://forum.hibernate.org/viewtopic.php?t=938011&highlight=failed+lazily+initialize
제목 : Re: 하이버네이트 관련글.
글쓴이: 김영근(naucika) 2005/02/01 18:16:34 조회수:1260 줄수:51
일단, 제경우 하이버네이트를 스프링 프레임웍에 붙였습니다.
코딩은 다음과 같죠.

public Object findMajorByCode(final String code, final String sc_code) throws MasterException,
			HibernateException {
	return getHibernateTemplate().execute(new HibernateCallback() {
  	      public Object doInHibernate(Session session) throws HibernateException, SQLException {
				Criteria crit = getSession().createCriteria(HA020TB.class);
				crit.add(Expression.eq("HA02_SCHOOLCAREER_CD", sc_code));
				crit.add(Expression.eq("HA02_MAJOR_CD", code));
				List list = crit.list();
				if (list.size() > 0) {
					HA020TB data = (HA020TB) list.get(0);
					return data.getHA02_MAJOR_NM();
				} else {
					return "";
				}

			}
		});
	}

위소스처럼 되어있는데요.
세션을 직접 얻어서 쓰는경우가 거의 없습니다.
혹은 아래처럼, 

BA010TB obj = (BA010TB) getHibernateTemplate().get(BA010TB.class, zipid);

식으로, 스프링에서 제공해주는 템플릿을 애용(?)합니다.
이런, 소스내용도 잘못된걸까요?

어쨋든, 한 이틀째 모니터링을 하고 있는데요,
(하이버네이트쪽은 검사하지 않고 있습니다.) 당혹스럽게도..
stmt, rs가 안닫히는 부문이 많습니다. --;;;;
게다가, 불필요한 커넥션을 많이 가져가는 부분도 있구요. (가급적 join으로 처리할려구요)
일단, 소스는 수정하지 않은상태에서 스트레스 테스트를 해볼려구요.
과연 이렇게 안닫혀지는 stmt들이 얼마나 악영향을 끼칠지 궁금합니다.

메모리 덤프로 떠볼려구요. (서비스넷 쥔장님이 그렇게 해보는게 가장 확실하다고 해서용)
어쨋든 일단 소스는 잘못된게 확실하네요.. 
자체 프레임웍을 썻는데, 미흡했나 봅니다.

모니터링 결과는 이번주내에 나올듯 싶습니다.






온세상을 자바로~ -_-!!
http://cafe.naver.com/javalove
제목 : Re: 메모리 리익
글쓴이: 손님(dhseong)(guest) 2005/02/01 19:24:14 조회수:1063 줄수:20
1) Statement/PreparedStatement/ResultSet을 정상적으로 닫지 않을 경우, native memory
 leak의 주원인이 되어....

위와 같은 경우라면 웬만큼 커넥션을 많이 열어서 작업하지 않고서는 OutOfMemory가 
발생하기 전에 (1) DB에서 inactive size의 증가나 cursor의 증가가 먼저 확인되리라
생각됩니다.

메모리 릭 자체에 집중하시기 전에 Connection Pool관련하여 Timeout도 발생한다고
하니 DB쪽에서 어떤 증상으로 나타나는지 점검하시는게 좋을듯 합니다.

Timeout관련해서 커넥션 (2) Pool min/max를 조절하시어 증상확인도 하셔야 할듯 합니다.

그리고, (3) Heap Size를 설정하실때는 OS마다 New/Old 영역의 비율이 중요한데
Windows 계열에서는 1:11~13로 설정하는 것이 좋습니다. 
예를 들어, -XX:NewSize=64m -Xms768 -Xmx768m 으로 설정할 수 있겠지요.

Heap Size도 변경하셔서 증상 확인 다시 하실 필요가 있습니다.

저희도 하이버네이트를 사용하는 패키지를 벤더가 들고 들어와서 쓰는데, 문제생기니
골치아프더군요.
제목 : Re: 스트레스 툴 쓰는법좀 알려주시면 안될까요? --;
글쓴이: 김영근(naucika) 2005/02/02 13:08:21 조회수:1268 줄수:32
현재 Web Application Stress Tool(MS) 을 사용해 볼려고 하는데요.
각페이지를 레코딩하고, 필요없는것들은 삭제를 한후에,
돌려보는데 맨 첨 페이지만 hit수가 1번 증가하고 그이후론 깜깜무소식입니다.
왜그런건지..

host : localhost
1. get /main.jsp -> 그냥 인덱스 페이집니다.
2. post /servlet/ens.common.auth.Authmgr -> 인증관련 서블릿이구요.
   QueryString 에 id/passwd 를 넣었습니다.
3. get /market/sfa/sfainfo/afc200/cust_search.jsp -> 고객검색 화면이구요.

위에서, 웹서버쪽의 콘솔내용을 보면, 2번까지는 옵니다.
인증완료되고, redirect까지 처리합니다.
3번부터는 오지않구요. 3번이후 대략 10개정도의 페이지가 등록되 있습니다.

그런데 stress tool 의 report 내용을 보면, hit가 1번에만 1번 있고,
나머지는 전부 0입니다.
에러내용도 없고, respose코드도 200으로 나오구요.
콘솔을 보면 2번이후부터는 아예 접근도 안합니다.

별다른 설정이 없는것 같은데.. 세션을 유지해야 하는 다른 설정이 있나요?
세션이 유지가 안된다면, 3번부터 접근이 안되는건 맞긴한데..

기본 셋팅내용은 default로 건드리지 않았구요.
Stress Level 을 1로 (테스트용) Test Run Time 을 1분으로 설정해놓았습니다.

이런건 첨해보는거라.. 도움을 주셨으면 감사하겠습니다. (_ _);



온세상을 자바로~ -_-!!
http://cafe.naver.com/javalove
제목 : Re: OutOfMemory 발생상황에서 Dump를 떳는데요. 해석이..
글쓴이: 손님(guest) 2005/02/03 16:14:55 조회수:1252 줄수:107
실환경에서 모니터링을 하고있는데, 어떤 오류도 아직 발생하지 않았습니다. 아직 이틀동안이지만..
덤프는 걸어놓긴 했는데, 에러가 발생하지 않으니 갑갑하네요.
전체적으로 JVM 힙메모리 차지하는 비율이 점점 증가하고 있습니다.

그래서, 일단 스트레스 테스트 툴로, 테스트를 해봤는데요.
유저 1명, 쓰레드 50, 100 으로 했습니다.
테스트 페이지는 로그인->활동검색->고객검색->기타등등 으로 히트수가 높은것 순으로 
했구요.

50개에선 별무리 없이 갔습니다만, 100개에서 드뎌 OutOfMemory가 발생되며, 덤프가
생기더군요. 그런데 이상한게 익셉션이 엄청난 양을 잡아먹고 있습니다.

아래는 덤프 내용입니다.

Requesting 74 mb of heapspace to process heapdump ...
 done.
Finding pure Roots
...................................................................... done.
DFS from pure Roots
...............................................................        done.
DFS from objects unreached from Roots (6,123)
                                                                       done.
Dump comments        : // Version: J2RE 1.4.2 IBM Windows 32 build
                       cn142-20040926
                       // Breakdown - Classes: 4865, Objects: 3911278,
                       ObjectArrays: 270052, PrimitiveArrays: 1837999
                       // EOF:  Total 'Objects',Refs(null) :
                       6024194,7241268(566154)
Dump has flags       : false
# Objects            : 6,024,194
# Refs               : 7,241,268
# Unresolved refs    : 0
Heap usage           : 534,302,592
Total object range   : 802,536,208

Pure roots           : 9,704
... which reach      : 6,018,071
Artificial roots     : 2,477
Softlinks followed   : 10

HR Memory Usage      : 200/307 mb

Enter: o{a,s,t,d,m,n}, g{c,s}, t{c,s,n}, i, p, d{t,d,m} or help for more info
> d

Enter total-size threshold [1048576]
>
Enter max depth or -ve for unlimited [8]
>
Enter 0x<addr> to dump from one address or any value for all roots [-]
>
Dumping object(s)  : 12181 roots, sorted by total-size
Filter             : total-size >= 1,048,576 and depth at most 8
Prune              : N

<0> [484,054,504] 0x11452960 [32] java/lang/ClassNotFoundException
<1>   [484,054,472] 0x1143ca78 [80] array of java/lang/Object
<2>     [484,053,952] 0x0032ba88 [304] class java/lang/ref/Finalizer
<3>       [483,920,104] 0x1443bdb8 [32] java/lang/ref/Finalizer
<4>         [483,786,368] 0x1443d6c8 [32] java/lang/ref/Finalizer
<5>           [483,652,632] 0x1443b328 [32] java/lang/ref/Finalizer
<6>             [483,518,896] 0x1443a808 [32] java/lang/ref/Finalizer
<7>               [483,385,160] 0x12ddff60 [32] java/lang/ref/Finalizer
<8>                 [483,251,424] 0x12ddf270 [32] java/lang/ref/Finalizer
                      - 4 children of 0x12ddf270 too deep.
<8>                 {483,518,896} 0x1443a808 [32] java/lang/ref/Finalizer
                    - 2 children of 0x12ddff60 filtered.
<7>               {483,652,632} 0x1443b328 [32] java/lang/ref/Finalizer
                  - 2 children of 0x1443a808 filtered.
<6>             {483,786,368} 0x1443d6c8 [32] java/lang/ref/Finalizer
                - 2 children of 0x1443b328 filtered.
<5>           {483,920,104} 0x1443bdb8 [32] java/lang/ref/Finalizer
              - 2 children of 0x1443d6c8 filtered.
            - 2 children of 0x1443bdb8 filtered.
          - 3 children of 0x0032ba88 filtered.
<2>     {484,053,952} 0x0032ba88 [304] class java/lang/ref/Finalizer
<2>     {484,053,952} 0x0032ba88 [304] class java/lang/ref/Finalizer
        - 13 children of 0x1143ca78 filtered.
      - 1 child of 0x11452960 filtered.
<0> [46,408,848] 0x03dd23b8 [304] class java/lang/Shutdown
<1>   [46,408,528] 0x10437350 [16] java/util/HashSet
<2>     [46,408,512] 0x10437320 [48] java/util/HashMap
<3>       [46,408,464] 0x104372d0 [80] array of java/util/HashMap$Entry
<4>         [46,408,240] 0x10842df8 [32] java/util/HashMap$Entry
<5>           [46,408,208] 0x10842e18 [16] java/lang/Shutdown$WrappedHook
<6>             [46,408,192] 0x101eafe0 [96] java/util/logging/LogManager$Clean
r
<7>               [46,408,032] 0x1029cb78 [56] java/lang/ThreadGroup
<8>                 [46,403,504] 0x101f76a8 [56] java/lang/ThreadGroup
                      - 4 children of 0x101f76a8 too deep.
                    - 3 children of 0x1029cb78 filtered.
<7>               <46,400,496 parent:0x101ea978> 0x101eb1c0 [160] org/jboss/mx/
oading/UnifiedClassLoader3
                  - 3 children of 0x101eafe0 filtered.
              - 1 child of 0x10842df8 filtered.
            - 3 children of 0x104372d0 filtered.
      - 1 child of 0x03dd23b8 filtered.

2/12181 roots were dumped. There were 18 objects expanded.

Enter: o{a,s,t,d,m,n}, g{c,s}, t{c,s,n}, i, p, d{t,d,m} or help for more info
>

서버로그를 찾아봐도 ClassNotFoundException 은 발생한적이 없는데,
이게 왜 이렇게 많이 잡아먹은거죠?? 위 덤프를 어찌 해석해야 하는건가요? ㅜ.ㅜ

제목 : Re: IBM JDK에서 Memory Leak의 발경 방법
글쓴이: 조대협(guest) 2005/02/04 09:41:33 조회수:1099 줄수:8
안녕하세요?
조대협입니다. 
Memory Leak의 발견과 조치가 언제나 큰 화두가 되는데요. 이원영님이 언급하신 Heap Dump에 
덧붙여서 Heap dump를 좀더 쉽게 분석할 수 있는 HeapAnalyzer라는 내용을 첨부합니다.

문서 배포는 자유입니다만, 배포시에 출처 명기 부탁드립니다.
수고하세요.

Download 자바스터디_AIX에서_Memory_Leak의_발견.doc (116736 Bytes) 자바스터디_AIX에서_Memory_Leak의_발견.doc (116736 Bytes)
제목 : Re: 그렇군요. 역시 Statement 가 나타나긴 합니다만..
글쓴이: 손님(guest) 2005/02/04 10:24:30 조회수:985 줄수:43
트리뷰 UI로 보니 정말 편하네요. 감사합니다. ^^

한눈에 들어오는것도 좋고, 따라가다 보면, 무엇으로 구성되어 있는지 잡을수 있을것 같습니다.
그래도 ClassNotFoundException은 좀 그렇네요. 자그마치 483메가를 차지하는..
ClassNotFound 따라가보니, ../trace/sql/PreparedStatemnet가 나옵니다.
적은양이긴 하지만, 이것들로 계속 구성된것이 아닌가 싶구요.

일단, Stress Test 결과, Statment 를 수정하지 않고 했을때,
동시유저수 100명에서 OOM이 발생되고, 110 명 120 명씩 늘렸을때,
계속 Reject되는 req들이 여럿 발생했습니다.

수정후에, 다시 몇번 테스트를 해보았는데요.
성능면에선 그리 나아진건 없으나, 일단 Reject되는 건수가 0였습니다.
몇번 테스트 해봤는데, 2~30명에서 2~3건 발생되는것 외엔 나머지 모두 0였죠.
OOM도 전혀 발생한적이 없었습니다. 신기하더군요.. --;

현재 운용중인 시스템은 계속 모니터링 하고있지만, 확실히 Memory Leak이였습니다.
Full GC하위로,  지속적으로 메모리가 누수되고 있었습니다.
몇몇 페이지를 수정하였지만, 이대로 계속가게되면 일주일정도 버틸수 있을것 같던데요.

어쨋든 적어도 주요 핵심 원인은 근거를 바탕으로 밝혀진것 같습니다.
그게 아닌 또다른 원인이 있더라도, 일단 이걸 전부 확실히! 수정하고
또 한 일주일 모니터링을 해봐야 겠습니다.

운영중인 서버를 대상으로 스트레스 테스트를 해보고 싶은데, 그럴순 없어 
안타깝네요.. 

이원영님한테도 정말 감사합니다. 도움이 정말 컷습니다.

%P.S
1)
스트레스 테스트 툴중 원영님이 추천하신 로드런너의 10일짜리 버젼을 받았는데,
스크립트 작성은 되나, 실제로 테스트 단계에서 라이센스가 필요하다고 하며
진행이 더이상 안됩니다. 10일 제한이라도 테스트는 되는거 아닌가요..

2)
여기 게시글 올릴때, 중간에 사진 넣는건 어떻게 하는걸까요?
태그가 먹히는걸까요? 테스트해볼려도 한번올리면 지울수가 없으니..^^;

3) 로그중에 not close pstmt같은것 말구요, 20050203/214126:W:16625 식으로
fetched 되었다고 찍히는 로그내용은 뭔가요? W이랑:수치가 뭘 의미하는지,
강제로 fetch한 내용인가요? 이게 무척 자주 찍히는데요...

Download anal0.JPG (165771 Bytes) anal0.JPG (165771 Bytes)
Download anal.JPG (168703 Bytes) anal.JPG (168703 Bytes)
제목 : Re: 열심히 소스를 수정하세요: pstmt.close()
글쓴이: 이원영(javaservice) 2005/02/04 12:19:39 조회수:1153 줄수:30
..trace/sql/PreparedStatemnet는 tracing을 위한 Wrapping클래스 이름입니다.
HeapDump의 결과가 .../trace/sql/PreparedStatemnet 들로 구성되어 있다는 것은,
PreparedStatement를 close하지 않았다는 증거가 될 수 있습니다.
또한, ...agent.log 파일에 "not close pstmt"라는 것이 곧 해당 URL, 해당 SQL과
관련된 소스코드 근방에서 pstmt.close()를 하지 않았다는 뜻입니다. SQL문장이
함께 로깅되니, 소스의 위치를 찾는 것은 어렵지 않을 것입니다.
서블렛 + JDBC 연동시 코딩 고려사항 -제1탄- 
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=968185187


PS1: 로드런너가 10일짜리, 25유저로 제한되지 않던가요?
PS2: 자바서비스넷의 이미지포함기능은 이미지를 아래에 첨부한 후에, 해당 글의 위치에서
   %% image.gif %% 와 같이 하시면  됩니다.
PS3: 20050203/214126:W:16625:/somewhere/some.jsp?a=b&c=d[3called][2fetched][select..]
 20050203/214126: 로그일자 및 시각
 W: Warning, E: Error
 16625: elapsed time(ms), 16초가 걸렸다는 것이지요.
 [3 called] 해당 요청에서 SQL이 3번 불려졌다는 것이구요
 [waiting] : ds.getConnection()에서 JDBC Connection Pool에서 가용한 연결을 기다리는 중
 [preparing] 해당 SQL을 conn.prepareStatement(...)수행중
 [prepared] 해당 SQL을 conn.prepareStatement(...)를 끝냈다는 뜻
 [executing] 해당 SQL을 현재 수행중이라는 뜻(pstmt.executeUpdte/Query())
 [4 fetching]: 현재 rs.next() 를 4번 돌고 있으며 진행중
 [10 fetched] : 현재 rs.next()를 끝냈고, 현재까지 rs.next()를 10회 돌았다는 뜻


자바서비스컨설팅 이원영
Phone: 010-6239-6498
E-mail: lwy@javaservice.com
MSN: javaservice@hanmail.net
제목 : Re: 오류수정이후 힙메모리 모니터링 결과입니다.
글쓴이: 김영근(naucika) 2005/02/17 11:04:49 조회수:1193 줄수:13
15일, 16일, 17일 3일치인데요.
이정도면, 정상적이라고 얘기할 수 있을까요?
그리고, 3일차에 갑자기 100메가 정도로 떨어져 버리네요? GC가 발생한건가요?
2일이나 지나서 GC가 이렇게 대량의 메모리를 제거하는건가요? 제가 알기론
어느정도 시스템 유휴상태가 되면, 꽤 자주 하는것으로 알고있는데. 100메가정도가
제거된 이유가 GC때문이 맞을까요?
사용자 삽입 이미지
사용자 삽입 이미지
제목 : Re: GC 그래프에 대한 의견
글쓴이: 조대협(guest) 2005/02/18 09:58:11 조회수:1252 줄수:7
전체 Heap이 1G 인가요?
거의 200~300M정도의 수준에서 머무는데.. 
제 의견으로는 아주 상당히 안정적인 것으로 보입니다.
시스템의 부하가 그리 많지 않나보네요. 100M가량 갑자기 떨어지는것은 Full GC가 일어났다고
보이는데요. GC로그에서 FullGC가 저시간즈음 일어나지 않았는지 확인해보세요.
수고하세요.
Posted by 알 수 없는 사용자
|

K사 프로젝트 중 다음과 같은 에러가 발생. 27일에는 단기적으로 발생하다가 28일에는 DB 커넥션에 거의 실패를 했음. 29일부터 31일까지 계속 커넥션이 되지 않음.

- 드라이버를 사용하여 커넥션을 연결한 경우.
java.sql.SQLException: IO 예외 상황: Broken pipe

- 톰켓의 데이터소스를 사용한 경우.
2006. 1. 31. 오전 10:39:26 org.apache.tomcat.util.threads.ThreadPool logFull
심각: All threads (150) are currently busy, waiting. Increase maxThreads (150) or check the servlet status
2006. 1. 31. 오전 10:39:36 org.apache.coyote.http11.Http11Protocol pause
정보: Pausing Coyote HTTP/1.1 on http-5010
2006. 1. 31. 오전 10:39:36 org.apache.jk.common.ChannelSocket acceptConnections
경고: Exception executing accept
java.net.SocketException: Invalid argument
 at java.net.PlainSocketImpl.socketSetOption(Native Method)
 at java.net.PlainSocketImpl.setOption(PlainSocketImpl.java:240)
 at java.net.Socket.setSoLinger(Socket.java:814)
 at org.apache.jk.common.ChannelSocket.accept(ChannelSocket.java:300)
 at org.apache.jk.common.ChannelSocket.acceptConnections(ChannelSocket.java:638)
 at org.apache.jk.common.SocketAcceptor.runIt(ChannelSocket.java:847)
 at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:684)
 at java.lang.Thread.run(Thread.java:534)
2006. 1. 31. 오전 10:39:37 org.apache.catalina.core.StandardService stop
정보: Stopping service Catalina
2006. 1. 31. 오전 10:39:37 org.apache.catalina.core.StandardHostDeployer remove
정보: Removing web application at context path /admin
2006. 1. 31. 오전 10:39:37 org.apache.catalina.logger.LoggerBase stop
정보: unregistering logger Catalina:type=Logger,path=/admin,host=localhost
2006. 1. 31. 오전 10:39:38 org.apache.catalina.core.StandardHostDeployer remove
메모리와 java 프로세스가 10~15%로 증가하다가 1%로 떨어지는 현상발생

http://javaservice.co.kr/~java/bbs/search.cgi?m=resource&b=engine&p=0&c=search&k=java.net.SocketException&o=tb&sb=engine


단지 커넥션의 문제인 줄 알았던 이슈. 로그를 보니 threadPool  이라는 메세지를 찾았다. server.xml 의 maxThread 갯수는 150 개 였는데 pop3로 메일을 가져오는 프로세스를 타는 순간 스레드갯수가 늘어난다. 스레드 갯수가 부족하여 다른 request 도 처리하지 못하고. 메일을 가져오지도 못한 상태로 멈춰버리는 현상이 발생한 것.
250 으로 늘리니 정상적으로 동작했다. 하지만 cpu 점유율이 25% 까지 증가. 메모리 증가.
정상적으로 메일을 가져온 후로는 cpu점유와 메모리 점유가 정상적으로 떨어졌다.

이후로 커넥션의 문제도 사라졌다. 커넥션 풀을 사용하는 경우. 스레드의 갯수가 부족했던 것이 원인이었던 듯.
톰켓의 데이터소스와 개별적으로 만든 커넥션 풀. 두가지를 모두 사용하고 있었으니 스레드의 갯수가 부족하게 되면서 커넥션관리가 제대로 되지 않았던 듯하다.
Broken pipe 의 경우도 네트웍의 문제라기보다는 커넥션 풀의 문제였던것이다.

실제로 jdbc로 테스트를 해본 결과 문제없이 동작했다;

'java' 카테고리의 다른 글

서블렛 + JDBC 연동시 코딩 고려사항 -제1탄-  (0) 2006.02.21
리소스 누수현상  (0) 2006.02.21
Jakarta POI - 쓰기  (0) 2005.11.30
이미지포함된 워드 파일 다운로드 시  (0) 2005.11.30
Jakarta POI - 읽기  (0) 2005.11.30
Posted by 알 수 없는 사용자
|

Jakarta POI - 쓰기

java 2005. 11. 30. 01:12

III. Formula(수식) 지원

엑셀을 읽고 쓸때 수식을 지원합니다.
org.apache.poi.hssf.usermodel.HSSFCell의 setCellFormula("formulaString") 메쏘드는 스프레드시트에 수식을 추가하는데 사용되며 getCellFormula() 메쏘드는 수식을 대표하는 문자열을 해석하는데 사용됩니다. 하지만 엑셀에서 사용하는 수식을 모두 사용 할 수는 없습니다.

 

지원되는 부분
-. 셀 참조, 시트참조, 지역참조
-. 상대적 혹은 절대적 참조
-. 수연산 및 논리연산
-. 시트 혹은 매크로 함수

-. 수식 결과값 반환

 

부분적 지원
문자열을 포함하는 수식을 해석할 수는 있지만 문자열값을 반환하는 수식은 아직 지원하지 않습니다.

지원되지 않는 부분

-. 배열 수식
-. 1진법 수식
-. 3D 참조
-. 에러 값 (cells containing #REF's or #VALUE's)


VII. 엑셀 쓰기예제

쓰기도 역시 읽기와 비슷합니다.

엑셀 워크북을 생성합니다. 행과 셀을 생성하려면 당연한 절차겠죠?

HSSFWorkbook workbook = new HSSFWorkbook();

시트를 생성합니다.

시트명을 파라미터로 바로 생성 합니다.

HSSFSheet sheet = workbook.createSheet("sheet name");

만약 한글로 시트명을 만들려면 다음과 같이 인코딩이 필요합니다.

HSSFSheet sheet = workbook.createSheet();
workbook.setSheetName( 0 , "한글" , HSSFWorkbook.ENCODING_UTF_16 );

셀에 사용할 스타일을 미리 생성해 둡니다.

HSSFCellStyle style = wb.createCellStyle();
style.setBorderBottom(HSSFCellStyle.BORDER_THIN);
style.setBottomBorderColor(HSSFColor.BLACK.index);
style.setBorderLeft(HSSFCellStyle.BORDER_THIN);
style.setLeftBorderColor(HSSFColor.GREEN.index);
style.setBorderRight(HSSFCellStyle.BORDER_THIN);
style.setRightBorderColor(HSSFColor.BLUE.index);
style.setBorderTop(HSSFCellStyle.BORDER_MEDIUM_DASHED);
style.setTopBorderColor(HSSFColor.BLACK.index);

등 여러가지 스타일을 만들 수 있습니다.

스타일은 다음 주소를 참고하세요
http://jakarta.apache.org/poi/apidocs/org/apache/poi/hssf/usermodel/HSSFCellStyle.html

로우를 하나 생성합니다.

HSSFRow row = sheet.createRow(0);

셀츨 하나 생성하여 스타일을 주고 값을 입력합니다.

HSSFCell cell = row.createCell((short)0);
cell.setCellStyle(style);
cell.setCellValue("jakarta project!");

만약 한글을 입력한다면 인코딩 해야 하며 값 세팅전에 해야 합니다.

cell.setEncoding(HSSFCell.ENCODING_UTF_16);  //한글 처리
cell.setCellStyle(style);
cell.setCellValue("자카드타 프로젝트!");

모든 셀이 다 입력되었으면 파일을 만듭니다.

FileOutputStream fs = new FileOutputStream("excelfile.xls");
workbook.write(fs);
fs.close();

VIII. 쓰기샘플 소스

<%@ page language="java" contentType="text/html;charset=euc-kr" %>
<%@ page import="java.io.*" %>
<%@ page import="org.apache.poi.poifs.dev.*" %>
<%@ page import="org.apache.poi.hssf.record.*" %>
<%@ page import="org.apache.poi.hssf.record.formula.*" %>
<%@ page import="org.apache.poi.hssf.model.*" %>
<%@ page import="org.apache.poi.hssf.usermodel.*" %>
<%@ page import="org.apache.poi.hssf.util.*" %>


<html>
<body>

<%

    String filepath = "C:\\Tomcat 5.0\\webapps\\ROOT\\write.xls";


    try {

        String[] cell_value = {"자카르타","프로젝트","www.jakartaproject.com"};


        HSSFWorkbook workbook = new HSSFWorkbook();


        HSSFSheet sheet = workbook.createSheet();
        workbook.setSheetName(0 , "한글명" ,HSSFWorkbook.ENCODING_UTF_16);


        HSSFCellStyle style = workbook.createCellStyle();
        style.setBorderBottom(HSSFCellStyle.BORDER_THIN);
        style.setBottomBorderColor(HSSFColor.BLACK.index);
        style.setBorderLeft(HSSFCellStyle.BORDER_THIN);
        style.setLeftBorderColor(HSSFColor.GREEN.index);
        style.setBorderRight(HSSFCellStyle.BORDER_THIN);
        style.setRightBorderColor(HSSFColor.BLUE.index);
        style.setBorderTop(HSSFCellStyle.BORDER_MEDIUM_DASHED);
        style.setTopBorderColor(HSSFColor.BLACK.index);           


        HSSFRow row = sheet.createRow(0);
        for (int i = 0 ; i < cell_value.length; i++){
            HSSFCell cell = row.createCell((short)i);
            cell.setEncoding(HSSFCell.ENCODING_UTF_16);
            cell.setCellStyle(style);
            cell.setCellValue(cell_value[i]);
        }
           
        FileOutputStream fs = null;
        try {
            fs = new FileOutputStream(filepath);
            workbook.write(fs);
        } catch (Exception e) {
        } finally {
            if (fs != null) fs.close();
        }
       
    } catch (Exception e) {
%>
        Error occurred:  <%= e.getMessage() %>
<%  
        e.printStackTrace();
    }   
   
%>

</body>
</html>


자 결과화면 입니다.

사용자 삽입 이미지


성공!
위의 소스를 기본으로 한다면 그리 어렵지 않을겁니다 ^^

참고로 셀병합은

HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet sheet = wb.createSheet("new sheet");

HSSFRow row = sheet.createRow((short) 1);
HSSFCell cell = row.createCell((short) 1);
cell.setCellValue("This is a test of merging");

//셀병합
//Region(int 시작row, short 시작col, int 종료row, short 종료col)
sheet.addMergedRegion(new Region(1,(short)1,1,(short)2));

FileOutputStream fileOut = new FileOutputStream("workbook.xls");
wb.write(fileOut);
fileOut.close();

와 같이하면 됩니다.

'java' 카테고리의 다른 글

리소스 누수현상  (0) 2006.02.21
java.sql.SQLException: IO 예외 상황: Broken pipe - thread  (0) 2006.01.31
이미지포함된 워드 파일 다운로드 시  (0) 2005.11.30
Jakarta POI - 읽기  (0) 2005.11.30
스케쥴러 구현  (0) 2005.11.30
Posted by 알 수 없는 사용자
|

웹상에서 입사지원서를 받아서 워드(.doc) 파일로 만드는데요.

<% 
 response.setHeader("Content-Disposition", "attachment; filename="+seat_no+".doc"); 
 response.setHeader("Content-Description", "JSP Generated Data"); 
 response.setContentType("application/vnd.ms-excel"); 
%> 

이런식으로 해서 워드문서는 만드는데 웹페이지에 있는 이미지들은 하나도 포함이 안되네요.

다음 2가지 방법으로 해결 가능 
1. 사용되는 모든 Image에 Full Path URL을 적으시면 됩니다. 즉, <a href="http://xxx.xxx.xxx.xxx./aaa.gif" target=_blank class=postlink>http://xxx.xxx.xxx.xxx./aaa.gif</a> 이런식으로.. 
2. base tag를 사용하시면 됩니다. body tag 바로 위에 <base href="URL">을 적으시면 이후의 모든 URL이 base의 URL을 기준으로 처리됩니다.

'java' 카테고리의 다른 글

java.sql.SQLException: IO 예외 상황: Broken pipe - thread  (0) 2006.01.31
Jakarta POI - 쓰기  (0) 2005.11.30
Jakarta POI - 읽기  (0) 2005.11.30
스케쥴러 구현  (0) 2005.11.30
JavaMail 을 이용하여 pop3로 메일 가져오기  (1) 2005.11.30
Posted by 알 수 없는 사용자
|

Jakarta POI - 읽기

java 2005. 11. 30. 00:12

http://poi.apache.org/index.html
http://blog.naver.com/levin01.do?Redirect=Log&logNo=100011049989


I. POI 란?

일반적으로 POI가 엑셀파일을 쓰는 컴퍼넌트로 알려져 있으나 POI는 프로젝트 이름입니다.
즉 POI는 Microsoft Format File을 액세스 할 수 있는 API를 제공합니다. (한마디로 자바에서 MS파일을 읽고 쓸수있도록 지원합니다.)

POI안에는 여러 컴퍼넌트들이 있습니다.

POIFS
Microsoft의 OLE2 포맷 형식의 문서를 자바로 읽고 쓸수 있는 컴퍼넌트입니다
기본적으로 POI의 모든 컴퍼넌트들이 POIFS를 사용합니다.
HSSF
Microsoft의 엑셀파일을 읽고 쓸수 있도록 지원하는 컴퍼넌트입니다.
HWPF
Microsoft의 워드파일을 읽고 쓸수 있도록 지원하는 컴퍼넌트입니다.
이 컴퍼넌트는 디자인 초기단계입니다.
HPSF
Microsoft의 OLE2 포맷 형식의 문서 속성을 어플리케이션에서 사용 할수 있도록 지원하는 컴퍼넌트입니다.
현재 읽기 기능만 제공합니다

워드파일을 핸들링 하는 HWPF는 초기단계라 사용을 못하지만 기대는 되는군요 ^^

ps. 영어사전을 찾아보니 poi는 하와이의 토란 요리를 뜻하더군요.
우리나라말로 하니 자카르타 토란 프로젝트 쯤 될라나? ㅎㅎ

II. 다운로드 및 설치
다운로드 받으러 갑시다~!
http://jakarta.apache.org/site/downloads/downloads_poi.cgi

현재 2.5.1버젼입니다.
다운받은 파일을 압축을 풀면 *.jar 파일들이 있을겁니다 이 파일들을 자신의 어플리케이션 /lib/에 복사합시다

POI API http://jakarta.apache.org/poi/apidocs/index.html

Quick Guide http://jakarta.apache.org/poi/hssf/quick-guide.html

III. Formula(수식) 지원에 관해..

엑셀을 읽고 쓸때 수식을 지원합니다.
org.apache.poi.hssf.usermodel.HSSFCell의 setCellFormula("formulaString") 메쏘드는 스프레드시트에 수식을 추가하는데 사용되며 getCellFormula() 메쏘드는 수식을 대표하는 문자열을 해석하는데 사용됩니다. 하지만 엑셀에서 사용하는 수식을 모두 사용 할 수는 없습니다.

지원되는 부분
-. 셀 참조, 시트참조, 지역참조
-. 상대적 혹은 절대적 참조
-. 수연산 및 논리연산
-. 시트 혹은 매크로 함수
-. 수식 결과값 반환

부분적 지원
문자열을 포함하는 수식을 해석할 수는 있지만 문자열값을 반환하는 수식은 아직 지원하지 않습니다.

지원되지 않는 부분
-. 배열 수식
-. 1진법 수식
-. 3D 참조
-. 에러 값 (cells containing #REF's or #VALUE's)

IV. 기본객체

가장 기본이되는 객체가 다음 4가지 입니다. 이름에서 무엇을 뜻하는지 대강 짐작 할 수 있겠죵?

HSSFWorkbook - 엑셀 워크북을 말합니다.
HSSFSheet - 엑셀 쉬트를 나타냅니다.
HSSFRow - 엑셀에서 특정 행입니다.
HSSFCell - 엑셀에서 특정 행에대한 특정 셀입니다

위 4가지 객체는 앞으로 계속 나올겁니다. 눈여겨 미리 봐 둡시다. @.@

V. 엑셀 읽기 예제

POSFS을 이용하여 엑셀 워크북을 생성합니다.

POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream("excelfile.xls"));
HSSFWorkbook workbook = new HSSFWorkbook(fs);

생성된 워크북을 이용하여 시트 수만큼 돌면서 엑셀 시트 하나씩을 생성합니다.

int sheetNum = workbook.getNumberOfSheets();

for (int k = 0; k < sheetNum; k++) {
   System.out.println("Sheet Number : "+k);

   System.out.println(Sheet Name : " + workbook.getSheetName(k));
   HSSFSheet sheet = workbook.getSheetAt(k);

}

생성된 시트를 이용하여 그 행의 수만큼 돌면서 행을 하나씩 생성합니다.

int rows = sheet.getPhysicalNumberOfRows();

for (int r = 0; r < rows; r++) {
   HSSFRow row   = sheet.getRow(r);

   System.out.println("Row : "+row.getRowNum());

}

역시나 생성된 행을 이용하여 그 셀의 수만큼 돌면서 셀을 하나씩 생성합니다.

int cells = row.getPhysicalNumberOfCells();

for (short c = 0; c < cells; c++) {              <--!! short 형입니다. 255개가 max!
    HSSFCell cell  = row.getCell(c);

    int celltype = cell.getCellType();

    ...

}

셀을 생성하여 셀 타입에 따라 처리를 해주면 끝~

주의사항

만약 엑셀에서 A열에 아무런 값이 없으면 그 행은 읽지 못합니다.
행을 읽지 못하니 셀또한 처리 할 수 없습니다

VI. 엑셀읽기 샘플소스

샘플 데이터

사용자 삽입 이미지

A열은 B열에 대한 셀 타입을 나타내며 C열은 D열에대한 셀 타입을 나타냅니다.
즉 B:1 의 123456의 셀 타입은 A:1 일반 이라는 것이며 마찬가지로
D:1의 2005-02-09의 셀타입은 C:1 사용자정의로 세팅하였다는 겁니다

이 엑셀의 데이터를 다음 소스로 읽어 보겠습니다.

<%@ page
language="java"
contentType="text/html;charset=euc-kr"
import="java.io.*,
 org.apache.poi.poifs.filesystem.POIFSFileSystem,
 org.apache.poi.hssf.record.*,
 org.apache.poi.hssf.model.*,
 org.apache.poi.hssf.usermodel.*,
 org.apache.poi.hssf.util.*" %>


<html>
<head><title>Read example</title></head>
<body>

<%

  String excelfile = "C:\\Tomcat 5.0\\webapps\\ROOT\\example.xls";

  try {
       POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(excelfile));


       //워크북을 생성!               

       HSSFWorkbook workbook = new HSSFWorkbook(fs);

       int sheetNum = workbook.getNumberOfSheets();


       for (int k = 0; k < sheetNum; k++) {


            //시트 이름과 시트번호를 추출
%>

            <br><br>
            Sheet Number <%= k %> <br>
            Sheet Name <%= workbook.getSheetName(k) %><br>
<%
            HSSFSheet sheet = workbook.getSheetAt(k);
            int rows = sheet.getPhysicalNumberOfRows();


            for (int r = 0; r < rows; r++) {


                // 시트에 대한 행을 하나씩 추출
                HSSFRow row   = sheet.getRow(r);
                if (row != null) {
                     int cells = row.getPhysicalNumberOfCells();
%>
                     ROW  <%= row.getRowNum() %> <%=cells%></b><br>
<%

                     for (short c = 0; c < cells; c++) {


                         // 행에대한 셀을 하나씩 추출하여 셀 타입에 따라 처리
                         HSSFCell cell  = row.getCell(c);
                         if (cell != null) {
                              String value = null;

                              switch (cell.getCellType()) {

                                   case HSSFCell.CELL_TYPE_FORMULA :
                                       value = "FORMULA value=" + cell.getCellFormula();
                                        break;
                                   case HSSFCell.CELL_TYPE_NUMERIC :
                                       value = "NUMERIC value=" + cell.getNumericCellValue(); //double
                                       break;
                                  case HSSFCell.CELL_TYPE_STRING :
                                       value = "STRING value=" + cell.getStringCellValue(); //String
                                       break;
                                  case HSSFCell.CELL_TYPE_BLANK :
                                      value = null;
                                     break;
                                 case HSSFCell.CELL_TYPE_BOOLEAN :
                                     value = "BOOLEAN value=" + cell.getBooleanCellValue(); //boolean
                                    break;
                                case HSSFCell.CELL_TYPE_ERROR :
                                     value = "ERROR value=" + cell.getErrorCellValue(); // byte
                                     break;
                                default :
                             }
%>        
                          <%= "CELL col=" + cell.getCellNum() + " VALUE=" + value %> <br>
<%
                        }
                    }
                }
            }
       }
   } catch (Exception e) {
%>
       Error occurred:  <%= e.getMessage() %>
<%  
       e.printStackTrace();
    }

%>


</body>
</html>

위 소스의 결과입니다.

Sheet Number 0
Sheet Name 한글
ROW 0 4
CELL col=0 VALUE=STRING value=일반
CELL col=1 VALUE=NUMERIC value=123456.0
CELL col=2 VALUE=STRING value=사용자정의
CELL col=3 VALUE=NUMERIC value=38392.0
ROW 1 4
CELL col=0 VALUE=STRING value=숫자
CELL col=1 VALUE=NUMERIC value=123456.0
CELL col=2 VALUE=STRING value=날짜 (yy-m-d h:mm)
CELL col=3 VALUE=NUMERIC value=38393.0
ROW 2 4
CELL col=0 VALUE=STRING value=통화
CELL col=1 VALUE=NUMERIC value=123456.0
CELL col=2 VALUE=STRING value=날짜 (yy年 mm月 dd日)
CELL col=3 VALUE=NUMERIC value=38394.0
ROW 3 4
CELL col=0 VALUE=STRING value=텍스트
CELL col=1 VALUE=NUMERIC value=123456.0
CELL col=2 VALUE=STRING value=날짜 (yyyy년 mm월 dd일)
CELL col=3 VALUE=NUMERIC value=38395.0


결과를 보니 사용자가 지정한 셀 타입에 관계없이 숫자관련 셀은 POI에서 모두 숫자 타입으로 인식해 버렸습니다.날짜 역시 지정한 셀 타입에 관계없이 모두 숫자 타입으로 인식해 버리는군요!
그럼 어떻게 날짜를 제대로 표현할까요?
날짜 타입을 제대로 나타내기 위해서는 날짜 Cell에는 getDateCellValue()를 사용하면 정상적으로 처리 할 수 있습니다.

SimpleDateformat sdf = new SimpleDateformat("yyyy-MM-dd hh:mm");
String date = sdf.format(cell.getDateCellValue());

등을 이용하면 나타내고자 하는 알짜를 표현 하기 더 쉽겠지요 나머지 수식을 가져 올때도 마찬가지입니다. 이런 사항을 도표로 나타내보았습니다.

org.apache.poi.hssf.usermodel.HSSFCell 에는 모두 6가지의 Cell Type이 있는데, cell.getCellType()을 하면 그 셀의 반환값을 알 수 있으며 그에 상응하는 static 필드타입은 다음과 같습니다.

셀타입 필드타입

함수

함수반환값
0 CELL_TYPE_NUMERIC

getNumericCellValue()
-> 숫자 타입일때
getDateCellValue()
-> 날짜 타입일때

double

Date

1 CELL_TYPE_STRING

getStringCellValue()

String
2 CELL_TYPE_FORMULA

getCellFormula()
-> 수식자체를 가져올때
getNumericCellValue()
-> 수식 반환값이 숫자일때
getStringCellValue()
-> 수식 반환값이 문자일때

String

double

String

3 CELL_TYPE_BLANK

4 CELL_TYPE_BOOLEAN

getBooleanCellValue()

boolean
5 CELL_TYPE_ERROR

getErrorCellvalue()

byte

'java' 카테고리의 다른 글

Jakarta POI - 쓰기  (0) 2005.11.30
이미지포함된 워드 파일 다운로드 시  (0) 2005.11.30
스케쥴러 구현  (0) 2005.11.30
JavaMail 을 이용하여 pop3로 메일 가져오기  (1) 2005.11.30
정규식  (0) 2005.11.30
Posted by 알 수 없는 사용자
|

스케쥴러 구현

java 2005. 11. 30. 00:03

http://okjsp.pe.kr/bbs?act=VIEW&seq=62106&bbs=bbs3&keyfield=content&keyword=timer&pg=

간단한 Sample을 보여드립니다.
프로퍼티 파일을 이용하면 동적으로 Class를 Load할 수 있죠.
또한 Quartz Sample을 보시면 Crontime까지 실행 도중에 변경가능합니다. 또한 같은 스케쥴 시간을 갖고 있는 Job은 여러개 동시에 등록 가능합니다.
Quartz의 Sample중 InterruptableJobTest을 보시면 실행중인 Job을 찾아서 Delete 시키는 방법도 있으므로, 이런 기능들을 잘 섞어서 쓰시면 가능할 것 같은데요.
-------------------
import java.text.ParseException;
 
import org.apache.log4j.Logger;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
 
import com.util.Config;
 
/**
 * project.properties에 설정된 내용을 이용하여 정해진 시간마다 등록된 Thread를 실행하기 위한 Main Program
 */
 
/**
 * 
 */
public class QuartzSample {
    static Logger logger = Logger.getLogger( Np2RawDataSyncTimerMain.class );
 
    public static void main(String [] args)
    {
       // Sample.properties 를 읽어 원하는 내용을 찾아주는 Class
        Config cf = new Config();
 
        // TaskGroups을 구한다.
        // White space로 구분한다.
        String [] taskGroups = cf.getConfig( "TaskGroups" ).split( "\\s" );
 
        SchedulerFactory schFact = null;
        Scheduler scheduler = null;
 
        JobDetail jobDetail = null;
        CronTrigger trigger = null;
        String className = null;
 
        try
        {
            // Schedule 처리를 위한 SchedulerFactory 생성
            schFact = new StdSchedulerFactory();
            scheduler = schFact.getScheduler();
            scheduler.start();
 
            for ( int i = 0; i < taskGroups.length; i++ )
            {
                String scheduleTime = cf.getConfig( taskGroups[i] + ".ScheduleTime" );
                String [] tackName = cf.getConfig( taskGroups[i] + ".TaskNames" ).split( "\\s" );
 
                for ( int j = 0; j < tackName.length; j++ )
                {
                    Class c = null;
                    className = cf.getConfig( tackName[j] + ".ClassName" );
 
                    logger.debug( tackName[j] + ".ClassName : " + className );
 
                    // 실행될 Class를 Load한다.
                    c = Class.forName( className );
 
                    jobDetail = new JobDetail( tackName[j], taskGroups[i], c );
 
                    trigger = new CronTrigger( tackName[j], taskGroups[i] );
                    try
                    {
                        trigger.setCronExpression( scheduleTime );
                    }
                    catch ( ParseException e2 )
                    {
                        logger.error( "CronTrigger ParseException : " + tackName[j] + ".ClassName : " + className );
                        logger.error( "CronTrigger ParseException : " + scheduleTime );
                        e2.printStackTrace();
                    }
 
                    // Config에서 읽은 공통 사항을 Job에 전달하기 위해 저장한다.
                    jobDetail.getJobDataMap().put( "web.driver", webDriver );
                    jobDetail.getJobDataMap().put( "web_db.url", webURL );
                    jobDetail.getJobDataMap().put( "web_db.username", username );
                    jobDetail.getJobDataMap().put( "web_db.password", password );
 
                    try
                    {
                        scheduler.scheduleJob( jobDetail, trigger );
                    }
                    catch ( SchedulerException e1 )
                    {
                        logger.error( "SchedulerException : " + tackName[j] + ".ClassName : " + className );
                        e1.printStackTrace();
                    }
                }
            } // for
 
        }
        catch ( ClassNotFoundException e )
        {
            logger.error( "ClassNotFoundException : " + className );
            e.printStackTrace();
        }
        catch ( SchedulerException e )
        {
            logger.error( "SchedulerException : Scheduler could not execute!!" );
            e.printStackTrace();
        }
 
    }
 
}
---------------------------
Sample.properties 파일의 설정내용
 
TaskGroups = RawDataSyncGroup
 
#
# RowDataSync Group 설정
# 매일 5분 마다 실행
RawDataSyncGroup.ScheduleTime = 0 0/5 * * * ?
RawDataSyncGroup.TaskNames = RawDataSync_1 RawDataSync_2
 
# 실행될 Task의 Full Package 이름을 설정
RawDataSync_1.ClassName = com.kt.np2.biz.sync.RawDataSync_1
RawDataSync_2.ClassName = com.kt.np2.biz.sync.RawDataSync_2

'java' 카테고리의 다른 글

이미지포함된 워드 파일 다운로드 시  (0) 2005.11.30
Jakarta POI - 읽기  (0) 2005.11.30
JavaMail 을 이용하여 pop3로 메일 가져오기  (1) 2005.11.30
정규식  (0) 2005.11.30
DB를 이용한 채번  (0) 2005.11.28
Posted by 알 수 없는 사용자
|