달력

4

« 2024/4 »

  • 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
2010. 6. 26. 16:26

[java] annotation 사용 Enjoy/JAVA2010. 6. 26. 16:26



JDK 1.5 부터 annotation 을 사용할 수 있도록 되었습니다. annotation 은 영어 사전을 찾아보면

주석 이라도 되어있군요.

 

자바에서 주석은

 

// 의  한줄 주석과, /*    */  의 여러줄 주석 그리고 /**    */ 의 java document 를 만들 수 있는 주석이 있습니다.

 

그러니까 이러한 주석과 번역은 주석 이라고 되더라도 annotation 은 다른것 입니다.^^

위의 것들은 comment 라고 하나요??

 

하여튼 먼저 annotation 의 사용법을 보면

 

@Annotation  이렇게 사용합니다. 앞에 @ 마크가 붙습니다.

 

예전에 document 주석내에서 많이 보던것과 유사하죠.

 

/**

  @author 저자명

*/

 

이런식의 주석이 있었습니다.

 

annotation 은 프로그램 코드의 관점에서 보면 그냥 설명 입니다. 즉 코드에 수행에 영향을 미치치 않습니다.

annotation 은 프로그램의 metadata 를 기술한다고 하는데 이는 프로그램에 대한 부연설명, 덧붙이는말

이 되겠습니다. annotation 은 compiler 나 외부 툴들에게 프로그램에 대한 부가적인 설명을 제공하는데 사용됩니다.

 

사용예를 보겠습니다.

 

프로그램을 작성하고 컴파일하면 deprecated 메소드라는 경고문구가 나오는 경우가 있습니다.

이는 사용하려고 하는 메소드가 앞으로 더 이상 지원되지 않으므로 사용하지 말라는 내용입니다.

물론 이런 경고가 나오는 시점에서는 사용할 수 는 있지만 언제 없어질지 모르는 경우죠.

반드시 대체할 수 있는 메소드나 방법을 찾아보아야 겠습니다.

 

그럼 이제 자신이 만든 메소드가 오래되어서 새로운 버전이 만들어지고 이것을 사용하는 사람들에게

구버전은 사용하지 말도록 deprecate 메세지를 컴파일시 보이고 싶을때 어떡할까요..

 

이때 @Deprecated annotation을 사용하면 됩니다

 

/**

  @deprecated 더이상 지원되지 않는 메소드 입니다.

 */

@Deprecated

public void myOldMethod() {

}

 

이렇게 선언을 하면 컴파일시 위의 메소드를 사용할때 경고를 보여줍니다. 주석안의 @deprecated 와 차이점을

@Deprecated 처럼 대문자로 시작한다는 것입니다.

 

또 다른 하나를 보겠습니다.

 

클래스를 상속 받아서 상위클래스의 메소드중하나를 오버라이드 하는경우 입니다.

 

// 상위 클래스

public class Parent {

  public void parentMethod() {

  }

}

 

// 하위 클래스

public class Child extends Parent {

  public void parentmethod() {

  }

}

 

이처럼 상위의 parentMethod 를 오버라이드할 경우 parentmethod로 소문자로 적어서 오버라이드가 되는게 아니라

새로운 메소드가 되어버리는 실수를 할 수 가 있습니다.

이럴경우 @Override  annotation 을 사용할 수 있습니다.

 

@Override

pubilc void paranemethod() {

}

 

이렇게 정의를 하면 컴파일시 parentmethod 가 상위 메소드를 오버라이드 하는것이라고 컴파일러에게 알리게 됩니다.

하지만 대소문자를 잘못적었기 때문에 오버라이드가 아니므로 컴파일러가 오류를 보여주게 됩니다.

 

이처럼 annotation 은 컴파일러 같은 외부툴들에게 프로그램의 부가적인 정보를 알려주기 위해서 사용됩니다.

또다를 사용처는 EJB3.0 에서는 빈의 특성을 컨테이너에게 알리는데 annotation을 사용합니다.

전에는 빈의 특성을 xml 파일로 작성을 해야만 했습니다.

:
Posted by 라면스프





플러그인 개발 기초 학습, Part 2: 리치 클라이언트 애플리케이션 도입하기








플러그인 개발 기초 학습 연재는 순전히 플러그인 개발에 관한 것이다. 그러나 본격적으로 시작하기 전에, 플러그인 개발을 할 수 있는 적당한 환경을 갖추었는지 확인할 필요가 있다. 먼저 할 일은 Eclipse.org에서 PDE(Plug-in Development Environment)를 갖춘 이클립스 배포판을 다운로드하는 것이다. 이클립스 클래식 최신판 다운로드를 권장한다. 이 연재에서는 이클립스 V3.4(M5) 마일스톤 배포판을 사용할 것이다. 이 작업만 완료하면, 시작할 준비가 끝났다(참고자료에서 이클립스를 어디서 찾을 수 있는지 익히고, 만약 이클립스를 처음 접한다면 추가적인 배경 지식을 얻길 바란다).

플러그인 개발을 쉽게 이해하기 위해 본 기사는 그림 1에 있는 작업 흐름도를 따를 것이다. Part 1에서 작업 흐름도의 앞선 다섯 가지 단계를 살펴보았다. 여기서는, 나머지 두 개의 단계를 살펴보고 리치 클라이언트 애플리케이션 소개를 중점적으로 설명하겠다.


그림 1. 플러그인 개발 작업 흐름도
플러그인 개발 작업 흐름도 

빌드하기

빌드 내용 설정은 플러그인 개발 세상에서 매우 중요한 과정이다. 이클립스에서 모든 플러그인 개발 빌드 관련 설정은build.properties 파일에 들어간다.


그림 2. 빌드 설정(build.properties)
빌드 설정(build.properties) 

자동화된 빌드?

플러그인을 위한 자동화된 빌드 설정은 본 기사의 주제 밖이다. 하지만 자주 물어오는 질문이기 때문에 몇 가지 배경지식을 말하고자 한다. 자동화된 빌드를 셋업하는 가장 일반적인 방법은 이클립스 SDK에서 PDE Build 구성요소를 이용하는 것이다. PDE 빌드의 단점은 컴파일로, 입문자들의 기세를 꺾는다. 자동화된 빌드를 설정하는 방법을 참조하는 빠른 방법은 Pluginbuilder 웹 사이트에 있는 플러그인과 도구들을 살펴보는 것이다(참고자료 참조).

샘플 빌드 설정 내용 중앙에 보면 MANIFEST.MF, plugin.xml과 아이콘 파일들이 있다. 또한 거기에는 국제화 지원을 위한 plugins.properties파일과 라이선스 파일 같은 것들이 존재할 수 있다. binary buildsource build가 각각 가지고 있는 것들의 차이를 아는 것이 중요하다. 일반적으로, 이클립스에서 플러그인을 내보낼 때는, 여러분의 친구들의 이클립스 환경에서 사용할 수 있는 바이너리 플러그인으로 내보낸다. 소스 빌드는 플러그인에 포함된 소스 파일들까지 포함하고 있다. 일반 내보내기 옵션에서 소스 빌드를 하도록 선택할 수 있다.

내보내기

플러그인 개발자의 작업 흐름도에서 마지막 단계는 생성한 플러그인을 내보내는 것이다. 이클립스 PDE는 특화된 내보내기 마법사를 통해 이 과정을 매우 간단하게 처리하도록 돕는다. 이 마법사를 사용하려면(그림 3 참조), 간단하게 File > Export를 클릭하고 Plug-in Development 카테고리에 있는 Deployable Plug-ins and Fragments를 선택한다.


그림 3. 플러그인 내보내기 마법사
플러그인 내보내기 마법사 

이 마법사에서 제일 먼저 선택해야 할 옵션은 내보내려는 플러그인이 무엇인가이다. 여기서는 간단하게 예제인 HelloWorld 플러그인이 될 것이다. 다음 과정은 플러그인을 내보낼 목적지다. 우린 선택적으로 플러그인을 ZIP 파일이나 디렉터리로 내보낼 수 있다. 이 마법사에 있는 다른 옵션들은 플러그인 사인과 플러그인에 있는 소스 빌드와 관련된 것이다. 지금은, 이것들은 일단 무시하고 간단하게 마법사에 있는 Finish를 클릭하여 플러그인을 내보낸다(그림 4 참조). 플러그인 manifest 편집기 화면에 있는 Overview 페이지에 보면 마법사를 실행할 수 있는 하이퍼링크가 있는 것을 눈여겨 봐두자.


그림 4. 디스크로 내보낸 플러그인
디스크로 내보낸 플러그인 

잘했다! 지금까지 살펴본 모든 것이 플러그인을 이클립스 워크스페이스에서 하드 디스크로 사용가능한 형태로 내보내는 과정이다. 이런 형태가 되면, 해당 플러그인을 아주 쉽게 친구나 동료 들이 사용할 수 있다. 이것으로 플러그인 개발 작업 흐름도와 관련된 내용은 끝났고, 이제 이클립스에서 리치 클라이언트 애플리케이션을 만드는 방법을 살펴보자.

리치 클라이언트 플랫폼(RCP)

리치 클라이언트 애플리케이션은 이클립스에서 RCP(Rich Client Platform)를 사용하여 만들 수 있다. 전통적으로, 이클립스 플랫폼은 공개 도구 플랫폼으로 설계되었다. 하지만 그렇게 설계되었기 때문에 그 컴포넌트를 어떤 다른 클라이언트 애플리케이션을 만들 때에도 사용할 수 있다. 리치 클라이언트 애플리케이션을 만들기 위해 필요한 최소한의 플러그인 집합을 RCP라고 한다. 좀 더 많은 내용을 알고 싶다면 참조자료를 참조하라.

