자바 어플리케이션(jar)을 실행파일(exe)로 만들기 :: launch4j

자바 어플리케이션도 실행 jar ( Executable JAR ) 파일로 작성 하면, jar파일을 더블클릭하여 실행하는 것이 가능합니다만..


정말?


자바어플리케이션을 실행할 PC에 자바구동환경( 자바런타임 JRE )이 미리 설치 되어 있지어야 한다는 약점이 있습니다.
이 때문에 자바 Swing/AWT등으로 작성된 GUI어플리케이션의 경우 어플리케이션 작성 후 Executable JAR파일을
윈도우용 실행 파일(exe)로 감싸는(Wrapping)하는 단계를 거치게 됩니다.

Executable JAR 파일을  exe파일로 감싸데는 몇 가지 방법이 있지만 이번 포스팅에서는 launch4j를 이용해 자바 어플리케이션을
exe형태의 파일로 변환하는 방법을 소개토록 하겠습니다.

잠깐 launch4j를 소개하자면 sourceforge.net 에 등록된 프로젝트로 Cross-platform Java executable wrapper를 구현하고 있습니다.
사용법의 간편함과 다양한 옵션이 존재하는 장점 외에도, 라이센스에서 이 프로그램을 이용해서 상업적인( commercial ) 어플리케이션을
작성해도 된다고 밝히고 있습니다.

  준비물 :
    launch4j ( 다운로드페이지 열기 )
    exe 파일을 만들 자바 어플리케이션.

PhotoRoverViewer.zip

테스트용 스윙어플리케이션





 

우선, 자바 어플리케이션을 작성하여 Executable JAR로 만들어 둔다.
( 이전 포스팅 참조 하며, 본 예제를 실행해 보기 원한다면 위에 링크한 자바 어플리케이션을 사용하시면 됩니다.)

그 다음, launch4j 실행하셔서 아래 화면을 참조 하셔서 wrapping환경을 세팅합니다.

launch4j 기본 설정 화면

launch4j 기본 설정 화면


1 번 항목에 jar파일을 어떤 디렉토리에 어떤 파일명의 exe로 떨굴지를 기술합니다.
2 번 항목에서 exe로 wrapping할 Executable jar파일을 선택 합니다.
3 번 항목에 기술한 URL은 변환된 자바 exe파일을 실행할 PC에 자바런타임 환경이 없을경우 안내문구와 이동할 웹사이트를 기술합니다.


launch4j ClassPath 설정 화면

launch4j ClassPath 설정 화면


4 번 항목에서 main 클래스를 기술합니다. Executable jar를 만들어 뒀다면 파일선택에서 jar파일을 선택하는것
만으로도 jar의 MANIFEST.MF 을 분석해서 main 함수와 lib 를 자동으로 세팅해 줍니다.


launch4j Header 설정 화면

launch4j Header 설정 화면


GUI 어플리케이션인지 Console 어플리케이션인지 세팅 해 줍니다.


launch4j JRE 설정 화면

launch4j JRE 설정 화면


5 번 항목은 exe파일과 jre를 함께 배포할 경우 jre패스가 어떻게 되는지 상대 경로 혹은 절대경로를 기술해 줍니다.
6 번 항목에 자바어플리케이션이 실행되기위한 최소버전의 JRE를 기술해 줍니다.


launch4j Version Info 설정 화면

launch4j Version Info 설정 화면


Add Version infomation을 체크하면 exe파일의 속성정보에 나타낼 정보를 기술합니다.


launch4j Message 설정 화면

launch4j Message 설정 화면


Add Custom Message를 체크하면 각 상황에따라 출력할 메세지를 기술할 수 있습니다.
현재 버전까지는 한글 메세지는 지원하지 않더군요. ^^

위와 같은 내용을 기술 해 준 후 launch4j 상단 아이콘 중 톱니바퀴를 클릭하면 지정한 exe파일이 생성 됩니다.



[Flash] http://www.yunsobi.com/anyone/2008-01-04_1144.swf

2008/01/04 11:51 2008/01/04 11:51
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다
  1. 2008/10/05 15:51
  1. 좋은 정보 잘 보고 가요 ~ ^^

  2. Blog Icon
    서비

    도움이 되셨다니 저도 기쁩니다.

  3. 저도 잘보고 갑니다.잘되는군요 ;)

  4. Blog Icon
    coding4fun

    좋은 정보 감사합니다. 그런데, lauch4j로 만든 exe를 실행할 때, 절대 경로가 영어로만 되어 있으면 문제 없는데, 절대 경로 상에 한글이 포함되어 있으면 실행이 안되는 문제가 있습니다. 혹시 해결 방법은 아시는지요..

  5. Blog Icon
    공대소년

    많이 배우고 갑니다.
    감사합니다. :)

  6. Blog Icon
    공생공사

    여러글을 보다가 여기까지 왔네요..혹시
    반대로는 가능한가요?
    exe --> jar 형태로요?

  7. Blog Icon
    서비

    공생공사님 안녕하세요.
    http://stackoverflow.com/questions/7760126/how-to-convert-exe-file-to-jar-file 에도 님과 같은 질문 관련글이 있는데 대부분의 jar -> exe 컨버팅 도구의 경우 exe내부에 단순히 jar를 싸고 있는 형태로 패키징 하지 않고 .class 정보를 이용하여 실행파일을 생성해내기 때문에 exe에서 jar를 추출해 낼 수는 없습니다.

    다만, jsmooth나 launch4j 라도 exe에 jar를 포함하는 형태가 아닌 단순히 jar를 실행하기위한 런처만 exe로 구성했다면 실행 jar가 어딘가에 위치하고 있을 확률은 존재합니다.

    어떤 이유로 추출이 필요하신지 대강 짐작은 됩니다만, 가능하시면 원 개발자와 컨택하셔서 소스를 얻어내는 방안을 고민해보시는게 현실적일것 같습니다.

  8. Blog Icon
    비밀방문자

    관리자만 볼 수 있는 댓글입니다.

  9. Blog Icon
    안녕하세요

    질문 있습니다.
    //자바어플리케이션을 실행할 PC에 자바구동환경( 자바런타임 JRE )이 미리 설치 되어 있지어야 한다는 약점이 있습니다.
    이 때문에 자바 Swing/AWT등으로 작성된 GUI어플리케이션의 경우 어플리케이션 작성 후 Executable JAR파일을
    윈도우용 실행 파일(exe)로 감싸는(Wrapping)하는 단계를 거치게 됩니다.

    ...라고 적혀있는데 조금만 밑으로 내려가면

    //변환된 자바 exe파일을 실행할 PC에 자바런타임 환경이 없을경우 안내문구와 이동할 웹사이트를 기술합니다.

    ...라고 적혀있습니다. exe파일로 만들면 jre가 없는 환경에서도 실행될 수 있다는 말 아닌가요? 근데 왜 안내문구와 이동할 웹사이트가 필요한지 모르겠습니다.

  10. Blog Icon
    ㄴㄴ

    jar 파일을 exe파일로 wrapping 하는 작업은 단순히 exe를 통해 jar파일을 실행시켜주는 역할만 하는 겁니다.
    실행은 jvm 위에서 해야하므로, 환경이 되어야 작동이 가능합니다.

  11. Blog Icon
    misterwon

    감사합니다 ㅎㅎ 큰 도움이 되었습니다

넷빈즈( NetBeans ) 6.0 자바 소스 인코딩 설정은 어디서?