제품

이클립스 내에서 리치 클라이언트 애플리케이션을 만들려면, 제품 설정(product configuration)이라는 개념을 이해해야 한다. 제품 설정은 플러그인 개발자가 리치 클라이언트 애플리케이션을 만들 수 있도록 PDE에서 제공한다. 제품 설정을 이해하려면, 그걸 사용할 샘플 리치 클라이언트 애플리케이션이 필요하다. Part 1에서 살펴봤던 PDE 템플릿 메커니즘의 장점을 활용하여 하나를 만들 것이다. 플러그인 프로젝트 이름은 rcp로 하고 그것이 리치 클라이언트 애플리케이션임을 확인시켜주고, RCP Application with a view 템플릿을 선택한다(그림 5 참조).


그림 5. 리치 클라이언트 애플리케이션 템플릿
리치 클라이언트 애플리케이션 템플릿 

다음 단계는 제품 설정 파일을 만들어 우리가 앞에서 만든 리치 클라이언트 애플리케이션을 구성할 때 사용할 것이다. 새로운 제품 설정을 만들려면, 플러그인 프로젝트에서 마우스 오른쪽 버튼을 클릭하고 New > Product Configuration을 선택하여 새로운 제품 설정 마법사를 실행한다(그림 6 참조). 모든 기본값을 그대로 놔두고, 제품 설정 파일 이름을 rcp.product라고 한 다음 Finish를 클릭하여 제품 설정 편집기를 실행한다. 다음 절에서 제품 설정 편집기의 여러 페이지를 살펴보자.


그림 6. 새로운 제품 설정 마법사
새로운 제품 설정 마법사 

Overview 탭

플러그인 manifest 편집기와 비슷하게, 제품 설정 편집기의 첫 번째 페이지는 Overview 탭이다(그림 7 참조). 제품 설정, 테스트와 제품을 내보내기 위한 링크, 그리고 제품이 플러그인을 기반으로 했는지 기능(features)을 기반으로 했는지 선택하는 화면으로 구성되어 있다. 애플리케이션을 빠르게 테스트해 보려면, Testing 섹션에 있는 Launch an Eclipse application을 선택하고 리치 클라이언트 애플리케이션이 어떻게 생겼는지 확인해보자.


그림 7. Overview
Overview 탭 화면 

이것이 제품 설정 편집기에서 제공하는 Overview와 관련된 모든 것이다. 제품 설정에서 할 수 있는 기본 동작들에 관심이 있다면, Overview를 다시 살펴보기 바란다.

애플리케이션 폼을 여기서 실행하는 것도 가능하다, Launch an Eclipse Application을 클릭하면 애플리케이션이 팝업으로 뜨는 것을 확인할 수 있다(그림 8 참조).


그림 8. 애플리케이션 실행하기
애플리케이션 실행하기 

Configuration 탭

Configuration 탭은 제품을 실행할 때 사용할 기본 구성요소들을 가지고 있다. 여기에는 제품을 실행할 때 필요한 파일들에 대한 정보가 포함되어 있다. 첫 번째 섹션인 플러그인과 프래그먼트(Fragment, 역자 주: 다른 번들에 속하여 동작하는 번들)에는 제품을 실행할 때 필요한 플러그인과 프래그먼트들이 간단하게 나열되어 있다. 도움이 되는 팁을 주자면, 제품을 만들고 있을 때는 새로운 의존성들을 추가하게 될 것이다. 이럴 때, Add Required Plug-in을 선택하여 제품 설정에 필요한 모든 것을 편하게 추가할 수 있다.

다음 섹션은, 설정 파일로 OSGi에 특화된 구성물로 config.ini 파일이라는 것을 나타낸다. 약 99.9% 가량 이 설정을 그냥 놔두고 이클립스가 생성한 파일을 사용할 것이다. 이 파일에 있는 모든 것은 이미 앞선 플러그인과 프래그먼트 섹션에서 기술한 것이지만, 이런 특별한 형식으로만 이클립스가 이해하고 실행할 수 있다.


그림 9. 설정
설정 

Launching

Launching 탭은 이클립스 기반 제품 실행을 다루는 모든 정보를 담고 있다. Java Runtime Environment 섹션은 플랫폼에 특화된 JRE를 빌드할 때 편리하게 사용할 수 있다. Program Launcher 섹션은 제품을 실행하는 것과 관련된 설정을 할 수 있다. 예를 들어, 보통 개발자들은 eclipse.exe라는 이름 말고 다른 이름으로 실행 파일을 지칭하길 원할 것이다. 실행 파일 이름 위에, 플랫폼에 특화된 아이콘을 사용하여 나타낼 수도 있다. Launching Arguments 섹션에 제품이 필요로 하는 플랫폼에 특화된 매개변수들을 설정할 수 있다. 이것은 Mac OS X과 같이 특정 운영체제에 따라 특화된 동작이 필요할 때 유용하게 사용할 수 있다.


그림 10. 실행
실행 

Splash 탭

Splash 탭을 사용하여 제품과 관련된 스플래시 화면을 부가적으로 설정할 수 있다(그림 11 참조). 예를 들어, 이클립스를 실행하면, 이클립스를 보여주고 플러그인 로딩 과정을 보여주는 간단한 스플래시 화면을 볼 수 있다. 기본적으로, 예제 RCP 애플리케이션은 간단한 비트맵 기반 스플래시 화면 템플릿을 포함하고 있다. 스플래시 화면과 관련된 재미난 것을 시연해 보기 위해, 로그인 스플래시 화면 템플릿(설정 섹션에 있음)을 활용해보자. 로그인 템플릿을 선택한 다음, 제품 설정을 저장하고 애플리케이션을 실행하자.


그림 11. 스플래시
스플래시 

새로운 스플래시 화면과 로그인 그리고 패스워드 창을 확인하라.


그림 12. 커스텀 스플래시 화면
커스텀 스플래시 화면 

Branding 탭

Branding 탭(그림 13 참조)에서는 세 가지 일을 할 수 있다. 창 이미지 수정, 커스텀 about 창 만들기, 부가적인 환영 페이지 만들기다. 창 이미지란 간단하게 애플리케이션 셸 창과 연관된 이미지들을 말한다. 예를 들어, 이클립스를 실행하고, 타이틀 바를 보면 16x16 크기의 이클립스 아이콘을 볼 수 있다. 설정할 수 있는 아이콘 종류가 몇 가지 있다. 대부분의 소프트웨어 애플리케이션은 라이선스, 개발자, 버전 정보가 담긴 about 창을 가지고 있다. 제품 설정 편집기는 이미 만들어둔 이클립스 about 창을 재활용할 수 있게 해준다. 물론 여러분의 이미지와 정보로 변경할 수 있다.

환영 페이지는 사용자가 애플리케이션을 실행하도록 돕는 역할을 할 것이다. 예를 들어, 기본 이클립스 환영 페이지를 보려면, Help > Welcome 메뉴를 선택하면 된다. 만약 여러분의 애플리케이션 용도로 그와 같은 것을 제공하고자 한다면, Branding 탭의 Welcome Page 섹션에서 할 수 있다. 환영 페이지를 작성하는 것은 본 기사의 주제에서 벗어나기 때문에, 여기에 대해 더 자세한 내용을 알고 싶다면, 참고자료에서 이클립스 사용자 도움 기술에 관해 많은 정보를 찾아보길 바란다.


그림 13. 브랜딩
브랜딩 

결론

이 모든 것을 통틀어, 플러그인 개발 기초 학습 연재의 목표는 베스트 프랙티스가 녹아있는 플러그인 개발 기본에 대해 소개하는 것이었다. Part 1에서 예제 플러그인을 만들고 플러그인 개발 작업 흐름도를 살펴보았다. Part 2에서는 플러그인 개발 작업 흐름도를 마쳤고 리치 클라이언트 애플리케이션을 생성했다. 작업 흐름도를 마치고 나면, 플러그인과 이클립스 RCP 기반 애플리케이션을 개발하는 것이 훨씬 수월해질 것이다.

이제, 계속해서 새로 알게 된 지식을 사용하여 플러그인과 이클립스 기반 애플리케이션을 만들어 보라.


참고자료

교육

제품 및 기술 얻기

토론

  • Eclipse Platform newsgroups는 이클립스와 관련하여 논의할 것이 있을 때 가장 먼저 들려야 할 곳이다(이 링크를 선택하면 기본 유즈넷 뉴스 리더 애플리케이션이 실행되고 eclipse.platform을 열 것이다).

  • Eclipse newsgroups에는 이클립스를 사용하고 확장하는 데 관심있는 사람들을 위한 다양한 참고자료가 있다.

  • developerWorks 블로그와 developerWorks 커뮤니티에 참여하라.


:
Posted by 라면스프




플러그인 개발 기초 학습, Part 1: 기본 요소


요약: 이클립스에서 플러그인 개발은 예술과도 같은 것입니다. 플러그인 개념, 특히 OSGi와 이클립스에 대해 낯설다면, 이클립스를 사용하여 플러그인을 만들 수 있도록 제공하는 수 많은 도구를 익히는 것이 다소 어렵게 느껴질 수도 있습니다. 본 기사의 목적은 좋은 평가를 받고 있는 베스트 프랙티스와 함께 기본적인 플러그인 개발 기술을 익힐 수 있도록 하는 것입니다.





플러그인 개발 101 연재는 순전히 플러그인 개발에 관한 것이다. 그러나 본격적으로 시작하기 전에, 플러그인 개발을 할 수 있는 적당한 환경을 갖추었는지 확인할 필요가 있다. 먼저 할 일은 Eclipse.org에서 플러그인 개발 환경(Plug-in Development Environment: PDE)을 갖춘 이클립스 배포판을 다운로드하는 것이다. 이클립스 클래식 최신 버전 다운로드를 권장한다. 이 연재에서는 이클립스 V3.4(M5) 마일스톤 배포판을 사용할 것이다. 이 작업만 완료하면, 시작할 준비가 끝난 것이다(참고자료에서 이클립스를 어디서 찾을 수 있는지 익히고 만약 이클립스를 처음 접한다면 추가적인 배경 지식을 얻길 바란다).

플러그인 개발을 쉽게 이해하기 위해, 본 기사는 그림 1에 있는 작업 흐름도를 따를 것이다. 본 기사의 Part 1에서는 작업 흐름도 중 처음 다섯 개 과정을 살펴볼 것이다. 나머지는 Part 2에서 살펴볼 것이고 리치 클라이언트 애플리케이션(rich-client application)을 집중적으로 다룰 것이다.


그림 1. 플러그인 개발 작업 흐름도
플러그인 개발 작업 흐름도 

OSGi는 무엇인가?

V3.0에서 이클립스는 이전 버전에서 사용하던 못마땅한 플러그인 기술 기반을 OSGi로 교체하는 큰 도약을 거두었다. OSGi Alliance는 OSGi 기술을 책입지는 독립적인 비영리 단체이고 기본적으로 이클립스 재단과 비슷하다. OSGi Alliance는 OSGi 기술을 설명하는 표준 문서 작성을 책임지고 있다. 간략하게 OSGi 기술은 애플리케이션 개발 용도의 서비스 지향적인 플러그인 기반 플랫폼을 제공한다. 다양한 구현체들이 이 표준을 기반으로 하고 있다. 가장 유명한 구현체 중 하나가 Equinox이며 이것이 이클립스에서 OSGi 스펙을 기반으로 구현한 구현체다(참고자료에서 자세한 배경 정보를 참조하라).

플러그인 생성을 자세히 살펴보기 전에, 정확하게 플러그인이 무엇인지 논의해보자. 기술적으로, 플러그인은 독립적으로 실행 가능하며(self-contained) 자기 서술적인(self-describing) JAR(Java™ Archive)다. 플러그인을 실행하는 데 필요한 코드와 리소스를 묶고 있기 때문에 독립적으로 실행할 수 있다. 자신이 무엇이며, 세상으로부터 무엇을 필요로 하는지, 그리고 세상에 무엇을 기여할지에 대한 정보를 가지고 있기 때문에 자기 서술적이다. 플러그인 내부에서 보통 MANIFEST.MF와 plugin.xml 두 개의 서술 파일을 볼 수 있다.

만들기부터 시작하자

플러그인 개발 작업 흐름도의 첫 번째 부분은 플러그인 프로젝트 만들기다. 이클립스에서, New >Project... 메뉴 옵션을 사용하여 쉽게 이 작업을 할 수 있다. 마법사가 보이면, 생성하고자 하는 프로젝트 타입에Plug-in Project를 선택한다.



그림 2. 새 플러그인 프로젝트 마법사
새 플러그인 프로젝트 마법사 

다른 이클립스 프로제트와 마찬가지로, 마법사는 프로젝트 이름을 입력하라고 요구한다. helloworld라고 하겠다. 그리고 타깃 플랫폼을 선택할 수 있는 옵션이 있다. 해당 문맥에서 타깃 플랫폼이란 단순하게 특정 이클립스 버전 용도인지 Equinox와 같은 OSGi 플랫폼 용도인지를 의미한다. 이 경우, 간단하게 이클립스 3.3 버전을 타깃으로 했다. 새 플러그인 프로젝트 마법사 다음 페이지는 플러그인 내용에 대한 것이다.


그림 3. 플러그인 내용
플러그인 내용 

우리가 만들 플러그인을 이클립스에서 사용할 수 있게 하려면, 플러그인 내용 마법사 페이지에서 반드시 폼을 작성해야 한다. 플러그인 식별자는 여러분의 플러그인을 식별할 수 있는 유일한 값을 타나내는 필드다. 다른 어떤 플러그인도 같은 식별자를 사용할 수 없다. 플러그인 버전은 네 부분으로 나뉘어 있다(세 개의 숫자와 문자열 하나). 각각은 major.minor.service.qualifier로 구성된다(참고자료에서 이크립스 플러그인 버전 정하기 가이드를 참조하라). 플러그인 이름은 사람들이 읽기 편한 간단한 이름을 준다. 플러그인 제공자 필드는 플러그인 작성자에 대한 정보를 사람들이 읽기 편한 형태로 기입한다. 간략하게, 실행 환경(EE) 필드는 여러분의 번들을 실행할 수 있는 최소한의 JRE를 명시한다(참고자료).

마법사는 플러그인 활성자(activator)를 설정할 수 있는 옵션을 제공한다. 플러그인 활성자는 단순한 클래스로 플러그인 생명 주기를 제어한다(start-and-stop 메서드처럼 생각하라). 보통, 활성자를 사용하여 기본 설정을 하고 플러그인이 더 이상 필요 없어질 때 자원을 반납하는 용도로 사용한다. 여기서는, 활성자를 사용하여 플러그인이 사용할 UI를 만들도록 하여, RCP(Rich-Client Platform) 애플리케이션을 만들 것이다(참고자료 참조).

플러그인 생성 마지막 단계는 새 플러그인이 기반으로 할 템플릿(그림 4 참조)을 선택하는 것이다. 플러그인 생성에 익숙하다면 이 단계는 보통 생략할 것이다. 하지만, 처음 접하는 사람들은 어느 정도 시작점이 필요하다. 따라서 이클립스 SDK는 다양한 템플릿을 제공하여 쉽게 시작할 수 있도록 하고 있다. 여기서는, 기본 Hello World with a view 애플리케이션을 선택한다.


그림 4. 플러그인 템플릿
플러그인 템플릿 

수정

MANIFEST.MF 내부

MANIFEST.MF 파일 내부에서 사용할 수 있는 헤더들이 궁금하다면, OSGi Alliance에서 가용한 OSGi 명세서를 참조하라(참고자료 참조).

앞선 단계를 마친 다음, 새로운 플러그인이 워크스페이스에 추가됐을 것이다. 그리고 현재 새로 만든 플러그인을 다룰 때 도움을 주는 편집기(그림 5 참조)가 보일 것이다. 편집기의 첫 번째 페이지는 개요(Overview) 페이지다. 이 페이지에는 수정 가능한 플러그인 식별자를 포함하여 플러그인과 관련된 여러 가지 편집 가능한 목록을 가지고 있다. 예를 들어, 이전에 정의했던 식별자(ID)와 버전 필드 그리고 그림 4에서 살펴봤던 템플릿이 있다.


그림 5. 플러그인 편집기(개요 페이지)
플러그인 편집기(개요 페이지) 

확장 기능과 확장 지점

다양한 확장 지점

이클립스 V3.3에는 200개가 넘는 확장 지점을 이클립스 SDK가 제공한다. SDK에서 무엇이 확장 가능한지(다른 말로 하자면, 확장 지점) 목록을 보고 싶다면, 이클립스 재단의 문서 "Platform Extension Points"를 확인하라(참고자료참조).

플러그인 탐험하기 다음 단계는 확장 기능을 추가하는 것을 살펴보겠다. 하지만 자세히 살펴보기 전에, 먼저 확장 가능한 이클립스 플러그인과 관련된 두 가지 중요한 개념을 살펴보자. 간단하게 비유하자면, 확장 기능(extension)은 플러그로 확장 지점(extension point)은 소켓으로 생각할 수 있다. 각각의 확장 지점은 유일하고 적용하기 위해 필요한 조건을 정의하고 있다. 예를 들어, 이클립스는 org.eclipse.ui.editors 확장 지점(소켓)에 우리가 만든 편집기(플러그)를 끼워넣고 사용할 수 있다.

여기서는, 이클립스 도구 모음에 추가할 새로운 확장 기능을 만들 것이다. 그러기 위해 org.eclipse.ui.actionSets 확장 지점에 해당하는 템플릿(앞에서 플러그인을 만들 때 사용한 템플릿과 비슷한 개념)을 사용할 것이다. 플러그인 편집기의 개요 페이지에서 Extension/Extension Point Content 섹션에 있는 Extensions 링크를 선택한다. Add...를 클릭하고 기본값이 채워진 상태로 org.eclipse.ui.actionSets 템플릿(그림 6 참조)을 선택한다.


그림 6. 확장 기능 템플릿
확장 기능 템플릿 

런타임

플러그인에서 중요한 부분으로 자기 서술을 하는 부분이다. 플러그인이 반드시 서술해야 하는 것 중 하나는 외부에 제공하느냐다. 이클립스 문맥에서 이를 공개한 패키지들(exported packages)이라고 한다. 우리의 플러그인에서, 다른 플러그인들이 우리 플러그인 내부에 의존하는 패키지들을 참조할 수 있도록 다른 플러그인들에게 어떤 패키지들을 공개할지 결정해야 한다. 또한 공개한 패키지들을 내부에서만 사용할 것으로 설정할 수도 있는데, 이것은 플러그인 개발자들에게 해당 패키지들은 API로 생각하고 있지 않다고 말하는 것과 같다. 공개한 패키지들을 설정하려면, manifest 편집기에 있는 런타임(Runtime) 페이지를 이용한다.


그림 7. 런타임 페이지
런타임 페이지 

의존성

다양한 플러그인