넷빈즈 4, 5 버전때까지는 자바소스의 인코딩 설정을 [옵션->Java Source] 항목에서 결정할 수 있었다.
(http://www.yunsobi.com/blog/52 참조)

사용자 삽입 이미지
[넷빈즈4 / 5 버전대의 소스 인코딩 설정 위치]


넷빈즈 6.0 버전으로 오면서 이 설정의 위치가 바뀌었다. 넷빈즈 6.0에서는 프로젝트 프로퍼티창의 소스 카테고리에서 설정이 이루어 진다.

사용자 삽입 이미지
[개별 프로젝트의 Properties 항목 선택 후]


사용자 삽입 이미지
[Source 카테고리에서 인코딩 설정]


옵션 위치의 변화는 무엇을 의미하는걸까?
4 / 5 버전대에서는 소스코드의 인코딩이 모든 프로젝트에 일괄 적용 될 수 밖에 없는 구조이지만, 6 과같이 프로젝트 프로퍼티로
옵션 위치를 변경하면 각 프로젝트마다 자바소스의 인코딩을 달리 적용 할 수 있음을 의미하며, 이는 IDE가 개별 프로젝트를
좀 더 유연하게 받아들일 수 있는 바탕이기도 하다.


#잠깐만 !!
 넷빈즈 IDE에서 위처럼 인코딩 옵션을 설정하며 자바소스 코드의 인코딩에만 영향을 미치며
 이 설정은 JSP 소스 인코딩에 영향을 주지 않는다. JSP소스 인코딩은 서블릿 규약에 따라 JSP
 페이지 디렉티브의 pageEncoding 속성에 기술하면 된다.
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>


2007/12/08 23:26 2007/12/08 23:26
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

넷빈즈 6.0 출시 :: NetBeans 6.0 Released!!

마일스톤, 베타, 캔디데이트를 거쳐 드디어 넷빈즈 6.0이 정식 출시 되었습니다.
넷빈즈 6.0 출시에 발맞추어 5.0 출시때와 마찬가지로 netbeans.org의 화면도 변화가 있네요.
(하지만 플러그인 탭으로 들어가면 아직까진 5 버전때의 화면을 볼 수 있습니다.)

netbeans.org
새 디자인이 적용된 netbeans.org


아래는 앞으로 넷빈즈의 개발 로드맵입니다. 현재 정식출시된 6.0 버전의 마이너 업그레이드는
계속 되겠지만, 메이저 업그레이드는 한동안 없을 것 같군요.
netbeans roadmap

넷빈즈 6.0 다운로드는 여기에서 할 수 있습니다.
다운로드 할 수 있는 패키지 형식도 다양해 졌습니다..
지금까진 NetBeans only,  NetBeans + J2SE, NetBeans + J2EE Server 세가지 종류의 패키지로
배포가 되었는데 이제는
Web & Java EE,  Mobility,  Java SE,  Ruby,  C/C++,  All  등으로 좀더 특화/세분화 되었습니다.
 물론 어떤 패키지를 받더라도 플러그인 다운로드를 통하여 다른 패키지의 기능을 추가할 수 있습니다.

넷빈즈 패키지 테이블



2007/12/06 12:40 2007/12/06 12:40
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

윈도우 서비스모드로 설치한 톰캣 4의 설정 변경하기

톰캣 5 버전부터는 작업표시줄에서 톰캣을 모니터링 할 수 있는 Apache Tomcat Proterties 라는 유틸리티가 있어
이를 통해 톰캣의 설정을 변경 할 수 있지만 톰캣 4 버전대를 서비스모드로 설치하면 프로퍼티를 수정 할 수 없어
난감할 때가 있습니다.
사용자 삽입 이미지
Tomcat 5 버전의 모니터링 툴

이럴 경우 레지스트리 에디터를 열어
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services 아래에서 Apache Tomcat 항목을 찾으시면
프로퍼티를 수정 할 수 있습니다.

사용자 삽입 이미지
Tomcat 4 버전이라면 레지스트리에서 수정하자!!



2007/11/27 15:56 2007/11/27 15:56
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

자바 SE 플랫폼 개괄 :: Java SE Platform at a Glance

2007/11/23 18:29

서비 JAVA ,

아래는 SUN사의 자바사이트에 포스팅 되어있는 자바 SE 플랫폼의 개략도 입니다.
사이트를 방문하시면 각 layer / topic 별로 링크가 되어 해당 기술에 대한 참조를 할 수 있습니다.
Java EE 와 ME 도 이런식의 도해가 있을 줄 알고 뒤져 보았는데 안보이더군요.

Java SE Platform at a Glance
2007/11/23 18:29 2007/11/23 18:29
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

Java SE 6.0 Performance White Paper

2007/11/05 14:04

서비 JAVA ,

SDN( Sun Developer Network ) 에 JAVA SE 6.0 성능 백서가 발표되었습니다.

성능 개선과 추가된 기능들, 새로운 플랫폼 지원, 개발 진행 방향이라는 세 가지 꼭지로 정리된 이 백서에는 지금까지 JDK가 그래 왔던 것처럼 성능향상을 위해 컴파일러, 버추얼머신, 가비지콜렉션, I/O, 자료구조와 알고리즘 개선 부분을 성능 향상의 포인트로 두고 있습니다.

특히  JAVA 6.0 SE에서는, Boot Class 로딩 속도의 향상이라든지, SWING GUI 어플리케이션에 Double Buffering이 기본 지원사양으로 포함되었다든지, 버추얼 머신이 시작되기 전 Splash Screen을 처리할 수 있는 기능 등이 포함되는 등 JAVA 데스크탑 어플리케이션을 위한 성능 향상부분에 많은 노력을 기울이고 있다는 것을 알 수 있는데요.

JAVA 데스크톱 어플리케이션이 특정분야에 한정되어있고, GUI 디자인도 서서히 MARK-UP 형식의 언어로 넘어가는 현실에서 Swing, AWT와같은 Form-Based GUI 디자인이 언제까지 계속될진 모르겠지만 Swing 데스크탑 어플리케이션 개발을 위한 지원이 계속되고 있다는 것은 반길만한 일이 아닌가 합니다.

백서의 전문은
http://java.sun.com/performance/reference/whitepapers/6_performance.html
에서 확인하실 수 있습니다.
2007/11/05 14:04 2007/11/05 14:04
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

JAVA Base64 Encoding / Decoding

2007/10/12 20:44

서비 JAVA ,

//Ok how about this pure Java API solution.
//Yours for free free free, no unintened theft.
//Completely portable per the Java promise.
//This code sets no preferences! The class just extends AbstractPreferences to get access to the put and get and over rides them.
//The super still calls the Base64 to do the encoding and decoding.
//This extension intercepts the encoded/decoded strings and makes them visible.
//TaDa Java API supports, has, allows, does base64 encoding, for the clever anyway.

package yourpackage;
public class YourPreferences extends java.util.prefs.AbstractPreferences {
//keep the key state
    private java.util.Hashtable encodedStore = new java.util.Hashtable();
   
    /** Creates a new instance of YourPreferences*/
    public YourPreferences(java.util.prefs.AbstractPreferences prefs, java.lang.String string) {
 super(prefs, string);
    }
//main acts as a driver i.e. used to test this class not required by this class
    public static void main(java.lang.String[] args ){
 java.util.prefs.AbstractPreferences myprefs = new yourpackage.YourPreferences(null, "");
 
 java.lang.String stringToEncode = "Aladdin:open sesame";
 java.lang.String key = "Aladdin:open sesame";
 java.lang.String key_ = "KEYisNOTtheSAMEasTHEstring";
 try{
     java.lang.String encoded = ((yourpackage.YourPreferences)myprefs).encodeBase64(stringToEncode);
     java.lang.System.out.println("ENCODED STRING TO: " + encoded);
     java.lang.String base64 = (java.lang.String)((yourpackage.YourPreferences)myprefs).encodedStore.get(key);
     java.lang.String decoded = ((yourpackage.YourPreferences)myprefs).decodeBase64("newkey",base64);
     java.lang.System.out.println("DECODED STRING TO: " + decoded);
    
     java.lang.String encoded_ = ((yourpackage.YourPreferences)myprefs).encodeBase64(key_,"ALONGSTRANGESTRINGTHATMAKESNOSENCEATALLBUTCANBEENCODEDANYWAYREGARDLESS");
     java.lang.System.out.println("ENCODED STRING TO: " + encoded_);
     java.lang.String base64_ = (java.lang.String)((yourpackage.YourPreferences)myprefs).encodedStore.get(key_);
     java.lang.String decoded_ = ((yourpackage.YourPreferences)myprefs).decodeBase64(key_,base64_);
     java.lang.System.out.println("DECODED STRING TO: " + decoded_);
     java.lang.System.out.println("\n");
     java.util.Enumeration enum = ((yourpackage.YourPreferences)myprefs).encodedStore.keys();
     java.lang.String enumKey = null;
     while(enum.hasMoreElements()){
  enumKey = (java.lang.String)enum.nextElement();
  java.lang.System.out.print(enumKey);
  java.lang.System.out.println(" : " +((yourpackage.YourPreferences)myprefs).encodedStore.get(enumKey).toString());
     }
 }catch(java.io.UnsupportedEncodingException uee){
     uee.printStackTrace();
 }catch(java.io.IOException ioe){
     ioe.printStackTrace();
 }
    }
    public java.lang.String encodeBase64(java.lang.String key, java.lang.String raw)throws java.io.UnsupportedEncodingException{
 java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
 java.io.PrintWriter pw = new java.io.PrintWriter(
     new java.io.OutputStreamWriter(baos,"UTF8"));
 pw.write(raw.toCharArray());
 pw.flush();//ya know
 byte[] rawUTF8 = baos.toByteArray();
 this.putByteArray(key, rawUTF8);
 
 return (java.lang.String)this.encodedStore.get(key);
    }
    public java.lang.String encodeBase64(java.lang.String raw)throws java.io.UnsupportedEncodingException{
 
 java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
 java.io.PrintWriter pw = new java.io.PrintWriter(
     new java.io.OutputStreamWriter(baos,"UTF8"));
 pw.write(raw.toCharArray());
 pw.flush();//ya know
 byte[] rawUTF8 = baos.toByteArray();
 this.putByteArray(raw, rawUTF8);
 
 return (java.lang.String)this.encodedStore.get(raw);
    }
    public java.lang.String decodeBase64(java.lang.String key, java.lang.String base64String)
    throws java.io.UnsupportedEncodingException, java.io.IOException{
 byte[] def = {(byte)'D',(byte)'E',(byte)'F'};//not used at any point
 this.encodedStore.put(key,base64String);
 char[] platformChars = null;
 byte[] byteResults = null;
 byteResults = this.getByteArray(key, def);
 java.io.InputStreamReader isr = new java.io.InputStreamReader(
     new java.io.ByteArrayInputStream(byteResults),"UTF8");
 platformChars = new char[byteResults.length];
 isr.read(platformChars);
 
 return new java.lang.String(platformChars);
    }
//intercept key lookup and return our own base64 encoded string to super
    public String get(String key, String def) {
 return (java.lang.String)this.encodedStore.get(key);
    }
//intercepts put captures the base64 encoded string and returns it
    public void put(String key, String value){
 this.encodedStore.put(key, value);//save the encoded string
    }
//dummy implementation as AbstractPreferences is extended to get acces to protected
//methods and to overide put(String,String) and get(String,String)
    protected java.util.prefs.AbstractPreferences childSpi(String name) {return null;}
    protected String[] childrenNamesSpi() throws java.util.prefs.BackingStoreException {return null;}
    protected void flushSpi() throws java.util.prefs.BackingStoreException {}
    protected String getSpi(String key) {return null;}
    protected String[] keysSpi() throws java.util.prefs.BackingStoreException {return null;}
    protected void putSpi(String key, String value) {}
    protected void removeNodeSpi() throws java.util.prefs.BackingStoreException {}
    protected void removeSpi(String key) {}
    protected void syncSpi() throws java.util.prefs.BackingStoreException {}
}

2007/10/12 20:44 2007/10/12 20:44
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

BigDecimal의 필요성

부동 소수점 숫자 사용은 재미있는 작업이 될 수 있다. 일반적으로 양을 다룰 때 정수가 아니라면 double 형식을 사용하려고 한다. 정수인 경우 일반적으로 int 형식이면 충분하다. 값의 크기에 따라 float나 long도 효과적일 수 있다. 그러나 화폐를 다룰 경우, 이 형식들은 최악의 선택이 되는데, 정확한 값을 제공하지 않을 때도 있기 때문이다. 단지 이진수 형식에 저장 가능한 값만 제공한다. 다음은 합계를 계산하거나 할인을 반영하거나 부가가치세를 추가할 때 double을 사용하면 겪게 되는 문제를 보여주는 간단한 예제이다.

Calc 프로그램은 금액 $100.05로 시작한 다음 사용자에게 10% 할인을 적용하고 다시 5% 부가가치세를 추가한다. 부가가치세율은 다를 수 있지만, 이 예제에서는 5%를 사용한다. 그 결과를 보자면, 이 클래스는 NumberFormat 클래스를 사용하여 결과의 형식을 지정하는데, 이 결과는 통화로 표시되어야 한다.

import java.text.NumberFormat;

public class Calc {

  public static void main(String args[]) {
    double amount = 100.05;
    double discount = amount * 0.10;
    double total = amount - discount;
    double tax = total * 0.05;
    double taxedTotal = tax + total;
    NumberFormat money = NumberFormat.getCurrencyInstance();
    System.out.println("Subtotal : "+ money.format(amount));
    System.out.println("Discount : " + money.format(discount));
    System.out.println("Total : " + money.format(total));
    System.out.println("Tax : " + money.format(tax));
    System.out.println("Tax+Total: " + money.format(taxedTotal));
  }
}

모든 내부 계산에 double 형식을 사용하면 결과는 다음과 같다.

Subtotal : $100.05
Discount : $10.00

Total : $90.04
Tax : $4.50
Tax+Total: $94.55

가운데의 Total이 기대하는 값일 수 있으나, 맨 끝의 Tax+Total 값에서 끝난다. 할인은 $10.01이 되어야 $90.04의 금액을 얻는다. 해당 부가가치세를 추가하고 나면 최종 합계가 1센트 늘어나 있다. 세무 당국은 이 점을 이해하지 못할 것이다. 반올림 오류가 문제이다. 이 반올림 오류를 바탕으로 계산한 것이다. 다음은 형식이 지정되지 않은 값이다.

Subtotal : 100.05
Discount : 10.005

Total : 90.045
Tax : 4.50225
Tax+Total: 94.54725

형식이 지정되지 않은 값을 살펴보자면, 맨 처음 떠오르는 질문은 왜 90.045가 90.05 대신 90.04가 되었는가이다. (즉, 왜 10.005가 10.00으로 반올림되는가?) 이는 소위 RoundingMode에 의해 제어되는데, 이는 Java SE 1.6에 도입된 계수법으로서 이전 릴리스에서는 그러한 제어가 없었다. 통화를 위해 도입된 NumberFormat은 기본 반올림 모드가 HALF_EVEN이다. 즉, 남은 값이 등거리(equidistant)이면 짝수 쪽으로 라운딩된다. 이 계수법에 대한 Java 플랫폼 설명서에 따르면, 통계상 이 방법은 여러 차례의 계산을 거친 후 누적 오류를 최소화한다.

RoundingMode
계수법에서 사용 가능한 또 하나의 모드는 다음과 같다.

  • CEILING 항상 양수 무한대로 라운딩한다.
  • DOWN 항상 0으로 라운딩한다.
  • FLOOR 항상 음수로 라운딩한다.
  • UP 항상 0으로부터 벗어나서 라운딩한다.
  • HALF_DOWN 항상 가장 가까운 인접 수로 라운딩한다. 단, 두 인접 수 모두 등거리라면 버림한다.
  • HALF_UP 항상 가장 가까운 인접 수로 라운딩한다. 단, 두 인접 수가 모두 등거리라면 올림한다.
  • UNNECESSARY 라운딩할 필요 없이 정확한 결과를 선언한다.

이 문제의 해결 방법을 알아보기에 앞서, 약간 다른 결과를 살펴 보도록 한다. 70센트로 시작하고 할인이 없는 경우이다.

Total : $0.70
Tax : $0.03

Tax+Total: $0.74

70센트 거래의 경우, 단순히 라운딩 문제가 아니다. 형식 지정 없이 값을 보자면 다음과 같다.

Total : 0.7
Tax : 0.034999999999999996

Tax+Total: 0.735

부가가치세인 0.035는 double로 저장할 수 없다. 즉, 이진 형식에서 double로 표현할 수 없다.

BigDecimal
클래스는 float 및 double을 사용하는 부동 소수점 연산에서 생기는 문제 몇 가지에서 도움이 된다. BigDecimal 클래스는 사실상 무제한적인 정밀도로 부동 소수점 숫자를 저장한다. 이 데이터를 다루기 위해 add(value), subtract(value), multiply(value) 또는 divide(value, scale, roundingMode) 메소드를 호출한다.

BigDecimal
값을 출력하려면 setScale(scale, roundingMode)로 단위 및 라운딩 모드를 설정하거나 toString() 또는 toPlainString() 메소드를 사용한다. toString() 메소드는 과학적 표기를 사용할 수 있지만, toPlainString()은 그렇지 않다.

BigDecimal
을 사용하도록 프로그램을 변환하기 전에 그 생성 방법을 확인하는 것이 중요하다. 이 클래스에는 16개의 구성자가 있다. BigDecimal의 값을 double과 같은 primitive 객체에 저장할 필요는 없으므로 String으로부터 BigDecimal 객체를 만드는 것이 가장 좋다. 이 오류를 보여 주기 위해 간단한 예제를 소개한다.

  double dd = .35;
  BigDecimal d = new BigDecimal(dd);

  System.out.println(".35 = " + d);

예상했던 출력이 아닐 것이다.

  .35 = 0.34999999999999997779553950749686919152736663818359375

그보다는 여기서 보여주는 것처럼 string "35"를 직접 사용하여 BigDecimal을 만들어야 한다.

  BigDecimal d = new BigDecimal(".35");

그 결과 다음과 같은 출력을 얻는다.

 .35 = 0.35

값을 생성한 후 숫자의 단위와 라운딩 모드를 setScale()을 사용하여 명시적으로 설정할 수 있다. Java 플랫폼의 다른 Number 하위 클래스와 마찬가지로, BigDecimal은 변경할 수 없다. 따라서 setScale()을 호출할 경우 반환 값을 "저장"해야 한다.

  d = d.setScale(2, RoundingMode.HALF_UP);

BigDecimal
을 사용하여 수정된 프로그램은 다음과 같다. 계산마다 또 다른 BigDecimal을 사용하고 그 단위를 설정해야 달러 및 센트에 대한 수학 연산이 수행된다. 분할 페니를 사용하려는 경우 단위에서 3개의 소수 자리로 갈 수도 있지만, 꼭 그럴 필요는 없다.

import java.math.BigDecimal;
import java.math.RoundingMode;


public class Calc2 {
  public static void main(String args[]) {
    BigDecimal amount = new BigDecimal("100.05");
    BigDecimal discountPercent = new BigDecimal("0.10");
    BigDecimal discount = amount.multiply(discountPercent);
    discount = discount.setScale(2, RoundingMode.HALF_UP);
    BigDecimal total = amount.subtract(discount);
    total = total.setScale(2, RoundingMode.HALF_UP);
    BigDecimal taxPercent = new BigDecimal("0.05");
    BigDecimal tax = total.multiply(taxPercent);
    tax = tax.setScale(2, RoundingMode.HALF_UP);
    BigDecimal taxedTotal = total.add(tax);
    taxedTotal = taxedTotal.setScale(2, RoundingMode.HALF_UP);
    System.out.println("Subtotal : " + amount);
    System.out.println("Discount : " + discount);
    System.out.println("Total : " + total);
    System.out.println("Tax : " + tax);
    System.out.println("Tax+Total: " + taxedTotal);
  }
}

여기서는 NumberFormat이 사용되지 않았다. 하지만 통화 기호를 보여주고 싶다면 다시 추가할 수 있다.

이제 프로그램을 실행하면 훨씬 나은 계산이 수행된다.

Subtotal : 100.05
Discount : 10.01

Total : 90.04
Tax : 4.50
Tax+Total: 94.54

BigDecimal
은 이 예제에서 보여준 것보다 더 다양한 기능을 제공한다. 정수를 사용하는데 무한대의 정밀도가 필요한 경우를 위한 BigInteger 클래스도 있다. 두 클래스에 대한 Java 플랫폼 설명서에서는 단위, MathContext 클래스, 정렬 및 동등(equality)에 대한 정보를 비롯하여 이 클래스와 관련된 자세한 내용을 확인할 수 있다.

저자 JZ Ventures사의 사장 겸 대표 컨설턴트 John Zukowski

2007/08/30 16:00 2007/08/30 16:00
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

자바 애플리케이션에서 동적으로 PDF 파일 생성하기

2007/08/26 22:06

서비 JAVA ,

애플리케이션에서 PDF 문서를 동적으로 만들어야 한다면 iText 라이브러리가 필요하다. 오픈 소스 iText 라이브러리로 PDF 생성이 간단해 진다. 이 글에서는 iText를 소개하고 이를 사용하여 자바 애플리케이션에서 PDF 문서를 생성하는 방법을 설명한다. iText를 잘 이해할 수 있도록 샘플 애플리케이션도 구현한다.

많은 애플리케이션들이 PDF 문서의 동적 생성을 요구하고 있다. 이 같은 애플리케이션은 이메일 전달용 고객 일람표를 생성하는 은행부터, 책의 특정 챕터를 구매하여 이를 PDF 포맷으로 받는 리더기 까지 다양하다. 그 리스트는 끝이 없다. 이 글에서 iText 자바 라이브러리를 사용하여 PDF 문서를 만들 것이다. 여러분의 이해를 돕기 위해 샘플 애플리케이션도 제공한다.
 
iText 공식 사이트 : http://www.lowagie.com/iText/download.html


iText와 친해지기

iText는 Lowagie.com(참고자료)에서 무료로 사용할 수 있는 자바 라이브러리이다. iText 라이브러리는 강력하고, HTML, RTF, XML 문서의 생성 뿐만 아니라 PDF 생성을 지원한다. 문서에 사용될 다양한 폰트를 선택할 수 있다. 또한 iText 구조에서는 같은 코드를 사용하여 앞서 언급한 유형의 문서를 만들 수 있다.

iText 라이브러리에는 다양한 폰트로 PDF 텍스트를 만들고, PDF 문서에 테이블을 생성하고, 워터마크를 페이지에 추가하는 클래스가 포함되어 있다. iText로 사용할 수 있는 더 많은 기능들이 있다. 하지만 그 모든 것을 이 글에서 다 설명하기는 불가능하다. PDF 생성에 필요한 기본적인 것만을 설명하겠다.

샘플 애플리케이션 개발에 Eclipse를 사용할 것이다. 오픈 소스 IDE인 Eclipse는 무료로 사용할 수 있고 매우 강력하다. 지금 Eclipse를 다운로드 하라. (참고자료)

iText API

com.lowagie.text.Document는 PDF 문서 생성을 위한 주 클래스이다. 인스턴스로 만들어질 첫 번째 클래스이다. 문서가 생성되면 라이터는 여기에 작성해야 한다. com.lowagie.text.pdf.PdfWriter는 PDF 라이터이다. 다음은 일반적으로 사용되는 클래스들이다.

  • com.lowagie.text.Paragraph -- 들여쓰기 단락을 나타내는 클래스이다.
  • com.lowagie.text.Chapter -- PDF 문서의 챕터이다. 타이틀로 Paragraph를 사용하고, 챕터 번호로 int를 사용한다.
  • com.lowagie.text.Font -- 폰트, 크기, 스타일, 컬러 같은 모든 폰트 스팩들이 포함되어 있다. 다양한 폰트들은 이 클래스에 정적 상수로서 선언된다.
  • com.lowagie.text.List -- 많은 ListItems를 포함하고 있는 리스트이다.
  • com.lowagie.text.Table -- 매트릭스에서 정렬된 셀들을 포함하고 있는 테이블이다.

Eclipse에서 iText 다운로드 및 설정하기

순수 자바 라이브러리인 아아텍스트는 JAR 파일의 형태로 되어 있다. (참고자료.) 라이브러리를 다운로드 하면(C:\temp) Eclipse 환경에서 iText 라이브러리를 설정한다.

  1. Eclipse에서 iText라는 이름의 새로운 자바 프로젝트를 만든다.
  2. Package Explorer 뷰에서 iText 프로젝트를 오른쪽 클릭하고 Properties를 선택한다.
  3. Java Build Path를 클릭한다. Libraries 탭에서, Add External JARs를 클릭한다.
  4. C:\temp 디렉토리를 검색하여 itext-1.3.jar를 선택한다.
  5. OK를 클릭한다.

iText가 설정되면 Eclipse는 동적인 PDF 문서를 생성하는 자바 애플리케이션을 구현할 준비를 갖춘 것이다.






샘플 애플리케이션

직접 샘플을 만드는 것 만큼 확실한 설명 방법은 없다. 필요한 툴(Eclipse IDE)과 라이브러리(iText 라이브러리)가 준비되었으니 샘플 프로그램을 디자인 및 개발해 보자.

평이한 텍스트, 특별 폰트로 색상을 입힌 텍스트, 테이블, 리스트, 챕터, 섹션 같은 기본적인 엘리먼트를 포함하고 있는 PDF 문서를 만들어 보자. 이 애플리케이션의 목적은 여러분이 iText 라이브러리를 사용하는 방법을 보다 잘 이해할 수 있도록 돕는 것이다. PDF 문서 생성과 관련되어 많은 일을 수행하는 많은 클래스들이 있다. 이 모든 클래스들을 다 다루기는 불가능하다. iText의 javadoc은 이러한 클래스들의 사용법을 잘 이해할 수 있는 좋은 자료이다. 코딩부터 시작해 보자.

첫 번째 단계는 문서를 만드는 것이다. 이 문서는 PDF 문서의 모든 엘리먼트용 컨테이너이다.


Listing 1. 문서 객체의 인스턴스화

				
Document document = new Document(PageSize.A4, 50, 50, 50, 50);

첫 번째 인자는 페이지 크기이다. 다음 인자들은 각각 왼쪽, 오른쪽, 상단, 하단 여백들이다. 이러한 유형의 문서는 아직 정의되지 않았다. 여러분이 만드는 라이터의 유형에 의존한다. 이 샘플에서 우리는 com.lowagie.text.pdf.PdfWriter를 선택했다. 기타 라이터로는 HtmlWriter, RtfWriter, XmlWriter 등이 있다. 이름만으로도 충분히 그 용도를 알 수 있다.


Listing 2. PdfWriter 객체의 생성

				
PdfWriter writer = PdfWriter.getInstance(document, \
new FileOutputStream("C:\\ITextTest.pdf"));
document.open();

첫 번째 인자는 문서 객체에 대한 참조이고, 두 번째 인자는 아웃풋이 작성될 파일의 고유 이름이다. 작성을 위해 문서를 연다.

이제, 문서의 첫 번째 페이지에 몇 가지 텍스트를 추가할 것이다. com.lowagie.text.Paragraph을 사용하여 어떤 텍스트라도 추가된다. 텍스트로 기본 단락을 만들고 폰트, 컬러, 크기 등 기본적인 설정을 할 수 있다. 각자의 고유 폰트를 사용해도 된다. 두 가지 옵션 모두를 살펴보자.


Listing 3. 단락 객체의 생성

				
document.add(new Paragraph("First page of the document."));
document.add(new Paragraph("Some more text on the \
first page with different color and font type.", 
FontFactory.getFont(FontFactory.COURIER, 14, Font.BOLD, new Color(255, 150, 200))));

아래 그림은 위 코드의 아웃풋이다. 위 코드 말미에 document.close();을 추가하여 문서를 마감한다.


그림 1. 아웃풋
Sample output of above code

평이한 텍스트를 PDF 문서에 추가하는 방법을 배웠다. 복잡한 요소들도 문서에 추가해야 한다. 새로운 챕터를 만든다. 이 챕터는 특별 섹션으로서 새로운 페이지로 시작하고 기본적으로 디스플레이 된 숫자가 있다.


Listing 4. 챕터 객체의 생성

				
Paragraph title1 = new Paragraph("Chapter 1", 
           FontFactory.getFont(FontFactory.HELVETICA, \
           18, Font.BOLDITALIC, new Color(0, 0, 255)));
Chapter chapter1 = new Chapter(title1, 1);
chapter1.setNumberDepth(0);

위 코드에서 chapter1 이라는 새로운 챕터 객체를 만들었다. 제목은 "This is Chapter 1."이다. 숫자 한계를 0으로 설정하면 페이지에 챕터 번호가 디스플레이 되지 않는다.

섹션은 챕터의 하위 요소이다. 다음 코드에서, "This is Section 1 in Chapter 1." 이라는 제목의 섹션을 만들었다. 이 섹션에 텍스트를 추가하기 위해 또 다른 단락 객체인 someSectionText를 만들고 이를 섹션 객체에 추가한다.


Listing 5. 섹션 객체의 생성

				
Paragraph title11 = new Paragraph("This is Section 1 in Chapter 1", 
           FontFactory.getFont(FontFactory.HELVETICA, 16, \
           Font.BOLD, new Color(255, 0, 0)));
Section section1 = chapter1.addSection(title11);
Paragraph someSectionText = new Paragraph("This \
text comes as part of section 1 of chapter 1.");
section1.add(someSectionText);
someSectionText = new Paragraph("Following is a 3 X 2 table.");
section1.add(someSectionText);

테이블을 추가하기 전에 문서가 어떤 모습인지를 보자. 다음 두 줄을 추가하여 문서를 종료하고 프로그램을 컴파일 및 실행하여 PDF 문서를 만든다. document.add(chapter1);document.close();.


그림 2. 아웃풋
Sample output of chapter

이제 테이블 객체를 만들어 보자. 테이블에는 열과 칼럼의 매트릭스가 포함된다. 한 열의 셀은 한 개 이상의 칼럼으로 확장될 수 있다. 마찬가지로, 한 칼럼에 있는 셀은 한 개 이상의 열로 확장될 수 있다. 따라서, 3 x 2 테이블은 정확히 6 개의 셀만 가질 필요가 없다.


Listing 6. 테이블 객체의 생성

				
Table t = new Table(3,2);
t.setBorderColor(new Color(220, 255, 100));
t.setPadding(5);
t.setSpacing(5);
t.setBorderWidth(1);
Cell c1 = new Cell("header1");
c1.setHeader(true);
t.addCell(c1);
c1 = new Cell("Header2");
t.addCell(c1);
c1 = new Cell("Header3");
t.addCell(c1);
t.endHeaders();
t.addCell("1.1");
t.addCell("1.2");
t.addCell("1.3");
section1.add(t);

위 코드에서, t라는 테이블 객체를 만들었다. 세 개의 칼럼과 두 개의 열을 갖고 있다. 테이블의 보더 색상을 설정했다. 패딩(padding)은 셀의 텍스트와 셀의 경계간 공간을 만드는데 사용된다. 스페이싱(spacing)은 인접하는 셀의 경계들간 공간이다. 그런 다음, 세 개의 셀 객체를 만든다. 각각 다른 텍스트를 갖고 있다. 이들을 계속해서 테이블에 추가한다. 첫 번째 칼럼에서 시작하여 같은 열의 다음 칼럼으로 이동한다. 열이 완성되면 다음 셀이 다음 열의 첫 번째 칼럼에 추가된다. 셀은 t.addCell("1.1"); 같은 셀의 텍스트를 제공하여 테이블에 추가될 수 있다. 마지막으로 테이블 객체가 섹션 객체에 추가된다.

마지막으로 PDF 문서에 리스트를 추가해 보자. 리스트에는 많은 ListItem들이 추가된다. 리스트에는 번호가 붙을 수도 있고 붙지 않을 수도 있다. 첫 번째 인자를 TRUE로 전달하면 번호가 붙은 리스트를 만들어야 한다.


Listing 7. 리스트 객체의 생성

				
List l = new List(true, false, 10);
l.add(new ListItem("First item of list"));
l.add(new ListItem("Second item of list"));
section1.add(l);

지금까지 chapter1 객체에 모든 것을 추가했다. chapter1에 추가될 더 이상의 객체가 없기 때문에, chapter1을 주 document에 추가해야 한다. 샘플 애플리케이션을 통해 한 개의 문서를 완성했다.


Listing 8. 주 document에 챕터 추가하기

				
document.add(chapter1);
document.close();






샘플 애플리케이션 실행하기

  1. 샘플 애플리케이션, j-itextsample.jar(Download)을 다운로드 한다.
  2. 디렉토리에 j-itextsample.jar의 압축을 푼다. C:\temp에 추출하면, 소스와 클래스 파일은 C:\temp\com\itext\test에 배치된다.
  3. 명령어 프롬프트를 열고 디렉토리를 C:\temp로 변경한다.
  4. 명령어 프롬프트에 시스템의 classpath를 설정한다. 시스템의 classpath에 C:\temp\itext-1.3.jar를 포함시킨다. Windows®에서는, set classpath=C:\temp\itext-1.3.jar;%classpath%명령어를 실행한다.
  5. java com.itext.test.ITextTest 명령어로 애플리케이션을 실행한다.

이 프로그램은 C:\에 ITextTest.pdf 문서를 생성할 것이다. 아래 그림은 이 PDF 문서의 두 번째 페이지 모습이다.


그림 3. PDF 문서 모습
Screenshot of PDF document

결론

지금까지 PDF 생성을 위한 기본 요소들을 살펴 보았다. iText의 장점은 같은 엘리먼트의 신택스가 다른 유형의 라이터에도 사용될 수 있다는 점이다. 또한, 이 라이터의 결과는 콘솔( XML과 HTML 라이터), 서블릿의 아웃풋 스트림 (PDF 문서의 웹 요청에 대한 응답), 또는 다른 유형의 OutputStream 으로 리다이렉션 된다. 또한 iText는 응답은 갖지만 PDF, RTF, HTML, XML 등 다양한 응답 유형을 보이는 상황에서 편리하게 사용할 수 있다. iText에서는 워터마크를 만들 수 있고 문서를 암호화 할 수 있으며 아웃풋도 정확하다.

다운로드 :

2007/08/26 22:06 2007/08/26 22:06
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

JavaMail :: 인증을 요하는 메일 SMTP에 접속하는 방법

2007/08/22 16:33

서비 JAVA , , ,

얼마전 인증을 해야하는 SMTP에 연결해야 했는데 그 방법 때문에 매우 고민했었습니다.
Mail API에서 Authenticator는 제공을 하면서 ID와 PW를 설정하는 부분이 없어서
참 난감했었습니다. 영어 실력이 짧아서 관련 Document를 읽어보다가 못찾고
다만 API에 있는 Session 클래스의
getInstance(Properties prop, Authenticator auth) 메소드가 마음에 걸려
혹시나 하는 마음으로 만들어 봤었는데 잘 되더군요.
sendmail 과 qmail 및 윈도기반 smtp server 몇 곳을 테스트 해봤는데 잘 되었습니다.

클래스 전체 소스는 다음과 같습니다.

/*
* MyAuthenticator.java
* 2002.05.27.
* Hyunho Kim(falllove@ducc.or.kr)
*
* Copyrightⓒ 2002 FancyArts Corps. All rights reserved.
*/

/**
* SMTP Authenticator
*/
public final class MyAuthenticator extends javax.mail.Authenticator {

    private String id;
    private String pw;

    public MyAuthenticator(String id, String pw) {
        this.id = id;
        this.pw = pw;
    }

    protected javax.mail.PasswordAuthentication getPasswordAuthentication() {
        return new javax.mail.PasswordAuthentication(id, pw);
    }

}

사용하실 때는 session을 얻기 전에 프로퍼티에 mail.smtp.auth 항목을 추가하시고
MyAuthenticator의 인스턴스를 하나 만드셔서 인자로 넘기시면 됩니다.

Properties prop = new Properties();
prop.put("mail.smtp.host", "smtp_host_name");
prop.put("mail.smtp.auth", "true");

MyAuthenticator auth = new MyAuthenticator("smpt_id", "smtp_pw");

// 새로운 메일 세션
Session mailSession = Session.getInstance(props, auth);
// or Session mailSession = Session.getDefaultInstance(props, auth);

정말 간단하죠^^;; 정말 암 것도 없는데 전에 이것을 하기 위해서 그 많은 글을 뒤적일
때는 못찾겠더라구요. 전 만들긴 했었지만 실제 서비스는 sendmail에서
localhost relay를 허용해서 사용하고 인증없이 발송하고 있습니다.
2007/08/22 16:33 2007/08/22 16:33
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

벤더로 부터 독립된 JMS 솔루션 구현하기

2007/08/15 17:49

서비 JAVA ,


자바 메시징 서비스 (JMS) 사양은 자바 기반의 지점간 (point-to-point, P2P) 메시징과 publish/subscribe (P/S) 메시징에 대한 표준을 열거한다. Sun은 현재 12개의 라이선스 받은 JMS 구현자 목록을 가지고 있고 16개의 라이선스 받지 않은 구현자를 가지고 있다. 구조적으로 JMS는 JDBC API와 유사한데, 둘 다 적은 수의 클래스를, 그러나 많은 인터페이스 집합을 정의하기 때문이다. 이 인터페이스들은 구현되어져야 하고, 여기에 상응하는 구현들도 동일하게 행동할 것이다.

대부분의 데이터베이스의 경우 행동의 유사성은 JDBC 인터페이스 구현으로 끝난다. SQL 준수 수준과 독자적인 절차적 SQL 확장 (Oracle의 PL/SQL과 Sybase의 Transact-SQL등)의 차이로 인해 데이터베이스 서비스에 접근해서 이를 사용하기 위해 작성된 코드에 상당한 차이가 있을 수 있다.

JMS에서는 그렇지 않다. 최소의 노력과 이 글에서 내가 제시한 절차들을 따름으로서 여러분은 여러분의 JMS 클라이언트 코드가 여러분이 사용하고 있는 벤더 구현을 알지 못하게 만들 수 있다. 여러분이 JMS 메시지 처리에 대한 기본적인 이해를 가지고 있다고 가정하지만, 기본 개념과 용어를 간단하게 살펴보면서 시작해보자.

JMS 아키텍처

메시지를 보내고 받는 기본은 연결인데,이는 JVM 외부에 자원을 할당하는 책임을 가지고 있다. JMS 벤더는 보통 P2P 트랜잭션을 위해 적어도 하나의 QueueConnection 을, P/S 트랜잭션을 위해 TopicConnection 을 구현할 것이다. 이들 접속은 Session 을 제공하는데, 이것은 메시지의 전송과 수신을 관리하는 생성자이다.

P2P 트랜잭션 관리를 위한 기본 생성자는 QueueSenderQueueReceiver이고, P/S 트랜잭션 관리를 위한 생성자는 TopicSubscriber과 TopicPublisher이다. Topic 객체와 Queue 객체는 각 메시지의 수신지와 출처를 나타내는 특정 정보를 캡슐화한다. 이 계층이 그림 1에 나와 있다.


그림 1. JMS 클래스 계층
JMS class hierarchy diagram

request/response 지원 클래스 등의 다른 생성자와 애플리케이션 서버에 고유한 기능들은 JMS 표준 (참고 자료 )에 나와있다.

다른 유형의 연결 설정

연결은 JMS 서버와의 상호작용을 위한 진입점이기 때문에, 각 연결 인터페이스를 구현하려면 자체적인 JMS 서버의 인스턴스에 어떻게 연결하는지를 알아야 한다. 기반이 되는 연결 프로토콜의 상세사항은 각 벤더마다 다른 경우가 많기 때문에, 유효한 연결 설정에 필요한 정보도 각 벤더마다 다르다.

대부분의 벤더는 연결이 동적으로 설정될 수 있도록 한다. 즉 연결 클래스의 생성자를 공개적으로 정의하여, 프로그래머들이 필요한 연결 정보를 정의할 수 있도록 하고 있다. 대부분의 벤더는 호출에 대응해 연결을 반환하는 factory 클래스를 제공한다.

연결 factory의 경우 factory 클래스는 독자적인 연결 정보를 가지고 미리 로딩되어있는 연결을 반환할 수 있다. 벤더가 정의한 factory 클래스는 프로그래머가 접속 매개변수를 설정할 수 있도록 하는 메소드들을 보여 줄 것이며, 이 접속 매개변수들에는 factory가 반환하는 접속의 특성이 들어 있다.

벤더 API가 다른 부분

이 모두를 좀 더 구체화하기 위해 몇몇 QueueConnectionQueueConnectionFactory 구현 제품의 생성자, 접속 factory 및 설정 메소드들을 살펴보자. (일부 경우에는 오버로드된 생성자가 많이 있음에 유의한다.; 나는 각 경우에 대해 한 개씩만 보여주겠다.)

  • java.lang.String socketFactory: 소켓 factory의 클래스 명
  • java.lang.String hostname: JMS 서버의 호스트 명
  • int port: JMS 서버의 포트
  • long keepalive: 활동 주기

다음 코드는 swiftMQ QueueConnectionFactory 객체를 생성하는 방법이다. :


IIT SwiftMQ 2.1.3 QueueConnectionFactory 생성자 매개변수

 
QueueConnectionFactory qcf = (QueueConnectionFactory) new 
com.swiftmq.jms.ConnectionFactoryImpl
  ("com.swiftmq.net.PlainSocketFactory", "myhost",4001,60000);

  • java.lang.String brokerURL: URL (in the form [protocol://]hostname[:port])
  • java.lang.String connectID: 접속을 구별하기 위한 ID 문자열
  • java.lang.String username: 기본 사용자명
  • java.lang.String password: 기본 패스워드

다음은 Progress SonicMQ QueueConnectionFactory 객체를 생성하는 샘플 코드이다:


Progress SonicMQ 3.5 QueueConnection 생성자 매개변수


progress.message.jclient.QueueConnection queueConnection = new
progress.message.jclient.QueueConnection("tcp://myhost:2506", 
    "ServiceRequest", "username", "password");

우리가 살펴볼 마지막 예제는 IBM MQSeries 구현이다. MQSeries는 연결 생성자를 이용하지 않는다. 대신 여러분은 동적으로 연결을 생성하기 위해 연결 factory를 구축해야 하는데, 이는 연결을 제공하기 위한 메소드를 제공한다. 매개변수가 없는 생성자를 만들기 위한 코드는 다음과 같다.:


MQSeries (MA88)


MQQueueConnectionFactory = new
MQQueueConnectionFactory();

연결 factory의 생성자는 매개변수가 없기 때문에, factory는 factory가 제공할 연결의 특성들을 제어하기 위해 호출될 수 있는 돌연변이 메소드를 가지고 있어야 한다.:

  • setTransportType(int x): 다음 옵션 중 하나에 전송 유형을 설정한다. :
    • JMSC.MQJMS_TP_BINDINGS_MQ: MQSeries 서버가 클라이언트와 동일한 호스트에 있을 때 사용된다.
    • JMSC.MQJMS_TP_CLIENT_MQ_TCPIP: MQSeries 서버가 클라이언트와 다른 호스트에 있을 때 사용된다.
  • setQueueManager(String x): 큐 관리자의 이름을 설정한다.
  • setHostName(String hostname): 클라이언트만 해당됨, 호스트의 이름을 설정한다.
  • setPort(int port): 클라이언트 연결을 위한 포트를 설정한다.
  • setChannel(String x): 클라이언트에만 해당됨, 사용할 채널을 설정한다.

다음은 MQSeries QueueConnectionFactory를 생성하고 특정 큐 관리자로 접속하게 하는 샘플 코드이다. :


com.ibm.mq.jms.MQQueueConnectionFactory factory = new 
com.ibm.mq.jms.MQQueueConnectionFactory();
factory.setQueueManager("QMGR");
com.ibm.mq.jms.MQQueueConnection connection = 
  factory.createQueueConnection();

벤더에 독립적인 코드 생성에서의 JNDI의 역할

간략하게 살펴보았듯이, 각 벤더는 자체적인 별개의 연결 매개변수를 채택하고 있다. 그렇다면 이들 모두를 여러분 코드에 어떻게 투명하게 지원할 것인가? 표준 솔루션은 네이밍 서비스를 사용하여 사전 설정된 ConnectionFactory를 유지하는 것이다. 실행 시에 여러분의 코드는 ConnectionFactory를 검색하고 여기에서 반환되는 연결을 여러분의 JMS 서버에 투명하게 연결시킬 수 있을 것이다. 간단히 여러분의 네이밍 서비스에서 정확하게 구성된 연결 factory들만을 관리하면 되기 때문에 코드를 유지보수하고 재구축할 필요가 없어진다.

Java Naming and Directory Interface (JNDI)는 네이밍 서비스와 인터페이스하는 가장 일반적인 방법이다. JNDI는 구현되어야 하는 인터페이스 세트를 간단히 정의한다는 점에서 JMS와 유사하다. JNDI를 구현하는 모든 네이밍 서비스는 하나의 표준 API하에 접근된다.

JNDI는 벤더에 독립적인 코드를 작성하려는 우리 노력의 중심이다. 벤더에 독립적인 방식으로 네이밍 서비스에 접근할 수 있는 방법을 제공하기 때문이다. JNDI는 우리가 위 섹션에서 설명한 독자적 구현 제품에 관해 걱정하지 않고 네이밍 서비스에서 올바른 객체를 검색하기 위한 코드 작성에만 신경을 쓰도록 해준다.






JMS 서버로의 접속

연결 factory를 생성하고 이를 미리 구성하여 여러분의 네이밍 서비스에 결합시킴으로써, 여러분의 메시징 서비스에서 특정 벤더에 국한된 연결 매개변수를 감출 수 있다. 여러분의 코드가 관련되어 있는 한 여러분은 일반적인 javax.jms.Connection 객체를 사용하고 있다. 벤더 구현은 인터페이스 뒤에 숨겨진다.

JMS 사양은 관리자에 의해 생성되고 JMS 클라이언트가 사용할 구성 정보를 가지고 있는 객체를 JMS 관리 객체로 지칭하고 있다. 관리 객체는 JNDI에 의존적이지 않지만, JNDI 이름공간에서 결합되고 검색될 수 있음을 암시한다.

Listing 1과 Listing 2는 JMS 서버 (이 경우에는 SwiftMQ)에 접속하기 위한 두 가지 다른 방식을 보여준다. 벤더에 의존하는 코드를 이용하는 방식과 벤더에 독립적인 코드를 이용하는 방식이 그것이다.


Listing 1. 벤더에 의존적인 접속 방법


1.QueueConnectionFactory queueConnectionFactory = 
(QueueConnectionFactory) new 
com.swiftmq.jms.ConnectionFactoryImpl
  ("com.swiftmq.net.PlainSocketFactory", "localhost",4001,60000);
2.QueueConnection queueConnection = 
queueConnectionFactory.createQueueConnection();


Listing 2. 벤더에 중립적인 접속 방법

1.Properties p = new Properties();
2.p.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.swiftmq.jndi.InitialContextFactoryImpl");
3.p.put(Context.PROVIDER_URL,"smqp://localhost:4001");
4.ctx = new InitialContext(p);
5.qcf = (QueueConnectionFactory)ctx.lookup("MyQCF");
6.oQueueConnection queueConnection = 
queueConnectionFactory.createQueueConnection();

코드를 나누는 방법

여러분들은 우선 벤더에 독립적인 코드가 몇 행을 더 가지고 있다는 점을 알아차릴 것이다. 이는 우리가 네이밍 서비스에 접속해야 하기 때문이다. 그러나 전체 프로그램에서 한번만 네이밍 서비스에 접속하면 된다는 사실을 염두에 두어야 한다. 따라서 추가적인 몇 행은 그 가치가 있다. (필요할 때마다 원격 context를 인스턴트화 하는 대신 네이밍 서비스를 재사용하도록 한다.)

네이밍 서비스와의 상호작용과 준비는 벤더에 중립적인 메시징 코드를 작성하는데 중요하다. 벤더에 의존적인 코드 예제에서 우리는 연결을 제공할 factory를 작성하기 위해 SwiftMQ 구현의 QueueConnectionFactory 생성자를 사용하였다. Listing 1의 1행에서와 같이, 이 구현을 위해 우리는 벤더에 독자적인 클래스를 포함시켜야 할 뿐 아니라 QueueConnectionFactory 생성자에게 특정 벤더에 국한된 매개변수도 보내야 한다.

벤더에 중립적인 예제에서, 특정 벤더에 국한된 코드는 없지만 우리는 초기 context factory와 네이밍 서비스를 제공하는 URL 뿐 아니라 QueueConnectionFactory의 바인딩 명도 알아야 한다. 바인딩 명의 경우 네이밍 서비스를 적절하게 유지하면 어떤 벤더가 제공하는 객체라도 여러분의 JNDI 트리에 바인딩시킬 수 있을 것이다. 따라서 벤더가 바뀌더라도 바인딩 명을 바꿀 필요가 없다. JNDI context의 경우 특성 파일에 매개변수 문자열 (Listing 2의 2행과 3행)을 저장하는 것이 일반적이다. 이렇게 하면 JMS 벤더가 바뀔 경우 간단히 특성 파일을 바꾸기만 하면 된다.

이 기법은 여러분에게 네이밍 서비스와 관련하여 유연성과 이식성을 제공한다는 점도 흥미로운 일이다. 많은 JMS 벤더 (Fiorano, SwiftMQ등)들이 자체적인 JNDI 서비스를 제공하지만, 여러분은 네이밍 서비스를 JMS 서비스에서 분리하고 싶어할 수도 있다. (예를 들어, 여러분은 여러분의 연결 factory를 중앙의 LDAP 서버에 저장하려 할 수 있다.)

다음은 각기 다른 JNDI 연결들을 가져올 특성 파일 엔트리의 예이다.:

  • java.naming.provider.url=smqp://myhost:4001
  • java.naming.factory.initial=com.swiftmq.jndi.InitialContextFactoryImpl

IBM WebSphere JNDI 서비스

  • java.naming.provider.url=iiop://myhost:9001
  • java.naming.factory.initial= com.ibm.websphere.naming.WsnInitialContextFactory

iPlanet 디렉토리 서버 (LDAP)

  • java.naming.provider.url=ldap://myhost:389
  • java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory

BEA WebLogic JNDI 서비스

  • java.naming.provider.url=t3://myhost:7001
  • java.naming.factory.initial=weblogic.jndi.WLInitialContextFactory

File System JNDI 서비스

  • java.naming.provider.url=file:/tmp/stuff
  • java.naming.factory.initial=com.sun.jndi.fscontext.RefFSContextFactory

여러분의 소스 코드는 벤더 클래스를 직접 참조하지 않을 수 있지만 벤더 클래스는 이름으로 JVM에 동적으로 로딩된다는 점에 주의하라. 따라서 실행 시에 여러분 프로그램의 클래스경로에 이들이 있어야 한다. JNDI와 JMS 클래스에서도 마찬가지이다.

특성 파일 설정하기

따라서 여기에서 우리는 JNDI 서비스에 연결하는 방법과 코드를 재컴파일하지 않고 상이한 JNDI와 JMS 구현으로부터 연결하는 방법을 알아야 한다. 특성 파일이 어떻게 설정되는지와 JNDI 연결에서 특성 파일이 수행하는 역할을 살펴보면서 지금까지 설명한 정보들을 종합해 보자.

JNDI 연결을 위한 기본 클래스는 javax.naming.InitialContext이다. InitialDirContext와 같이 디렉토리 작업에 국한된 InitialContext 의 하위 클래스도 있지만, 일반 클래스가 그 작업을 수행할 것이다. InitialContext 가 구축되면 환경 (시스템 특성이나 애플릿 매개변수)으로부터 JNDI 매개변수를 가져오거나 특정한 jndi.properites 파일을 찾는다.

이 작업은 J2SE 1.3.1 javadoc에 다음과 같이 설명되어 있다.:

JNDI는 다음 두 소스의 값을 차례로 합하여 각 특성의 값을 결정한다.

  1. 생성자의 환경 변수에서 처음 나타나는 특성과 (적합한 특성에 대한) 애플릿 매개변수와 시스템 특성

  2. 애플리케이션 자원 파일 (jndi.properties)

추가적인 특성들

지금까지 우리는 접속 제공자의 URL와 InitialContext factory 명(name)이라는 두개의 매개변수만 살펴보았다. 실제로는 훨씬 더 많은 특성 변수들이 제공될 수 있다. 우리가 살펴본 두 가지를 제외하고 가장 일반적인 것은 보안 처리된 JNDI store에 접근시 여러분의 신원을 확인해주는 사용자 명과 패스워드이다. 이 매개변수들은 다음과 같다.:

  • java.naming.security.principal (사용자명)
  • java.naming.security.credentials (패스워드)

파일 로딩하기

나는 여러분 애플리케이션의 모든 런타임 구성 매개변수를 하나의 애플리케이션 특성 파일에 두고 거기에 JNDI 매개변수를 포함시킬 것을 권한다. 모든 매개변수를 한 곳에 두면 불확실성이 없어진다. 그러면 여러분은 애플리케이션 특성 파일을 로드하기 위한 몇 가지 옵션을 가진다. 여기서는 두 가지 예를 들겠다. 파일은 자원 번들로 로딩될 수도 있고, 또는 명령행 매개변수로 특성 파일의 이름과 위치를 전달할 수도 있다.각 방식은 각기 다른 장점을 가지고 있다.

파일 위치를 명령행 매개변수로 전달하는 것이 여러분 코드를 구성하는 가장 쉬운 방법이다. 매개변수는 간단히 애플리케이션의 시동을 수정하여 변경시킬 수 있다.

자원 번들로 파일을 로딩하면 두 가지 이점이 있다.:

  • JVM의 위치에 따라 다른 자원 번들이 로딩될 수 있다. 예를 들어, application_en_US.properties 파일은 뉴욕의 JNDI 서비스를 가리키고, application_fr.properties 파일은 파리의 JNDI 서비스를 가리킨다.
  • 자원 번들에서 특성을 로딩하는 것은 아키텍처와 플랫폼에 독립적인 방법이다. 자원 번들은 클래스 경로로부터 로딩되기 때문에
    코드는 JVM의 명령행 매개변수를 읽을 수 있는지와 무관하다. 또한 EJB 컴포넌트와 같은 일부 컴포넌트들이 파일 입출력을 직접 이용하지 않기 때문에 자원 번들은 특성 파일의 내용을 로딩하는 더욱 편리한 방법을 제공한다.

상충되는 환경 설정에서 오는 혼란을 피하기 위해 나는 항상 내 특성 파일에서 읽은 JNDI 값으로 특성 인스턴스를 설정한다.

특성 파일 초기화 및 JNDI 검색

이 섹션에 나와 있는 코드는 특성 파일 초기화의 두 유형 (명령행 매개변수 및 자원 번들) 뿐 아니라 일반적인 JNDI 검색도 보여준다. 우선 Listing 3에 나와 있는 application_fr.properties라는 구성 파일 샘플을 살펴보자.:


Listing 3. PropertiesManagement.properties


java.naming.provider.url=smqp://localhost:4001
java.naming.factory.initial=com.swiftmq.jndi.InitialContextFactoryImpl
java.naming.security.principal=admin
java.naming.security.credentials=secret
com.nickman.neutraljms.QueueConnectionFactory=myQueueConnectionFactory
com.nickman.neutraljms.TopicConnectionFactory=myQueueConnectionFactory
com.nickman.neutraljms.Queue=testqueue@router1
com.nickman.neutraljms.Topic=testtopic

특성 파일 정의

이제 특성 파일을 읽기 위한 코드를 살펴보자. 앞에서 설명했듯이, 여러분은 JNDI 접속 매개변수를 결정하기 위한 두 가지 옵션을 가지고 있다. Listing 4는 JNDI 특성을 검색하기 위한 샘플 코드이다.:


Listing 4. JNDI 특성 조회하기


package com.nickman.jndi;
import javax.naming.*;   // For JNDI Interfaces
import java.util.*;
import java.io.*;
import javax.jms.*;
public class PropertiesManagement {
   Properties jndiProperties = null;
   Context ctx = null;
   public static void main(String[] args) {
     PropertiesManagement pm = new PropertiesManagement(args);
.
.
   public PropertiesManagement(String[] args) {
     jndiProperties = new Properties();
     if(args.length>0) {
       try {
         loadFromFile(args[0]);
.
.
     } else {
       try {
         loadFromResourceBundle();
.
.
   private void loadFromFile(String fileName) throws Exception {
     FileInputStream fis = null;
     try { 
       fis = new FileInputStream(fileName);
       jndiProperties.load(fis);
     } finally {
       try { fis.close(); } catch (Exception erx){}
     }
   }
   private void loadFromResourceBundle() throws Exception {
     String key = null;
     String value = null;
     ResourceBundle rb = 
       ResourceBundle.getBundle("PropertiesManagement");
     Enumeration enum = rb.getKeys();
     while(enum.hasMoreElements()) {
       key = enum.nextElement().toString();
       value = rb.getString(key);
       jndiProperties.put(key, value);
     }
   }

이 글에서의 작업시 참조하기 위해 전체 소스 코드 파일을 다운로드 받을 수 있다.

코드를 나누는 방법

Listing 4는 특성 파일을 두 가지 다른 방식으로 로딩하기 위한 코드를 보여준다. 명령행 매개변수가 전달되면 코드는 이것이 완전한 조건을 갖춘 특성 파일명이라고 가정하며, loadFromFile(String fileName) 메소드를 사용해 특성이 로딩된다.

클래스는 다음과 같이 호출된다. :


java com.nickman.jndi.PropertiesManagement 
c:\config\PropertiesManagement.properties

명령행 매개변수가 전달되지 않으면 코드는 loadFromResourceBundle() 메소드를 호출할 것이다. 이 메소드는 CLASSPATH에서 특성 파일을 찾는다. 따라서 클래스 경로에 이 파일의 디렉토리를 두어야 한다. 양 방식 모두에서 특성은 jndiProperties라는 특성 변수로 로딩된다.

I JNDI 서비스에 연결하기

Listing 5는 JNDI 서비스로의 연결을 보여준다. :


Listing 5. JNDI 연결


   public void connectToJNDI() throws javax.naming.NamingException {
     
     // jndiProperties was loaded from PropertiesManagement.properties
     ctx = new InitialContext(jndiProperties); 

     System.out.println("Connected to " + 
       ctx.getEnvironment().get(Context.PROVIDER_URL));
   }

위의 연결 코드는 아주 직접적이다. jndiProperties 변수는 InitialContext 생성자로 전달되고, 결과로 나오는 Context는 JNDI 서비스에 대한 일종의 "핸들"이다. javax.naming.Context 인터페이스가 사용 가능한 모든 환경 특성 변수들을 모두 표현하는 상수들의 셋트를 담고있다는 데 주의하는 것이 좋다.

구축된 Context로 우리는 Listing 6에서와 같이 JMS 객체 검색을 진행할 수 있다.:


Listing 6. JNDI 검색


   public QueueConnectionFactory lookupQueueConnectionFactory() 
        throws javax.naming.NamingException {
     return 
     (QueueConnectionFactory)ctx.lookup(jndiProperties.get
         ("com.nickman.neutraljms.QueueConnectionFactory").toString());
   }
   public Queue lookupQueue() throws javax.naming.NamingException {
     return 
     (Queue)ctx.lookup(jndiProperties.get
         ("com.nickman.neutraljms.Queue").toString());
   }

검색은 간단히 Context의 lookup(String name) 메소드를 호출하고, 우리가 원하는 객체가 바인딩될 이름으로 전달되는 과정이라고 볼 수 있다. 반환되는 객체는 올바른 클래스로 보내져야 하는데, 이 경우에는 표준 javax.jms 인터페이스중 하나가 될 것이다.

Destination 인터페이스

javax.jms.Destination 은 메시지가 전달될 특정 목적지를 캡슐화하는 인터페이스이다. QueueTopic 인터페이스는 모두 Destination 인터페이스를 확장한다. Destination 은 JMS가 관리하는 객체이기 때문에, Queues와 Topics도 마찬가지이다.

여러분은 JMS API가 Topic과 Queue 세션 클래스에 두개의 메소드를 가지고 있음을 알게 될 것이다.:

  • Topic TopicSession.createTopic(java.lang.String topicName)
  • Queue QueueSession.createQueue(java.lang.String topicName)

따라서 다음과 같은 질문을 던질 수 있다.: 하나의 문자열을 사용해 간단히 이를 참조할 수 있는데 왜 JNDI에 queue나 topic을 저장하는 수고를 해야 합니까? 그 이유는 아주 미묘하다.: 이를 이해하기 위해 javacdoc에서 다음을 인용하겠다.:

Destination 객체는 제공자(provider) 고유의 주소를 캡슐화한다. JMS API는 표준 주소 구문을 정의하지 않는다. 표준 주소 구문을 염두에 두더라도, 기존의 메시지 지향 미들웨어 (MOM) 제품들간의 주소 의미론간에 차이가 너무 커서 하나의 구문으로는 해결할 수 없다는 결론이 내려졌다.

Destination 은 관리되는 객체이기 때문에 자신의 주소 외에 제공자 고유의 구성 정보를 포함하고 있을 수 있다.

간단히 말해, 이것은 우리가 특정 JMS 제공자에 관한 상세사항을 JNDI에서 이름 공간을 검색하기 위해 사용되는 간단한 이름 뒤에 숨길 수 있다는 것이다. 나는 또한 JMS 클라이언트와 실제 JMS 목적지간에 중간층을 두면 아키텍쳐상에 추가적인 유연성이 주어진다는 것을 알았다. 클라이언트 코드는 myQueue라고 불리는 JNDI내의 이름공간을 참조할 수 있지만 관리자는 모든 벤더의 큐 수신지가 될 수 있는 객체를 그 이름공간에 할당할 수 있다. 이 개념이 그림 2에 나와 있다.


그림 2. 수신지 유연성
Destination Diagram





골치 아픈 topics

JMS의 publish-and-subscribe 프레임워크는 벤더에 중립적이 되려는 우리의 작업을 왜곡시키는 몇 가지 기능을 정의한다. 많은 JMS 서버가 계층적 이름 공간이라는 개념을 지원하는데, 이는 P/S 메시지가 하나의 계층으로 분류될 수 있게 한다. 토픽 구독 클라이언트가 JMS 서버에 접속하면 계층의 특정 부분에 해당하는 메시지를 요청할 수 있다. 그림 3에 설명된 계층을 예로 살펴보자.:


그림 3. 샘플 계층
Sample Hierarchy

이를 좀 더 잘 이해하기 위해 예제 시나리오를 사용해보자. 가령 구독자 클라이언트가 서비스 내에 있는 모든 미국 주가 정보를 구독하고 싶어한다고 가정해보자. 정적인 구독의 경우 클라이언트는 미국 주가를 구독하기 위해 사전 구성된 topic을 나타내는 JMS 관리 객체를 간단히 검색하기만 하면 된다. JMS 벤더들마다 계층적인 구독을 표현하기 위해 다른 구문을 사용하기 때문이 이것이 이상적인 방법이 될 것이다. 그리고 이를 JNDI가 검색하는 객체 뒤로 숨김으로써 클라이언트는 기반이 되는 JMS 구현을 알지 못할 것이다.

그러나 수백개의 다른 구독 "cell"을 제공하며 50개의 다른 단계를 포함하고 있는 한 계층을 생각해보자. 또한 그 계층이 동적이며 관리자가 이를 지속적으로 늘린다고 생각해보자. 그런 경우 JMS 관리자가 가능한 모든 구독을 표시하는데 필요한 JMS 관리 객체를 모두 생성하는 것은 불가능할 것이다. 더구나 계층 선택은 클라이언트 애플리케이션을 지원하도록 유연하고 동적이어야 한다.

이런 상황에서는 topic 명이 실행 시에 정의되도록 하는 것이 더 적합할 것이다. 문제는 계층을 표현하기 위해 사용되는 구문이 벤더마다 틀리다는 점이다. 다음 코드들은 세 개의 다른 벤더가 사용하는 구독 구문들이다.


MQSeries JMS


Topic topicEqUs = topicSession.createTopic("topic://Prices/Equity/US");
Topic topicEqAll = topicSession.createTopic("topic://Prices/Equity/*");


SonicMQ 3.5

Topic topicEqUs = topicSession.createTopic("Prices.Equity.US");
Topic topicEqAll = topicSession.createTopic("Prices.Equity.*");


SwiftMQ 2.1.3

Topic topicEqUs = topicSession.createTopic("Prices.Equity.US");
Topic topicEqAll = topicSession.createTopic("Prices.Equity.%");

MQSeries JMS 구현은 다른 두개의 구현과 다른 topic 구분자를 채택했다는 점에 주의한다. (거의 모든 다른 벤더들은 마침표를 사용하는데 MQSeries는 사선 표시를 사용한다.)

위에서 설명한 문자들과 별도로, topic 명에 나타날 수 있는 벤더마다 고유한 문자열들이 있다. 예를 들어, SonicMQ는 상위 계층을 구분하기 위해 파운드 기호를 사용하고, MQSeries는 보통 API용으로 남겨둔 옵션을 표현하기 위해 topic명에 접미사를 붙인다. 거의 모든 JMS 벤더들이 다른 와일드카드를 채택하고 있기 때문에 실행 시에 토픽 명을 일률적으로 정의하는 것이 어렵다. 다행히도 이 문제에 대한 해결방법이 있다. 모든 특수 문자를 하나의 참조 소스에 저장하고 이 소스에서 문자들을 로드하여 실행 시에 사용하는 것이다.

참조 소스는 여러분의 애플리케이션 특성 파일이 될 수도 있고 JNDI 서비스가 될 수도 있다. 이를 보여주기 위해 Listing 7과 같이 우리의 PropertiesManagement.properties 파일에 topic 문자를 추가할 것이다:


Listing 7. topic 문자가 추가된 PropertiesManagement.properties


#Topic Delimiter For  Sonic and Swift
com.nickman.neutraljms.TopicDelemiter=.
#Topic Delimiter for MQSeries
#com.nickman.neutraljms.TopicDelemiter=/
#Topic Wild Card For Sonic and MQSeries
com.nickman.neutraljms.TopicWildCard=*
#Topic Wild Card For Swift
#com.nickman.neutraljms.TopicWildCard=%
#Topic Prefix For MQSeries
#com.nickman.neutraljms.TopicPrefix=topic://
#Topic Prefix For All Others
com.nickman.neutraljms.TopicPrefix=

이들 엔트리가 추가되어 우리의 topic 구독 코드가 다음과 같이 되면 이 코드는 벤더에 독립적으로 될 것이다:


Listing 8. Topic 구독 코드


String delim = 
jndiProperties.get("com.nickman.neutraljms.TopicDelemiter").toString();
String wildcard = 
jndiProperties.get("com.nickman.neutraljms.TopicWildCard").toString();
Strung prefix = 
jndiProperties.get("com.nickman.neutraljms.TopicPrefix").toString();
Topic topicEqUs = topicSession.createTopic("Prices" + delim + 
"Equity" + delim + "US");
Topic topicEqAll = topicSession.createTopic("Prices" + delim + 
"Equity" + delim + wildcard);

일단 여러분이 Listing 7에 나타난 외부 설정을 끝내고 나면 여러분은 여타의 벤더 고유 옵션을 처리하도록 이를 확장할 수 있다. 예를 들어, JMS 사양은 한 세션이 구현할 수 있는 두 개의 배달 (delivery) 모드를 정의하지만, Sonic MQ는 세 개의 배달모드를 추가적으로 더 지원한다. 배달 모드를 외부에서 정의함에 따라 여러분은 Sonic MQ의 독자적 확장자를 구현하는 한편 여러분 코드의 벤더 중립성을 유지할 수 있다.


원문 출처 : http://www.ibm.com/developerworks/kr/library/j-jmsvendor/

2007/08/15 17:49 2007/08/15 17:49
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

ANT 를 이용한 RMI 컴파일 ( rmic 로 stub 생성하기 )

2007/08/03 11:36

서비 JAVA , ,

Ant를 이용하여 RMI 빌드를 위한 스크립트 이다.

(Language : xml)
<!-- RMI 생성 -->
<target name="rmi" description="RMI Stub 생성" depends="make">
<echo message="* rmic 실행  *"></echo>
 <rmic base="./bin/classes" classname="com.prompt.rfid.rmi.CallableFunctionImpl" classpath="./bin/classes" stubversion="1.2">
   <classpath>
     <fileset dir="${jar.dir}">
      <include name="**/*.jar" />
     </fileset>
     <fileset dir="${lib.dir}">
        <include name="**/*.jar" />
       </fileset>
     <fileset dir="${classes.dir}">
      <include name="**/*.class" />
     </fileset>
   </classpath>
 </rmic>
</target>

다른부분은 특별히 언급 할 필요는 없을 듯 하다.
<rmic .. > 태그의 속성만 설명 하자면
base : rmic로 컴파일된 skel 과 stub이 위치할 경로 지정
classname : rmic 로 stub을 생성할 원격 객체를 지정
                 ( 소스를 지정하는게 아니라 javac에 의해 컴파일된 class 파일을 지정 한다. )
classpath : classname에 지정한 원격 객체 class파일이 위치하고 있는 경로
stubversion : 1.1 과 1.2 중 하나를 선택 할 수 있다. ( http://www.yunsobi.com/blog/61 참조 )
                   1.1을 지정하면 jre 1.3 이전의 방식대로 skel 과 stub 을 모두 생성 한다.
                   1.2를 지정하면 jre 1.4 이후 방식으로 sutb 만 생성한다.
2007/08/03 11:36 2007/08/03 11:36
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

컴포넌트 시스템과 클래스 로더 경계 :: Java Class Loader

어느 곳에서나 소프트웨어 개발자들은 단일 애플리케이션 개발 전략을 넘어서서 상호작용(interoperating) 애플리케이션 시스템 개발로 나아가고 있다. 현재 엔터프라이즈 개발자들이 당면하고 있는 문제들 중 하나는 최근의 WAR 와 EAR 파일 등 Web and Enterprise Archives 같은 단일 애플리케이션 디플로이먼트 구조에서 느슨하게 결합된 애플리케이션 컴포넌트 시스템으로 어떻게 옮겨갈 것인가 이다. 개발자들은 J2EE 플랫폼의 기본 기능과 호환됨과 동시에 코드 재사용을 늘리고 애플리케이션 복잡성을 줄이기 위해 컴포넌트 기반 애플리케이션 전략을 사용하길 원한다. JARs, WARs, EARs 등과 같이 플러그할 수 있는 컴포넌트 아카이브를 개발하는 것이 이 전략의 관건이 될 것이다.

그러나 J2EE에서의 컴포넌트 기반 애플리케이션 시스템 개발과 배포를 둘러싼 많은 함정이 도사리고 있다. 중요 함정 중 한가지는 Java 버추얼 머신에서 클래스 로더의 경계를 교차하여 기능하기 위한 컴포넌트 시스템을 얻는데 따르는 어려움이다.

클래스 로더는 환경 구획의 의미로 생각하면 된다. 명심해야 할 것은 클래스 로더는 하위 클래스 로더들을 생성할 수 있으며, 따라서 하위환경을 만들어낼 수 있다는 것이다. 이 하위 클래스 로더 환경의 클래스들은 윗 계급인 시스템 클래스 로더쪽을 볼 수 있지만 더 하위 계급의 클래스들은 볼 수 없다.

이런 종류의 다단계적 환경 구조는J2EE 서버 환경에서 매우 보편적이며 심지어 J2EE 기술을 둘러싼 다양한 규격에는 권장되고 있는 사항이다. 예를 들어 서블렛들은 각각 WAR 파일에 패키지화되어 있으며, 오버롤 시스템의 각각의 클래스 로더 컨텍스트에 디폴트값으로 주어져있다. 이는 WEB-INF/lib 디렉토리에 포함되어 있는 JAR 파일은 다른 웹 아카이브의 JAR 파일 안에 저장되어 있는 클래스들에 접근하지 못한다는 것을 의미한다. 단일 애플리케이션에 패키징하기 위해 WAR을 사용할 때 이 배열은 허용가능하나 올인원 전략을 넘어서서 좀 더 컴포넌트 기반 애플리케이션 아키텍처로 나아간다면 바로 문제가 되어버릴 것이다.

이런 문제들의 증상은 확실하지 않을 때가 많으므로, 클래스 로더 경계를 교차하여 컴포넌트를 전달하지 못한다는 것을 진단하기는 매우 어렵다. 예를 들어 웹 애플리케이션 아카이브의 서블렛들 사이로 전달시키고자 하는 Foo라는 이름의 오브젝트가 있다고 하자(어느 한 웹 아카이브 안의 서블렛에서부터 다른 웹 파카이브의 서블렛으로 전달됨을 의미). Foo컴포넌트를 위한 클래스 파일은 JAR 파일에 패키징되어 있으며 JAR 파일의 동일한 복사본이 각 웹 아카이브의 /WEB-INF/lib 디렉토리 안에 있다. 다음의 코드를 작성하자.

   /* This code runs in a servlet in WAR #1 */
   SystemScopeObjectCache cache = 
       SystemScopeObjectCacheFactory.getInstance();

   WARScopeFoo foo = new WARScopeFoo ();
   System.out.println(foo);
   cache.addToCache("myFooObject", foo);

   /* This code runs in a servlet in WAR #2 */
   SystemScopeObjectCache cache = 
       SystemScopeObjectCacheFactory.getInstance();
   Object o = cache.getFromCache("myFooObject");

   try {
     // the following throws a ClassCastException! 
     WARScopeFoo foo = (WARScopeFoo)o; 
        
   } catch(ClassCastException e) {
     e.printStackTrace();
   }

각 웹 아카이브의 서블렛에 접근가능한 WARScopeFoo 오브젝트를 위한 컴포넌트 캐시를 쉽게 생성할 수 있다. 그러나 만약 서블렛 A가 오브젝트의 인스턴스를 중앙 캐시로 전달하고, 다른 WAR의 서블렛 B가 캐시로부터 인스턴스를 꺼내 Object에서 WARScopeFoo로 전달하려한다면 시스템은 ClassCastException을 전달하게 될 것이다.

이 상황은 클래스 로더를 고려한 것이 아니라면 아무 의미가 없다. 서블렛 A 에서 참조하는 WARScopeFoo 클래스는 서블렛 B에서 참조하는 WARScopeFoo클래스와 전혀 연관 없는 다른 클래스 로더로부터 생성된다. 정확히 얘기하자면, 그 둘은 의도적으로 전혀 연관되어 있지 않다. 이것은 같은 서블렛 컨테이너에서 구동되는 웹 애플리케이션 간의 네임스페이스 보전성을 강화하기 위한 안전한 매커니즘이다.

클래스 로더가 충돌한다는 또 다른 신호는 시스템의 단일 클래스에서 다중 인스턴스를 찾았을 때이다. (단일 클래스에는 단 한 개의 클래스 인스턴스가 생성되어야 한다.) 기술적으로 단일 인스턴스는 그 클래스 로더 안에서만 독자적일 뿐이다. 따라서 클래스 로더 단계에서 단일성에 의지하는 것은 위험하다. 다음의 예제를 보자.

   /* MyServlet in WAR #1 */
   WARScopeSingleton cache = WARScopeSingleton.getInstance();
   WARScopeFoo foo = new WARScopeFoo ();
   cache.add("myFooObject", foo);
   System.out.println(cache.length()); //output is 1

   /* MyOtherServlet in WAR #2 */
   WARScopeSingleton cache = WARScopeSingleton.getInstance();
   System.out.println(cache.length()); //output is 0!

이 예제에서 코드는 단일 클래스에서의 오브젝트들을 저장한다. 그러나 단일 클래스에서의 스코프는 WAR 클래스 로더를 필연적으로 발생시킨다.

다행히 엔터프라이즈 Java 개발자들을 위해 이런 장애들을 극복할 몇 가지 방법이 있다. 첫번째 단계는 어떤 클래스 로더 경계가 늘 존재하는 것이 보장되는지 확인하고 그에 대한 전략을 계획하는 것이다. 모든 J2EE 환경에는 세 개의 클래스 로더 레벨이 구축되어 있다. 시스템 단계의 컨텍스트는 대부분 VM을 교차하며 J2SE와 J2EE 플랫폼의 클래스와 호환된다. 이것은 애플리케이션 서버가 자체적으로 구동하는 레이어이다.

다음 단계는 Enterprise Archive 컨텍스트인데, 이는 엔터프라이즈 애플리케이션의 모든 JAR와 WAR를 포함하고 있다.

마지막 단계는 Web Archive 컨텍스트이며, 이는 WAR 파일 /WEB-INF/classes디렉토리의 모든 파일과 /WEB-INF/lib 디렉토리의 모든 JAR 파일을 포함하고 있다. WAR 파일 안에 로딩된 모든 클래스가 상호 접근 가능함은 물론이고 EAR 파일과 System 클래스 로더의 클래스들과도 접근가능하지만 다른 WAR 파일 안에 로딩된 클래스들과는 접근될 수 없다.

따라서 웹 아카이브 간의 사용자 비즈니스 오브젝트를 공유하고자 한다면 오브젝트의 JAR 파일을 EAR 클래스 로더 컨텍스트에 위치시키고 WAR 파일의 /WEB-INF/lib 디렉토리는 피하는 게 좋다. 다음은 그 예이다.

   /* MyServlet in WAR #1 */
   EARScopeCache cache =EARScopeCache.getInstance();
   EARScopeFoo foo = new EARScopeFoo();
   cache.add("MyFooObject", foo);

   /* MyServlet in WAR #2 */
   EARScopeCache cache = EARScopeCache.getInstance();
   Object o = cache.get("myFooObject");
   EARScopeFoo foo = (EARScopeFoo)o; //SUCCESS!

이 작업의 이유는 각각의 서블렛은 상위 계급의 클래스 로더를 추구하고 WAR 클래스 로더 컨텍스트에 클래스들을 로딩하는 대신 같은 EARScopeCacheEARScopeFoo오브젝트를 찾는다는 데에 있다.

그러나, EAR 파일을 조합하는 것은 서블렛 규격의 참조 구현인 Tomcat 같은 어떤 컨테이너 에서는 옵션이 아니다. Tomcat은 EAR 파일에서 클래스들을 로딩하는 정보처리 기능이 없으며 따라서 다중 웹 아카이브에 클래스들을 교차할 수 있게 만들지 못한다. 그러나 Tomcat은 일반 클래스 로더를 보유하여 /common디렉토리의 모든 JAR 파일을 모든 웹 아카이브 컨텍스트위에 직접적으로 존재하여 접근가능한 클래스 로더 스페이스에 로딩한다.

생각해볼 수 있는 다른 기술로는 서로 다른 컴포넌트 하위 시스템 간에 데이터를 왕복전달하기 위해Java 배열을 사용하는 것이 있다. 두개의 애플리케이션으로부터의 컴포넌트가 바이트로 배열된 데이터를 저장할 수 있는 공유 장소가 있고, 버전 충돌이 잠재적으로 있다는 사실을 배제하면 이 전략은 빠르고 효과적이다.

원문 출처 : http://kr.sun.com/developers/techtips/e2004_0824.html#2

2007/07/22 13:39 2007/07/22 13:39
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

넷빈즈를 이용한 Open Office 플러그인 개발

아래는 넷빈즈 5.5와 Open Office 개발 플러그인을 통하여 Open Office플러그인의
개발 과정을 보여주는 스크린 캐스트입니다.

넷빈즈의 오픈오피스 플러그인은 OpenOffice.org APIs를 자바에서 이용할 수 있도록
idl파일과 Open Office 플러그인 개발에 필요한 클레스, 메소드를 생성 해 주며
플러그인 빌드시 오픈오피스 플러그인에 등록 절차까지 완료 해 줍니다.



스크린 캐스트 출처 : netbeans.org

2007/07/09 11:53 2007/07/09 11:53
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

문자열 오브젝트의 길이는?

여러분의 텍스트 스트링의 길이는 얼마나 되는가? 사용자 입력이 데이터 필드 길이 제한 조건에 부합하는지 확인하기 위해선 그 답이 필요하다. 데이터베이스의 텍스트 필드는 입력을 일정한 길이로 제한하는 경우가 많으므로 텍스트를 제출하기 전에 길이를 확인할 필요가 있다. 이유야 어쨌든, 누구든지 이따금씩 텍스트 필드의 길이를 알아야 할 때가 있다. 많은 프로그래머들이 String 오브젝트의 length 메소드를 이용하여 그 정보를 얻으며, 대개의 경우 length 메소드는 올바른 솔루션을 제공한다. 하지만 이것이 String 오브젝트의 길이를 결정하는 유일한 방법은 아니며, 언제나 올바른 것도 아니다.

자바 플랫폼에서 텍스트 길이를 측정하는 데 세 개 이상의 일반적인 방법이 있다.

  1. char 코드 유닛의 수
  2. 문자 또는 코드 포인트의 수
  3. 바이트의 수


char
유닛 수 세기

자바 플랫폼은 유니코드 표준 을 사용하여 문자를 정의하는데, 이 유니코드 표준은 한때 고정 폭(U0000~UFFFF 범위 내의 16비트 값)으로 문자를 정의했다. U 접두사는 유효한 유니코드 문자 값을 16진수로 표시한다. 자바 언어는 편의상 char 타입에 대해 고정 폭 표준을 채택했고, 따라서 char 값은 어떠한 16비트 유니코드 문자라도 표현이 가능했다.

대부분의 프로그래머는 length 메소드에 대해 잘 알고 있고 있을 것이다. 아래의 코드는 예제 문자열에서 char 값의 수를 세는데, 예제 String 오브젝트에는 자바 언어의 \u 표기법으로 정의되는 몇 개의 단순 문자와 기타의 문자가 포함되어 있다는 점에 유의하기 바란다. \u 표기법은 16비트 char 값을 16진수로 정의하며 유니코드 표준에서 사용되는 U 표기법과 유사하다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
System.out.printf("char count: %d\n", charCount);

length 메소드는 String 오브젝트에서 char 값의 수를 센다. 이 예제 코드는 다음을 출력한다.

char count: 7


문자 유닛 수 세기

유니코드 버전 4.0이 UFFFF 이상 되는 상당히 큰 수의 새로운 문자를 정의하면서부터 16비트 char 타입은 더 이상 모든 문자를 표현할 수 없게 되었다. 따라서 자바 플랫폼 J2SE 5.0버전부터는 새로운 유니코드 문자를 16비트 char 값의 쌍(pair)으로 표현하며, 이를 surrogate pair 라고 부른다. 2개의 char 유닛이 U10000~U10FFFF 범위 내의 유니코드 문자를 대리 표현(surrogate representation)하는데, 이 새로운 범위의 문자를 supplementary characters 라고 한다.

여전히 단일 char 값으로 UFFFF까지의 유니코드 값을 표현할 수는 있지만, char 대리 쌍(surrogate pair)만이 추가 문자(supplementary characters)를 표현할 수 있다. 쌍의 최초값 또는 high 값은 UD800~UDBFF 범위에, 그리고 마지막값 또는 low 값은 UDC00~UDFFF 범위에 속한다. 유니코드 표준은 이 두 범위를 대리 쌍을 위한 특수 용도로 배정하고 있으며, 표준은 또한 UFFFF를 벗어나는 대리 쌍과 문자 값 사이의 매핑을 위한 알고리즘을 정의한다. 프로그래머는 대리 쌍을 이용하여 유니코드 표준 내의 어떠한 문자라도 표현할 수 있는데, 이런 특수 용도의 16비트 유닛을 UTF-16 이라고 부르며, 자바 플랫폼은 UTF-16을 사용하여 유니코드 문자를 표현한다. char 타입은 이제 UTF-16 코드 유닛으로, 반드시 완전한 유니코드 문자(코드 포인트)가 아니어도 된다.

length 메소드는 단지 char 유닛만을 세기 때문에 추가 문자는 셀 수 없다. 다행히도 J2SE 5.0 API에는 codePointCount(int beginIndex, int endIndex) 라는 새로운 String 메소드가 추가되었다. 이 메소드는 두 인덱스 사이에 얼마나 많은 유니코드 포인트(문자)가 있는지 알려주는데, 인덱스 값은 코드 유닛 또는 char 위치를 참조한다. 식 endIndex - beginIndex 의 값은 length 메소드가 제공하는 값과 동일하며, 이 둘의 차이는 codePointCount 메소드가 반환하는 값과 항상 동일하지는 않다. 텍스트에 대리 쌍이 포함되어 있는 경우에는 길이 수가 확실히 달라진다. 대리 쌍은 하나 또는 두 개의 char 유닛으로 된 단일 문자 코드 포인트를 정의한다.

하나의 문자열에 얼마나 많은 유니코드 문자 코드 포인트가 있는지 알아보려면 codePointCount 메소드를 사용하도록 한다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
int characterCount = testString.codePointCount(0, charCount);
System.out.printf("character count: %d\n", characterCount);

이 예제는 다음 내용을 출력한다.

character count: 6

testString 변수에는 두 개의 흥미로운 문자가 포함되어 있는데, 그것은 '배움(learning)'이라는 뜻의 일본어 문자와 GOTHIC LETTER AHSA 라는 이름의 문자이다. 일본어 문자는 유니코드 코드 포인트 U5B66을 사용하며, 이는 동일한 16진수 char 값 \u5B66을 가진다. 고딕체 글자의 코드 포인트는 U10330이고, UTF-16에서 고딕체 글자는 대리 쌍 \uD800\uDF30이다. 이 쌍은 단일 유니코드 코드 포인트를 표현하므로 전체 문자열의 문자 코드 포인트 수는 7이 아니라 6이 된다.


바이트 수 세기

String 안에는 얼마나 많은 바이트가 있을까? 그 대답은 사용되는 바이트 기반 문자 세트에 의해 결정된다. 바이트 수를 알아보려는 이유 중 하나는 데이터베이스의 문자열 길이 제한 조건을 충족하기 위해서이다. getBytes 메소드는 유니코드 문자를 바이트 기반 인코딩으로 변환하며, 이는 byte[] 를 반환한다. UTF-8 은 바이트 기반 인코딩의 일종이지만, 모든 유니코드 코드 포인트를 정확하게 표현할 수 있다는 점에서 대부분의 다른 바이트 기반 인코딩과는 구별된다.

다음 코드는 텍스트를 byte 값의 배열로 변환한다.

byte[] utf8 = null;
int byteCount = 0;
try {
  utf8 = str.getBytes("UTF-8");
  byteCount = utf8.length;
} catch (UnsupportedEncodingException ex) {
  ex.printStackTrace();

System.out.printf("UTF-8 Byte Count: %d\n", byteCount);

타깃 문자 세트가 생성될 바이트의 수를 결정한다. UTF-8 인코딩은 단일 유니코드 코드 포인트를 1~4개의 8비트 코드 유닛(=1 바이트)으로 변환한다. 문자 a , b , c , d 는 모두 4 바이트만을 요구하는 한편, 일본어 문자(한국어도 같음)는 3 바이트로 변환된다. 또한 고딕체 글자는 4 바이트를 차지한다. 결국 다음의 결과가 나오는 것을 알 수 있다.

UTF-8 Byte Count: 11









그림 1. 무엇을 세느냐에 따라 문자열의 길이가 달라진다.


요약

추가 문자를 사용하지 않으면 length codePointCount 의 반환 값 간에는 차이점이 없다고 할 수 있다. 하지만 UFFFF 범위를 넘어서는 문자를 사용할 경우에는 길이를 결정하는 다양한 방법에 관해 알아야 할 필요가 있다. 예컨대 미국에서 한국으로 제품을 발송할 경우에는 십중팔구 length codePointCount 가 서로 다른 값을 반환하는 상황이 발생한다. 데이터베이스의 문자 세트 인코딩과 일부 직렬화 포맷은 UTF-8을 최선의 방법으로 권장되고 있는데, 이 경우에도 역시 텍스트 길이는 달라진다. 결국 길이를 어떻게 활용할 것인지에 따라 다양한 측정 옵션을 사용할 수 있다.


추가 정보

본 테크 팁 주제에 관한 자세한 내용을 보려면 다음 리소스를 참조할 것(영문)


2007/07/03 20:57 2007/07/03 20:57
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다