이클립스 생태계에는 수많은 플러그인들이 있어 찾다가 필요한 것을 헤매기 쉽다. 플러그인을 찾을 때 유용한 두 개의 사이트를 소개하고자 한다. 하나는 이클립스 재단의 프로젝트 목록이고, 두 번째는 이클립스 플러그인 센터(Eclipse Plug-in Central)다(참고자료 참조).

앞선 절에서, 플러그인이 반드시 자신의 기능을 세상에 알리기 위해 해야 하는 일을 설명했다. 자, 이제 다른 플러그인에서 앞에서 말한 기능을 사용하고 싶다고 상상해보자. 해당 기능을 사용하고 싶다는 것을 어떻게 표현할 것인가? 이클립스 문맥에서는 의존성(dependencies)으로 표현한다. 가장 간단한 경우, 플러그인들이 다른 플러그인에 의존할 수 있다. 예를 들어, 만약 여러분의 편집기를 이클립스에서 사용하고 싶다면,org.eclipse.ui.editors 플러그인에 의존해야 할 것이다. 의존성을 기술하려면, 플러그인 편집기의 의존성(Dependencies) 페이지를 사용해야 한다.


그림 8. 의존성 페이지
의존성 페이지 

다른 플러그인에 의존할 때 알아야 할 것은, 플러그인들이 공개한 패키지들에만 선택할 수 있다는 것이다(의존성 페이지의 불러올 패키지(Imported Packages) 부분을 참조하라). 이 부분은 더 어려운 주제인데 여러분의 플러그인이 특정 구현체에 묶이는 것을 원하지 않을 때 유용하다. 예를 들어, XML 파서가 제공하는 com.company.xml.parser 패키지에 의존한다고 생각해보자. 이 때 각기 다른 환경에서 사용할 수 있는 두 개의 플러그인 com.company.xml.parser.mobile과 com.company.xml.parser.desktop 이라는 동일한 XML 파서의 서로 다른 구현체를 머리속에 떠올려보자.

소스 편집기

런타임과 의존성 페이지는 MANIFEST.MF 파일에 기술되어 있는 중요 정보를 가시화해 보여준 것이다. 이 파일 소스를 직접 편집하고자 하는 고급 사용자들을 위해, PDE는 소스 편집기를 제공한다. 소스 편집기는 플러그인 manifest 정의에 사용하는 다양한 헤더에 코드 완성 기능을 제공한다.


그림 9. 소스 편집하기
소스 편집하기 

테스트와 디버깅

플러그인 탐험 다음 단계는 테스트와 디버깅이다. 플러그인 테스트 용도로, 이클립스와 PDE는 자기 호스팅(self-hosting)이라는 개념을 가지고 있다. 자기 호스팅은 간단하게 현재 작업하는 플러그인을 탑재한 새로운 이클립스를 실행하는 것을 말한다. 물론 이 과정에서 실제로는 어떤 플러그인도 공개하거나 배포하지 않는다. 새로운 이클립스를 실행하려면, 개요 페이지에서 테스팅(Testing) 부분(개요 페이지에 있는 Launch an Eclipse application 참조)에서 새로운 런타임 워크벤치를 시작하면 된다. 몇 초 후에, 여러분의 플러그인 샘플 액션이 도구 모음에 추가된 채 새로운 워크벤치 팝업이 뜨는 걸 볼 수 있을 것이다.


그림 10. 이클립스의 자기 호스팅
이클립스의 자기 호스팅 

테스트 주도 플러그인 개발

실제 플러그인을 개발하기 전에 테스트부터 작성하는 것에 관심이 있다면, 테스트만 있는 플러그인을 작성하는 것이 가능하다. 플러그인에 포함되어 테스트를 실행할 수 있는 Plug-in JUnit Test라는 특별한 실행 설정이 존재한다.

디버그 모드에서 자기 호스팅 상태의 플러그인을 실행하려면, 개요 페이지의 테스팅 섹션에서 Launch an Eclipse application in debug mode 링크를 클릭하면 된다. 실제로 여러분의 플러그인을 디버깅하려면, 관련된 중단점을 설정할 필요가 있다. 이클립스에서 디버깅을 처음 해본다면, developerWorks 기사 "Debugging with the Eclipse Platform"을 추천한다. 이 문서가 처음해보는 독자들에게 도움이 될 것이다(참고자료 참조).

플러그인 실행이나 디버깅에 대해 더 상세한 옵션을 설정하려면, 실행 설정(Launch Configuration) 창으로 이동한다. 이 창은 Run > Run Configurations... 메뉴 옵션을 선택하면 볼 수 있다. 여러분의 플러그인과 관련 있는 실행 설정 타입은 "Eclipse Application"이다. 우리가 실제 실행하고 싶은 것이 이클립스 애플리케이션이기 때문이다(그림 11 참조). 실행 설정에서, 여러분은 인자와 같은 것들을 설정할 수 있으며 애플리케이션을 실행할 때 사용할 JRE를 제어할 수 있다.


그림 11. 실행 설정 창
실행 설정 창 

문자열 외부화하기

플러그인 개발에서 가장 흔히 하는 작업이 국제화다. 여러분의 플러그인이 유용하고 많은 사람이 사용하면, 여러분의 플러그인을 여러 언어 환경에서 동작할 수 있도록 해달라는 요청을 받게 될 것이다. 고맙게도, 플러그인과 관련된 문자열을 외부화할 때 필요한 상당량의 작업이 최소화되었다. PDE 안에서 플러그인 개요 페이지에서 마우스 오른쪽 버튼을 클릭하고 Externalize Strings... 메뉴 아이템을 선택하면 마법사를 볼 수 있다. 선택하면, 외부화와 관련된 모든 문자열을 보여주는 화면을 볼 수 있다.


그림 12. Externalize Strings 마법사
Externalize Strings 마법사 

실제로, 이것은 plugin.properties 파일(외부로 빼낸 문자열들을 담고 있다)을 플러그인 내부에 생성하고 MANIFEST.MF 파일에Bundle-Localization 헤더를 추가한다. Bundle-Localization 헤더를 사용하여 간단하게 외부화한 문자열들의 이름과 부가적인 위치를 설정할 수 있다. 여기서는, Bundle-Localization에 "plugin,"이라고 설정했는데, 이것은 다양한 지역의 문자열이 plugin.properties(그림 13 참조), plugin_fr.properties, plugin_de.properties 같은 파일에 존재할 수 있다는 것을 뜻한다. 파일 이름에서 밑줄 다음에 오는 문자는 지역 정보를 나타낸다.


그림 13. plugin.properties 파일
plugin.properties 파일 

이게 다다! 플러그인과 관련된 국제화와 관련된 모든 것을 살펴봤다. 좀 더 많은 정보를 원한다면, 다소 오래되긴 했지만 여전히 유효한 이클립스 코너의 기사 "How to Internationalize your Eclipse Plug-in" 과 "How to Test Your Internationalized Eclipse Plug-In"을 추천한다(참고자료 참조).

manifest 정리하기

우리 여행의 다음 목적지는 manifest 파일들(MENIFEST.MF와 plugin.xml)을 몇 가지 베스트 프랙티스로 정리하는 것이다. PDE는 개요 페이지의 Exporting 섹션을 통해 그와 관련된 편리한 마법사를 제공한다. 마법사가 뜨면(그림 14 참조), 선택할 수 있는 다양한 옵션이 보일 것이다. 기본값들이 보통 타당하지만, plugin.properties 파일에 키값이 없는지 확인하는 것과 같은 특정 옵션들이 있을 수 있는데 이것들이 매우 유용하다.


그림 14. Optimize Manifest 마법사
Optimize Manifest 마법사 

결론

전체적으로, 본 기사의 미션은 몇몇 베스트프랙티스를 기반으로 플러그인 개발 기본 요소를 소개하는 것이다. 우리는 샘플 플러그인을 만들고 일반적인 플러그인 개발 작업 흐름도를 통해 이를 달성했다. 작업 흐름도를 다 익히고 나면, 플러그인을 개발하는 것이 더 쉬워질 것이고 Organize Manifest 마법사와 같이 베스트프랙티스를 사용하여 유지보수 할만큼 익숙해질 것이다. Part 2에서는 리치 클라이언트 애플리케이션 개발을 할 때 이용할 수 있는 도구 사용법과 그림 1에서 살펴보았던 플러그인 개발 작업 흐름도 나머지 부분들을 끝낼 것이다.


참고자료

교육

제품 및 기술 얻기

토론

  • Eclipse Platform newsgroups는 이클립스와 관련하여 논의할 것이 있을 때 가장 먼저 들려야 할 곳이다(이 링크를 선택하면 여러분의 기본 유즈넷 뉴스 리더 애플리케이션이 실행되고 eclipse.platform을 열 것이다).

  • Eclipse newsgroups는 이클립스를 사용하고 확장하는 데 관심있는 사람들을 위한 다양한 참고자료가 있다.

  • developerWorks 블로그에 가입하고 developerWorks 커뮤니티에 참여하라.



Part 2 보기

:
Posted by 라면스프
2010. 6. 24. 15:05

eclipse plugin Enjoy/JAVA2010. 6. 24. 15:05

* Eclipseutil-refresh 플러그인

https://eclipseutilplugins.dev.java.net/

자주쓰는 CVS 커밋,업데이트,싱크 메뉴를 단축아이콘으로 등록하여 편리하다.

 

 

 

* Copy Fully Qualified Class Name 플러그인

http://www.jave.de/eclipse/copyfully/index.html

클래스이름을 패키지명까지 포함해서 쓸일이 많다면 필수적인 플러그인이다.

 

 

 

* GotoFile 플러그인

http://www.muermann.org/gotofile/

프로젝트에 속한 파일을 빨리 찾을 수 있다. 파일명의 일부만 치면 바로바로 목록이 나온다.

 

 

* Windows Context Menu 플러그인

http://www.geocities.com/richard_hoefter/ContextMenuPlugin/

이클립스에서 윈도우 컨텍스트메뉴(오른클릭 메뉴)를 쓸 수 있다.

 

 

* Call Hierarchy 플러그인

http://eclipse-tools.sourceforge.net/call-hierarchy/

메소드 호출관계를 트리로 보여준다.

 

스크린샷 http://eclipse-tools.sourceforge.net/call-hierarchy/images/screenshot1.png

 

 

* Commonclipse 플러그인

http://commonclipse.sourceforge.net/

toString() 메소드 등을 자동으로 만들어준다. 자바빈의 내용을 로그에 찍고 싶을 때 좋다.

 

 

 

* Properties Editor 플러그인

http://propedit.sourceforge.jp/index_en.html

properties 파일 편집할 때 그냥 일반 텍스트 편집하듯 편집하면된다.native2ascii 같은거 몰라도 된다.

 

 

 

 

* AnyEdit tools 플러그인

http://andrei.gmxhome.de/anyedit/index.html

텍스트 에디터 기능이 약한 이클립스에 몇가지 기능을 추가해준다. 나는 주로 Open type(file) under cursor 메뉴를 사용한다.

 

step1

 

 

* Log4E 플러그인

http://log4e.jayefem.de/index.php/Main_Page

소스에 log4j 구문을 삽입해준다. 코딩 무지 편해졌다. -.-;

 

 

* Web Service Console 플러그인

http://wscep.sourceforge.net/

웹서비스를 개발한다면 꼭 있어야한다. 요청/응답 SOAP 메시지를 직접 눈으로 보면서 서비스를 테스트할 수 있다.

스크린샷 : http://wscep.sourceforge.net/images/s5.GIF

:
Posted by 라면스프





왜 Apache HTTP Server와 Tomcat을 연동하는가?

Apache Tomcat(이하 Tomcat)은 아마도 가장 널리 알려진 JSP / Servlet Engine일 것입니다. JSP / Servlet 명세에 대한 참조 구현물(Referencial Implementation, 흔히 줄여서 RI라고도 하죠) 역할도 하고 있으며, 실 업무에서도 꽤 씁니다(왕년에 Naver Blog를 쓸 때 한번은 Java Exception Trace log가 Web Browser에 표시된 것을 본 적이 있었습니다. 그 덕에 Naver Blog가 Tomcat을 쓴다는 것을 알았죠).

 

그러나 실전에서는 web server와 tomcat 같은 JSP/Servlet Engine 둘을 연동해서 쓰지, tomcat만 쓰지는 않습니다. 그 이유는 아래와 같습니다.

성능 상의 이유

JSP/Servlet Engine 상에서 동작하는 Servlet이나 JSP라는, (동일한 HTTP Request를 보내도 매 번 그 결과가 다를 수 있는) 동적인 HTTP Response를 생성해 보내는 것들입니다. 그에 비해 단순한 HTML 문서, image, CSS(Cascading Style Sheet), Javascript file은 거의 변할 일는 (동일한 HTTP Request를 보내면 그 결과가 늘 같은) 정적 contents입니다. 이런 정적 contents는 통상적으로 Web Server가 JSP/Servlet Engine보다 더 빠르게 service합니다.

 

감 이 오시나요? 결국 정적 content는 정적 contents 처리에 강한 Web Server가 처리하고, 동적 contents는 JSP/Servlet Engine이 맡는 것입니다. 일단 HTTP Request를 Web Server가 먼저 받아보고 정적 contents를 요구하면 자신이 처리하고, 동적 contents request라면 이 request를 JSP/Servlet Engine에게 넘기죠. 아니면 특정 URL pattern이 들어오면 JSP/Servlet Engine으로 넘기는 방법도 있습니다. 결국 이렇게 하면 부수적으로 동적 contents 생성하느라 바쁜 JSP/Servlet Engine에 부하를 덜 줄 수 있고 정적 contents service하느라 귀중한 JVM Heap을 아낄 수 있는 이점도 누릴 수 있습니다.

보안 상의 이유

보 통 JSP/Servlet Engine은 중요한 업무 절차에 대한 구현을 가지고 있게 마련입니다. 단순한 image, CSS 같은 정적 contents를 담은 Web Server보다 더 안전해야 보호해야 한다는 뜻이 됩니다. 보통 어떤 기업 같은 조직의 network를 구성할 경우 외부 network와 조직의 network를 방화벽(firewall)으로 단절시키고 방화벽에 규칙(rule)을 등록, IP packet이 제한적으로만 그 방화벽을 넘다들 수 있도록 합니다. 안전성이 더 높아야 하는 server들이 있는 network zone는 여기에 한 번 더 방화벽을 칩니다. 그리고 이 두 방화벽 사이의 network zone을 DMZ라고 합니다.

 

이렇게 Web Server와 JSP/Servlet Engine을 따로따로 쓰면 DMZ에 덜 중요한 data를 가진 web server를 놓고 이중 방화벽 뒤에 있는 network zone에 JSP/Servlet Engine을 놓음으로써 높은 보안성을 획득하면서도 외부에 동적 contents를 제공할 수 있게 됩니다.

가용성 상의 이유

web server도 하나, JSP/Servlet Engine도 하나라면 둘 중 하나만 죽으면 정상적인 service가 불가능합니다. 따라서 절대 멈추면 안되는 service(세계화가 되면서 이런 요구 사항은 더 늘었습니다. 지구 상 어딘가는 늘 업무 시간이니까요)를 담당하는 web server, JSP/Servlet Engine은 두 개 이상을 가동하는 이중화를 적용합니다.

 

web server가 어떤 HTTP Request를 받아 이를 JSP/Servlet Engine으로 넘기려 할 때 그 JSP/Servlet Engine이 이중화가 되어 있다면 web server는 이 request를 좀 한가한 JSP/Servlet Engine에 넘긴다던지 할 수 있습니다(물론 실제로는 간단하게는 round robin부터 시작해서 이에 대한 여러가지 방법이 있습니다). 내지는 JSP/Servlet Engine 중 하나가 비정상적으로 종료한 상태이면 현재 살아 있는 JSP/Servlet Engine에게 이 request 처리를 위임, 전반적인 service 중단을 막을 수 있습니다.

 

준비물

  • Apache Tomcat

    • Apache Tomcat : Apache Tomcat 대표 site. Tomcat을 내려받으려면 가야 하는 곳.
  • Apache HTTP Server

    • Apache HTTP Server : Apache HTTP Server 대표 site.
    • Apache Lounge : Windows용 Apache HTTP Server 정보가 풍부함. Windows용 Apache HTTP Server는 여기에서 받을 것을 추천!
  • mod_jk

    • Apache Tomcat : Apache Tomcat 대표 site. mod_jk도 여기서 받을 수 있습니다(이 site에서는 mod_jk를 'Apache Connector'라고 부릅니다).

 

작업 절차

 

간단한 구성

간단한 구성이란, 개발이나 가벼운 Web Service 운영을 목적으로 하나의 Apache HTTP Server + 하나의 Tomcat 연동 구성을 하는 것입니다. 이 작업에 대한 구체적 작업 절차는 [tomcat]apache, tomcat 연동하기 글에서 잘 설명하고 있습니다(Windows / Linux에서 mod_jk로 Apache와 Tomcat 연동하는 것을 proxy_module과 같이 설치할 경우와 아닌 경우 모두 설명하고 있습니다).

 

고가용성을 위한 구성

Naver Blog와 같이 사용자가 많다거나 장애 등을 감내할 수 있는(Fault Tolerant) Web Service를 구성하려면 위 간단한 구성으로는 어렵고 여러 개의 Apache HTTP Server + 여러 개의 Tomcat 연동 구성을 해야 합니다. 이렇게 해야 일부 Apache HTTP Server나 Tomcat이 죽어도 나머지들이 request를 처리할 수 있지요. 이러한 특성을 고가용성(High Availability)이라 하는데 이 고가용성은 위와 같은 단순한 설정만으로는 얻기 어렵습니다. 이런 고가용성 설정은 '아파치와 톰캣을 활용한 대용량 웹서비스 운영'이란 글에서 잘 설명하고 있습니다.

 

참고 문헌

 

예시

  • Apache HTTPD 2.2 설정 file : mod_jk로 Apache HTTP Server 2.2와 Tomcat 6을 연동시킨 결과로 나온 Apache Web Server 2.2 설정 file.
  • Apache Tomcat 6 설정 file : mod_jk로 Apache HTTP Server 2.2와 Tomcat 6을 연동시킨 결과로 나온 Apache Tomcat 6 설정 file.
  • mod_jk.conf : 실제 mod_jk 설정 정보를 담은 mod_jk 설정 file.

출처 : http://ryudaewan.springnote.com/pages/610881
:
Posted by 라면스프

원문 : http://www.ibm.com/developerworks/kr/library/opendw/20061017/?ca=drs-kr

난이도 : 초급 
2006년 10월 17일


웹 개발자에게 있어 톰캣은 JSP를 배우거나 간단한 테스트를 하는 정도의 웹 컨테이너로 생각하는 경우가 많다. 하지만 근래 들어 기업 및 대형 포탈에서 상용 서비스를 위한 웹 컨테이너로서 톰캣을 선택해, 성공적으로 적용한 사례들이 늘고 있다. 톰캣에서 안정적인 웹 서비스를 제공하기 위해서 지원하는 기능은 5가지가 있다. 아파치 웹서버와 연동, 로드밸런싱, 세션 클러스터링, 데이터베이스 처리, 모니터링 및 관리 등이 그것이다. 
이 문서에서는 로드밸런싱과 세션 클러스터링 위주로 설명을 할 것이며, 다음에 기회가 된다면 다른 부분에 대해서도 자세히 알아보도록 하겠다.

아파치 웹 서버와 톰캣의 연동

일반적으로 정적인 페이지를 서비스할 때는 웹서버가 훨씬 더 좋은 성능을 발휘한다. 또한 이렇게 역할 분담을 함으로 톰캣에 가중되는 부하를 줄여주는 효과도 얻을 수 있다. 아파치웹서버와 톰캣을 연동하는 것을 일반적으로 ‘커넥터(Connector)'라고 부르며, 여기에는 WARP 커넥터, JK 커넥터 그리고 JK2 커넥터가 있다. 이중에서 WARP와 JK2는 공식 커넥터에서 제외되었고 현재 남아 있는 것은 JK 커넥터뿐이다. 그럼 먼저 JK 커넥터를 이용해서 아파치 웹서버와 톰캣을 연동해 보도록 하겠다. 
아파치 웹사이트에서 바이너리 혹은 소스 코드를 다운로드 받도록 하자. 유닉스 혹은 리눅스는 mod_jk.so이며 윈도우용은 mod_jk.dll이다. 이 파일을 아파치 웹서버의 modules 디렉토리에 저장한다(주의, 아파치 웹서버를 컴파일해서 사용하는 경우는 컴파일시에 DSO 기능이 가능하도록 설정해줘야 한다). 저장을 한 후에 아파치 웹서버에 해당 모듈을 인식시켜야 하며 아파치 웹서버의 httpd.conf 파일에 다음 내용을 추가하도록 하자.


리스트 1. httpd.conf
        ...
LoadModule jk_module modules/mod_jk.so    # 모듈 추가
JkWorkersFile "conf/workers.properties"   # JK 설정 파일 위치 및 이름
 
JkLogFile "logs/mod_jk.log"               # JK에 대한 로그 파일 위치
JkLogLevel info                           # 로그 레벨 지정
JkLogStampFormat "[%a %b %d %H:%M:%S %Y]"   # 로그 시간 포맷 지정
JkRequestLogFormat "%w %V %T"             # 로그 내용 포맷
JkMount /* loadbalancer                   # URL 링크 -> 모든 요청을 톰캣으로 지정
JkMount /servlet/* loadbalancer           # URL 링크 -> servlet 요청을 톰캣으로 지정
...

위와 같은 설정을 하게 되면 아파치 웹서버로 들어온 모든 요청을 톰캣으로 재전송 하게 된다. 만일 JSP와 서블릿만 톰캣에서 서비스를 하고 나머지는 아파치 웹서버에서 서비스 하고자 한다면 다음과 같이 수정하면 된다.

  
JkMount /*.jsp loadbalancer                # URL 링크 -> *.jsp 요청을 톰캣으로 지정 
JkMount /servlet/* loadbalancer           # URL 링크 -> servlet 요청을 톰캣으로 지정 

httpd.conf에는 위의 내용이 전부이다. 그럼 이제 workers.properties 파일을 작성해 보도록 하겠다. 이 파일이 실제 로드밸런싱을 위한 설정이 되겠다.




위로


라운드 로빈 방식의 로드밸런싱 설정

톰캣에서 제공하는 로드밸런싱은 정확히 톰캣 자체에서 제공하는 것이 아니라 아파치 웹서버와 연동되는 커넥터에 의해서 제공된다(로드밸런싱은 JK, JK2 커넥터에서만 제공된다). 현재는 라운드 로빈(Round Robin) 방식만이 제공되며 로드밸런싱에 대한 설정은 workers.properties 파일에서 정의하게 된다.

리스트 2. workers.properties 
  
worker.list=tomcat1, tomcat2, loadbalancer
 
worker.tomcat1.type=ajp13
worker.tomcat1.host=localhost
worker.tomcat1.port=11009
worker.tomcat1.lbfactor=100
 
worker.tomcat2.type=ajp13
worker.tomcat2.host=localhost
worker.tomcat2.port=12009
worker.tomcat2.lbfactor=200
 
worker.loadbalancer.type=lb
worker.loadbalancer.balanced_workers=tomcat1,tomcat2

worker라는 개념은 톰캣의 프로세스로 보면 된다. 즉 worker를 설정하는 구성 요소는 JK 커넥터를 연결하는 방식(JK는 ajp13을 이용한다), 톰캣이 실행되어 있는 IP 혹은 도메인, ajp13 서비스 포트, 그리고 작업 할당량이다. 여기서 주의 깊게 볼 것이 작업 할당량인데 로드밸런싱 시에 lbfactor라는 작업량의 비율을 보고 라운드 로빈 방식의 서비스를 제공하게 된다. 여기서는 tomcat1과 tomcat2를 1대 2의 비율로 작업량을 할당한 것이다. 
그럼 이제 남은 작업은 2개의 톰캣 프로세스를 실행시키는 것이다. 톰캣 프로세스를 여러 개 띄우는 방법은 2가지가 있다.

  • 톰캣을 2개 설치해서 기동시킨다. 이때 포트 충돌을 피하기 위해 서버 포트, AJP13과 HTTP 1.1 커넥터 포트 2개를 충돌되지 않게 재정의 한다.
  • 하나의 톰캣에 2개의 서비스를 정의하고 톰캣을 기동시킨다. 이때 AJP13과 HTTP1.1 커텍터 포트 2개를 충돌되지 않게 재정의 한다.

먼저 2개의 바이너리를 설치했다고 가정하면 각각의 톰캣은 다음과 같은 형태의 server.xml 파일로 적용해 준다.

리스트 3. server.xml 
<Server port="11005" shutdown="SHUTDOWN"> <!-- 톰캣 프로세스를 관리하는 포트 --> <Service name="Catalina"> <Connector port="11080"/> <!-- 아파치를 통하지 않고 직접 접속하고자 할때의 포트 --> <Connector port="11009" protocol="AJP/1.3"/> <!-- 아파치와 연동하기 위한 포트 --> <!-- jvmRoute 명 JK 커넥터에서 톰캣 프로세스를 구분하는데 사용. 프로세스 별로 다르게 적용해야 함 --> <Engine jvmRoute="tomcat1" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> </Server>


리스트 4. server.xml 
<Server port="12005" shutdown="SHUTDOWN"> <!-- 톰캣 프로세스를 관리하는 포트 --> <Service name="Catalina"> <Connector port="12080"/> <!-- 아파치를 통하지 않고 직접 접속하고자 할때의 포트 --> <Connector port="12009" protocol="AJP/1.3"/> <!-- 아파치와 연동하기 위한 포트 --> <!-- jvmRoute 명 JK 커넥터에서 톰캣 프로세스를 구분하는데 사용. 프로세스 별로 다르게 적용해야 함 --> <Engine jvmRoute="tomcat2" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> </Server>

리스트 3은 tomcat1의 환경 설정이고 리스트 4는 tomcat2의 환경 설정이다. 두 환경 설정의 차이는 3개의 포트번호와 <Engine> 태그의 jvmRoute 속성이다. <Server> 태그의 포트는 톰캣을 종료 시키는 작업을 할 때 사용하는 것이며 <Connector> 태그의 포트들은 아파치를 통하지 않고 직접 톰캣에 접속할 경우와 아파치와 연계하여 JK 커넥터와 연동할 때 사용하는 포트이다. 마지막으로 <Engine> 태그는 JK 커넥 터에서 로드밸런싱을 수행할 때 해당 값을 구분자로 활용하게 되는데 이 값을 반드시 다른 톰캣 프로세스와 다른 이름으로 지정해야 한다. 지금까지의 환경 설정은 하나의 아파치 웹서버와 두 개의 톰캣 간의 연계를 위한 것이며 톰캣은 동일한 하드웨어 장비에 설치되어 있다는 가정하에 적용한 것이다. 만일 각각의 톰캣이 서로 다른 하드웨어에 존재한다면 jvmRoute명만 다르게 하고 포트명은 동일해도 상관이 없다. 하지만 만일 하나의 장비에 4개의 톰캣 프로세스를 실행시키고 로드밸런싱을 하려고 한다면 어떻게 될까? 톰캣을 4번 설치하고 각각의 환경 설정 파일을 수정해 주어야 할까? 만일 필요한 환경 설정 내용이 변경된다면(예를 들어 JNDI Resource 정보) 모두 운영자가 환경 설정 파일을 수정해 주어야 할까? 다행히도 톰캣에서는 하나의 바이너리에 여러 개의 프로세스가 뜨도록 할 수 있다. 톰캣의 server.xml 태그는 다음과 같은 구조를 가지고 있다.
<Server> --> <Service> --> <Engine> --> <Host> --> <Context>

여기서 Server 태그는 유일해야 하며 Server 태그 밑에는 여러 개의 <Service> 태그가 올 수 있다. 여기서 말하는 <Service> 태그가 바로 하나의 톰캣 프로세스가 된다. 만일 2개의 <Service> 태그를 정의했다면 2개의 프로세스가 구동되는 것이다. 리스트 5는 이렇게 구현한 환경 설정 파일이다. 리스트 5. server.xml 
<Server port="8005" shutdown="SHUTDOWN"> <!-- Service 1 --> <Service name="Catalina1"> <Connector port="11080"/> <Connector port="11009" protocol="AJP/1.3"/> <Engine jvmRoute="tomcat1" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> <!-- Service 1 --> <Service name="Catalina2"> <Connector port="12080"/> <Connector port="12009" protocol="AJP/1.3"/> <Engine jvmRoute="tomcat2" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> </Server>

리스트 5는 하나의 톰캣 바이너리를 통해 2개의 프로세스를 실행시키는 것이다. 이렇게 하면 환경 설정의 편리성을 가져올 수 있지만 특정 서비스만 실행하거나 종료 시키는 것은 아직 지원되지 않는다. 즉 모든 서비스가 동시에 실행되거나 혹은 동시에 종료되는 것을 의미한다. 이런 점을 잘 판단해서 두 가지 형태의 환경 설정 중 하나를 선택하면 되겠다. 
지금까지는 로드밸런싱에 대해 알아보았다. 위의 환경설정을 가지고 테스트를 하다 보면 한가지 문제가 발생한다. 예를 들어 어떤 사용자가 tomcat1을 이용해서 쇼핑몰 서비스를 받고 있다가 tomcat1이 비정상 종료를 하게 되었다. 이때 사용자가 웹 페이지를 요청하게 되면 아파치 웹서버는 tomcat1이 종료된 것을 인지하고 그 이후부터 서비스를 tomcat2로 요청하게 된다. 하지만 tomcat1에 저장되어 있던 쇼핑바구니 정보 즉 세션 정보는 사라진 상태다. 즉, 서비스는 유지되지만 사용자는 다시 이유도 모르게 처음부터 쇼핑 항목들을 등록해야 하는 문제를 가지게 된다. 이제부터는 이런 문제를 해결할 수 있는 톰캣 프로세스 간의 세션 정보 공유에 대해서 알아보겠다.




위로


세션 클러스터링 설정

클러스터링은 톰캣 5.x 버전부터 지원이 되고 있지만 아직은 초기 단계이고 세션 클러스터링만이 제공되고 있는 수준이다. 기능이 많이 부족하긴 하지만 로드밸런싱과 더불어 사용할 경우에는 좀 더 안정적인 서비스를 제공할 수 있다. 작업을 해주어야 할 것은 다음과 같다.

  • server.xml에 <Cluster> 태그 정의
  • 웹 어플리케이션의 web.xml에 <distributable/> 태그 추가
그럼 server.xml에 설정해 보자. <Cluster> 태그는 <Host> 태그의 하위에 정의해 주면 된다. 즉 여러 개의 호스트(예를 들어 가상 호스트) 를 설정했다면 각각의 경우에 맞게 설정해 주어야 한다. 여기서는 tomcat1과 tomcat2가 동일한 하드웨어에 별도의 바이너리 형태로 설치되어 있다고 가정하고 진행하겠다. 리스트 6. server.xml 
... <Cluster className="org.apache.catalina.cluster.tcp.SimpleTcpCluster" managerClassName="org.apache.catalina.cluster.session.DeltaManager" expireSessionsOnShutdown="false" useDirtyFlag="true"> <Membership className="org.apache.catalina.cluster.mcast.McastService" mcastAddr="228.0.0.105" mcastPort="45564" mcastFrequency="500" mcastDropTime="3000"/> <Receiver className="org.apache.catalina.cluster.tcp.ReplicationListener" tcpListenAddress="auto" tcpListenPort="4001" tcpSelectorTimeout="100" tcpThreadCount="6"/> <Sender className="org.apache.catalina.cluster.tcp.ReplicationTransmitter" replicationMode="pooled"/> <Valve className="org.apache.catalina.cluster.tcp.ReplicationValve" filter=".*\.gif;.*\.js;.*\.jpg;.*\.htm;.*\.html;.*\.txt;"/> </Cluster> ...

리스트 6은 tomcat1의 server.xml에 적용한 <Cluster> 태그이다. 이 내용은 <Host> 태그 사이에 위치하게 된다. <Cluster> 태그 사이에는 <Membership>, <Receiver>, <Sender>라는 3개의 태그가 위치하는데 <Membership>은 멤버 그룹을 정의하는 것으로 해당 값이 동일한 모든 톰캣 프로세스는 클러스터로 묶이게 된다. <Receiver>는 클러스터 그룹에서 보내오는 메시지와 세션 정보 등을 받아오는 것이며 <Sender>는 자신의 세션 정보 및 메시지를 전송하는 것이다. 위의 내용을 tomcat2의 server.xml에 Receiver 태그의 tcpListenPort 값을 4002로 변경해서 적용하도록 하자.


위로


웹 어플리케이션 작성을 통한 테스트

먼저 테스트를 위해서 간단한 웹 어플리케이션을 작성하도록 하겠다. 여기서 웹 어플리케이션 이름은 lbtest라고 하겠다.

리스트 7. index.jsp 
<%@ page contentType="text/html; charset=euc-kr" %> <HTML> <HEAD> <TITLE>세션 JSP 테스트</TITLE> </HEAD> <BODY> <h1>세션 JSP 테스트</h1> <% Integer ival = (Integer)session.getAttribute("sessiontest.counter"); if(ival==null) { ival = new Integer(1); } else { ival = new Integer(ival.intValue() + 1); } session.setAttribute("sessiontest.counter", ival); %> 당신은 이곳을 <b> <%= ival %> </b>번 방문 했습니다.<p> 여기를 클릭하세요. <a href="index.jsp">여기</a> <p> <h3>request 객체와 관련된 세션 데이터</h3> 요청된 세션 ID : <%= request.getRequestedSessionId() %><br /> 쿠키로 부터 요청된 세션 ID 인가? : <%= request.isRequestedSessionIdFromCookie() %><br /> URL로부터 요청된 세션 ID 인가? : <%= request.isRequestedSessionIdFromURL() %><br /> 유효한 세션 ID 인가? : <%= request.isRequestedSessionIdValid() %><br /> </BODY> </HTML>

<span> 작성된 웹 애플리케이션을 tomcat1과 tomcat2에 배포한다. 이때 가장 중요한 것이 web.xml에 반드시 &lt;distributable/&gt;이라는 항목을 넣어 야 한다. 그럼 이제 테스트를 해보도록 하자. 먼저 아파치 웹서버, tomcat1, tomcat2를 차례로 실행시켜 보자. 그리고 <a class="smarterwiki-linkify" href="http://ipaddress/lbtest/index.jsp" style="color: rgb(92, 129, 167); ">http://ipaddress/lbtest/index.jsp</a> 접속하여 세션 객체를 생성해보자. 결과 화면은 그림 1과 같다. 여기서 요청된 세션ID를 보면 뒤에 어떤 톰캣에 접속한 상태인지를 알 수 있다. 이 화면상에서는 tomcat2에 접속한 세션이다. 그럼 tomcat2를 강제로 종료시켜 보도록 하자. 종료 후 화면에 보이는 “여기”를 계속 눌러 보자. 분명히 tomcat2가 종료되었음에도 불구하고 세션 값이 유지되고 있음을 알 수 있다. 이젠 반대로 tomcat2를 다시 실행시킨 후에 tomcat1을 종료시켜 보도록 하자. 역시 마찬가지로 세션 정보가 유지되는 것을 확인할 수 있을 것 이다. </span>test 결과화면
그림 1. 테스트 결과 화면

이상으로 톰캣을 이용한 로드밸런싱과 세션 클러스터링에 대해서 알아보았다. 일반적으로 로드밸런싱과 클러스터링은 성능 향상이라는 측면과 안정성 확보에 그 목적을 가지고 있다. 물론 고가의 상용 웹 어플리케이션 서버에 비하면 많이 부족하고 하드웨어를 이용한 로드밸런싱과 클러스터링에 비하면 안정성이 떨어질 수도 있지만 저렴한 비용으로 최대의 안정성과 성능을 얻고자 한다면 한번쯤 시도해 볼만한 좋은 기능이라고 할 수 있다. 아무쪼록 리눅스, 아파치, 톰캣 그리고 오픈소스를 통해 즐거운 웹 어플리케이션 개발 환경을 느껴보기 바란다.




위로


참고 자료
The Apache Tomcat Connector: http://tomcat.apache.org/connectors-doc/ 
Clustering/Session Replication HOW-TO: http://tomcat.apache.org/tomcat-5.5-doc/cluster-howto.html 


dW의 관련 기술 자료
Jakarta Tomcat을 갖춘 개발 환경으로서의 Eclipse (한글)

:
Posted by 라면스프
2010. 5. 12. 21:19

[Java] CallableStatement 예제 Enjoy/JAVA2010. 5. 12. 21:19


[Java]
CallableStatement
CallableStatement Sample   Java / Program  
 
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CallableStatementDemo {
 public static void main(String[] args) {
  try {
   Class.forName("oracle.jdbc.driver.OracleDriver");
  } catch (ClassNotFoundException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  Connection con = null;
  CallableStatement cstmt = null;
  ResultSet rs = null;
  
  try {
   con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "tiger");
   cstmt = con.prepareCall("{call adjust(?,?)}");
   cstmt.setString(1, "kts8395");
   cstmt.setFloat(2, 0.3f);
   cstmt.executeUpdate();
  } catch (SQLException e) {
   // TODO: handle exception
   e.printStackTrace();
  } finally {
    try {
     if(rs != null) rs.close();
     if(cstmt != null) cstmt.close();
     if(con != null) con.close();
    } catch (SQLException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
  }
 }
}






=======================================================


function 에서 Java로 cursor 넘기기   ORACLE  
=========================
1. ORACLE 처리
=========================

create or replace package types
as
    type cursorType is ref cursor;
end;
/


create or replace function sp_ListEmp return types.cursortype
as
    l_cursor    types.cursorType;
begin
    open l_cursor for select ename, empno from emp order by ename;
    return l_cursor;
end;
/



=========================
2. JAVA Program
=========================

import java.sql.*;
import java.io.*;
import oracle.jdbc.driver.*;

public class ReturnCursor
{

  public static void main (String args [])
                     throws SQLException, ClassNotFoundException
  {
  String driver_class = "oracle.jdbc.driver.OracleDriver";
      String connect_string = "jdbc:oracle:thin:@211.111.111.111:1521:sid";
      String query = "begin ? := sp_listEmp; end;";
      Connection conn;
      Class.forName(driver_class);


      conn = DriverManager.getConnection(connect_string, "id", "passwd");
      CallableStatement cstmt = conn.prepareCall(query);
      cstmt.registerOutParameter(1,OracleTypes.CURSOR);
      cstmt.execute();
      ResultSet rset = (ResultSet)cstmt.getObject(1);
      while (rset.next ())
        System.out.println( rset.getString (1) + "\t" + rset.getInt (2) );
      cstmt.close();

  }
}



========================================================================================




CallableStatementTest.java(CallableStatement 예제)
 
: CallableStatement는 SQL의 스토어드프로시저(Stored Procedure)를 실행시키기 위해
  사용되는 인터페이스

 
import java.sql.*;
public class CallableStatementTest{
   public static void main(String[] args){
       try{
          Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
          Connection con = DriverManager.getConnection("jdbc:odbc:dbdsn", "id", "password");
          CallableStatement cs = con.prepareCall("{call myStoredProcedure(?,?,?)}");
          cs.setInt(1,2);
          cs.registerOutParameter(2, java.sql.Types.VARCHAR);
          cs.registerOutParameter(3, java.sql.Types.INTEGER);
          cs.execute();
          System.out.println("*name : "+ cs.getString(2) +"*age : "+ cs.getInt(3));
          cs.close();
          con.close();
       }catch(Exception e){System.out.println(e);}
   }
}




/*
create or replace function employ_ename (e_empno in emp.empno%TYPE) return varchar2 
is 
e_ename 

varchar2(10) := '홍길동'; 
begin 
  select ename into e_ename from emp 
  where  empno = e_empno; 
  return e_ename; 
exception 
  when no_data_found or too_many_rows then 
       return e_ename; 
end; 
*/
import java.sql.*;
public class TestCallable {
    public static void main(String args[]) throws Exception {

//예외처리를  main(String args[]) throws Exception 처름 하지 마세요

//여기서는 흐름을 쉽게 보이기 위해서 사용한 것입니다.
        String driverClass = "oracle.jdbc.driver.OracleDriver";
        String Url             = "jdbc:oracle:thin:@localhost:1521:sid";
        String User          = "scott";
        String Pwd           = "*****";
        
        Class.forName(driverClass);
        Connection conn = DriverManager.getConnection(Url,User,Pwd);
        // call 함수명
        String call = "{? = call employ_ename(?) }";
        int result = 0;
        
        CallableStatement c = conn.prepareCall(call);
        
        // IN parameter설정
        c.setInt(2, 7566);
        
        // Out parameter의 Type설정
        c.registerOutParameter(1, java.sql.Types.VARCHAR );
        
        // CallableStatement실행
        c.executeUpdate();
        
        // Out parameter의 값을 얻고, 출력한다.
        System.out.println("result : " + c.getString(1));
        conn.close();        
    }  

:
Posted by 라면스프
2010. 5. 4. 09:50

[Java] javadoc(API 문서) 작성 Enjoy/JAVA2010. 5. 4. 09:50



* API 문서작성 주석문
※ '@태그'를 사용한다.
@author : 클래스나 인터페이스의 제작자 표시 
@version : 버전정보 
@return : 메소드가 void형이 아닌경우 return value type을 기술 
@exception : 메소드가 발생 시킬수 있는 예외를 기술 
@throws : @exception Tag와 동일 
@deprecated : 다음버전에서 폐기된 메소드를 알림 
@param : 매개변수에 대한 설명(@param 변수 설명의 형태) 
@serial : 기본적으로 직렬화 할 수 있는 클래스의 멤버를 설명 
@see : 어떤 클래스 , 인터페이스,메소드, 생성자 혹은 URL에 대한 전후참조표시 
@since : Tag를 가진 객체가 언제 추가 되었는지 명시 

* API 문서작성법
javadoc -d . 소스파일.java

* 예제

/** 
* 자바 API문서 만들기 예제 소스 
*  
* @author 별소리
* @version 1.0 
*   
*/

public class JavaDocTest 
{
 /** 기본 생성자
 */
 public JavaDocTest()
 {
  //내용 없는 기본 생성자
 }


 /** 메서드입니다. 단순히 문자열을 프린트합니다.
 */
 public void printTest()
 {
  System.out.println("Hello World!");
 }


 /** 실행 main 메서드
 */

 public static void main(String[] args) 
 {
  new JavaDocTest().printTest();
 }
}



이를 [ javadoc -d . JavaDocTest.java ]로 컴파일하면...

이런 식으로 익숙한 형태의 API 문서가 만들어집니다.

:
Posted by 라면스프

제목 : 서블렛 + JDBC 연동시 코딩 고려사항 -제1탄-
글쓴이: 이원영(javaservice) 2000/09/06 05:19:47 조회수:96798 줄수:1544
최초작성일자: 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
================================================
:
Posted by 라면스프
2009. 4. 15. 17:59

JVM Memory Enjoy/JAVA2009. 4. 15. 17:59





* JVM Memory 영역에 대한 설명

- Method Area : 메소드와 클래스 변수를 저장하기 위한 공간, 모든 프로그램에 의해 공유.
- Heap Area : 사용자가 생성하는 Java Object들이 저장되는 공간, 동적으로 할당하여 사용되어짐.
- Stack Area : 메소드 호출시 해당 메소드의 매개변수, 지역변수, 임시변수 등을 저장하기 위한 Stack 구조의 메모리.
- Native Heap Area : Java Object가 아닌 Native Object들이 거주하는 공간. OS 차원에서 결정.
- Permanent Space : Class에 대한 Meta 정보를 저장하는 공간. (Permanent Space는 Java Heap의 하위 영역)


* Java 실행 Option

1. -X Option (모든 VM에서 동작하지 않을 수 있는 비표준 option이며, 버젼별로 언급없이 변경되어질 수 있음)
-Xms : 초기 Heap size 설정
-Xmx : 최대 Heap size 설정
-Xss : 각 Thread에 할당되는 Stack size 설정
-Xmn : New 영역을 위한 Heap size 설정

2. -XX Option (올바른 동작을 위해 특정한 시스템 요구사항들이 있으며, 시스템 설정 파라미터에 대한 접근 권한이 요구됨)
-XX:PermSize : 초기 Permanent size 설정
-XX:MaxPermSize : 최대 Permanent size 설정

※ 참고 사이트 : http://blogs.sun.com/watt/resource/jvm-options-list.html
                           http://www.monosun.com/doc/hotspotvmoption.html


* Heap Size 구하기

long   heapSize   = Runtime.getRuntime().totalMemory();
String heapSizeMB = (heapSize / (1024*1024)) + "MB";


* 영역별 OutOfMemoryError 대처 방법

1. Heap Area
Exception in thread "main": java.lang.OutOfMemoryError: Java heap space
Exception in thread main: java.lang.OutOfMemoryError: Requested array size exceeds VM limit


원인 : Heap size의 부족으로 Java Object를 생성하지 못하여 발생
해결 : 1. -Xmx Option을 이용하여 최대 Heap size의 크기를 늘려줌
           2. Application 로직이 잘못되었거나 JDK나 WAS의 Bug로 인한 Memory 누수가 있는지 확인하여 수정
           3. finalize method에 의해 즉각적인 GC가 이루어지지 않으므로 로직 수정을 통해 해결

※ Object Allocation Profiling (Hprof)
java -Xrunhprof:heap=sites [Main Class]
java -Xrunhprof:heap=sites,doe=n [Main Class] (Thread Dump 생성)


2. Permanent Space
Exception in thread "main": java.lang.OutOfMemoryError: Perm Gen space'

원인 : Permanent 저장 공간이 부족하여 발생.
          JSP -> Servlet 변환, Reflection을 사용하여 동적으로 로딩되는 Class가 많은 경우에 발생할 수 있으며,
          WAS의 Class Reloading 기능이 자주 실행 될 경우에도 발생할 수 있음.
해결 : -XX:PermSize, -XX:MaxPermSize Option을 이용하여 크기를 늘려줌

※ Class Loading Monitoring
java 실행시 -verbose:gc을 사용하여 Class가 Loading 되는 것을 Monitoring


3. Native Heap Area
java.lang.OutOfMemoryError: request bytes for . Out of swap space?
java.lang.OutOfMemoryError: (Native method)'
java.lang.OutOfMemoryError: unable to create new native thread


원인 : Native Heap memory가 부족하여 발생
해결 : 1. Physical memory를 초과할 경우 Virtual Memory를 요청하여 필요한 메모리를 확보하게 되는데,
             이 과정에서 오류가 발생할 경우 OS가 제공하는 툴을 통해 이를 모니터링 하고
             이 공간 자체가 부족할 경우 크기를 늘려줌
          2. -Xmx Option을 이용하여 Heap Area 공간을 줄이고 Native Heap Area 공간을 늘림
          3. Thread Stack Space가 부족한 경우 Thread의 수를 줄이거나,
              -Xss Option을 통해 Thread별 Stack Size를 줄여줌
              (단, Thread별 Stack Size를 과도하게 줄였을 경우 Stack Overflow Error가 발생할 수 있음)



참고 사이트 : http://ukja.tistory.com/61
                      http://2005elc.elancer.co.kr/eTimes/page/eTimes_view.html?str=c2VsdW5vPTIwODY=
:
Posted by 라면스프