반응형
자바 웹 스타트(JWS)는 웹 기반으로 애플리케이션을 배포할 수 있지 않습니까? 그런데 사람들은 왜 CD-ROM으로 자바 웹 스타트(JWS) 애플리케이션을 배포하려고 할까요? 이유는 여러 가지입니다. 우선, 대규모 애플리케이션이라면 고속 광대역 회선으로도 설치 프로그램 전체를 다운로드하기가 만만치 않을 수 있습니다. 둘째로, 기업 보안 등의 이유로 인해 온라인 연결이 안 되는 데스크탑도 많고 또 인터넷에 액세스할 수 없는 시스템도 많습니다. 마지막으로, 그저 CD가 더 좋다는 사람들이 많기 때문입니다.

클라이언트 기업에서 광대역 회선이 거의 없는 지역을 비롯하여 전 세계에 자사의 애플리케이션을 배포해 달라고 요청합니다. 이 애플리케이션에는 수많은 제품에 대한 정보와 함께 상세한 도면과 다이어그램까지 들어 있습니다. 애플리케이션의 상당 부분은 이러한 정보로 구성되어 있으며 JVM을 포함한 전체 설치 용량은 40MB를 넘어섭니다. 또 이 회사에서는 무역 박람회 등에서도 홍보물과 함께 이 애플리케이션을 CD에 담아 배포하고 싶다고 합니다. 그러므로 CD 배포도 준비해야 합니다. 보통, CD 설치에는 여러 가지 상업적 설치 프로그램이나 오픈소스 설치 프로그램을 이용하게 됩니다. 그러나 자바 웹 스타트로 실행하게 되어 있는 애플리케이션은 보통의 설치 프로그램에서와 달리 특정 위치에 설치해야 하며 사용자가 재량을 발휘할 여지가 없습니다.

이 기사에서는 CD와 인터넷 양쪽 모두를 이용하는 애플리케이션 설치 단계에 대해 설명합니다. 설치 프로세스에 필요한 조건은 다음과 같습니다.

  1. 설치된 애플리케이션은 업데이트를 스스로 확인하고 JWS 캐시와 통합되어야 합니다.
  2. 기존 또는 최신 버전의 자바 없이도 설치 후 즉시 시스템에서 실행할 수 있어야 합니다.
  3. 설치된 애플리케이션에는 인터넷 연결이 필요 없습니다.
  4. 설치된 프로그램은 사용하기 쉬워야 하며 사용자 인터페이스는 간단해야 합니다.

애플리케이션 설치는 보통 일반적인 설치 프로그램을 사용하여 이루어집니다. 그러나 기존의 설치 프로세스에서는 효율성 제고를 위해 JWS를 인식하지 못하는 별도의 애플리케이션을 작성하는 경우가 많았습니다. 따라서 업데이트가 발표될 때마다 사용자는 새 버전을 다운로드하여 설치해야 했습니다. 이와 달리 JWS 애플리케이션은 업데이트된 구성요소만 다운로드하므로 프로세스의 효율성과 신뢰성이 훨씬 더 높습니다. 그러므로 이 기사에서는 JWS 애플리케이션 설치 프로그램에 대해서도 설명하고자 합니다.


JWS 프라이머

자바 웹 스타트가 있으면 JNLP 파일 링크를 통해 자바 애플리케이션을 시작할 수 있습니다. 이 JNLP 파일은 애플리케이션에 대한 진입점 또는 기본 메소드를 설명하는 한편 애플리케이션에서 사용할 리소스를 참조합니다.

JWS 애플리케이션을 시작하면 JVM이 필요한 리소스에 액세스하려고 시도하면서, 경우에 따라 리소스를 업데이트하고 파일을 캐시로 복사합니다. 그 뒤로 이 애플리케이션을 시작하면 JWS가 이 캐시부터 확인하므로 리소스 다운로드 단계를 건너뛸 수 있습니다. 클라이언트 시스템이 오프라인 상태이거나 서버에 연결할 수 없는 경우, JWS는 오프라인 모드로 애플리케이션을 실행합니다.

JWS 시작 파일(JNLP 파일)이 CD에 들어 있는 경우, JWS는 서버에 연결하여 새 파일을 다운로드하려고 시도합니다. 물론 클라이언트 시스템이 온라인 상태일 때 이렇게 하는 것은 CD를 이용한 파일 배포의 취지에 어긋나는 일입니다. 대신에 우리는 앞서 JWS가 애플리케이션을 로드한 것처럼 JWS 캐시를 업데이트할 방법을 찾아야 합니다.


JWS 캐시 업데이트

자바 5 버전의 JWS에는 잘 알려진 -import 옵션이 있습니다. 이것은 특정 위치의 JWS 애플리케이션을 캐시로 가져오는 옵션입니다.

이 위치에 있는 CD 이미지는 보통 웹 서버에 배치되는 것, 즉 JNLP 파일과 이 JNLP 파일에서 참조하는 리소스 및 .jar 파일 등의 사본에 불과합니다. 서블릿을 사용하여 JNLP를 서비스하는 경우, CD 이미지를 실행하려면 생성된 JNLP 파일의 완벽한 스냅샷이 필요합니다.

따라서 다음을 호출하여 JWS 캐시에 CD 이미지를 설치할 수 있습니다.

<JAVA_HOME>/jre/bin/javaws -codebase <CACHE_IMAGE> -import <CACHE_IMAGE>/<XXXX>.jnlp

여기서 <JAVA_HOME>은 새 JVM 또는 기존 JVM의 루트이고, <CACHE_IMAGE>은 CD에서 JWS 애플리케이션의 위치이며, <XXXX>는 애플리케이션 JNLP 파일의 이름입니다. 이 명령을 자동화하고 간단한 GUI로 래핑하는 방법은 나중에 설명하겠습니다.

캐시에 저장된 애플리케이션을 설치하는 동안 JWS는 애플리케이션 시작을 위한 단축키를 바탕 화면이나 메뉴에 설치할지 묻는 메시지를 표시합니다. JWS 설치가 완료된 후 JWS를 다시 호출하여 새로 설치한 애플리케이션을 시작할 수 있습니다.

<JAVA_HOME>/jre/bin/javaws -import <CACHE_IMAGE>/<XXXX>.jnlp

여기서 다시 한 번 CD를 사용합니다. 그러나 이번에는 JWS가 JNLP 파일에서 참조하는 설치 리소스를 사용하게 됩니다. 시스템이 인터넷에 연결되어 있을 때는 통상적인 방법으로 업데이트를 확인한 다음, 애플리케이션을 시작합니다. 네트워크 연결이 설정되지 않았다면 CD에 저장된 상태 그대로 애플리케이션이 시작됩니다.

다음 번에 이 애플리케이션을 시작하는 사용자는 메뉴 또는 바탕 화면의 단축키를 사용할 수 있으며 CD는 이제 필요 없습니다. 아니면 웹 페이지의 링크를 사용하여 애플리케이션을 시작할 수 있습니다. 이 링크는 동일한 URL/JNLP 파일 조합, 예를 들면 해당 웹 사이트에 있던 원래 버전을 가리키는 링크여야 합니다.


JVM 문제

지금까지 설명한 내용에는 허점이 하나 있습니다. 위의 명령을 실행하려면 JVM이 있어야 하는데, 드물기는 하지만 JVM이 설치되어 있지 않거나 시스템 경로에 기본값으로 설정되어 있지 않아서 별도의 조치를 통해 사용 가능한 JVM을 찾아야 하는 경우가 있습니다. 이와 함께 사용자가 CD를 넣으면 설치가 시작되고 그 과정에서 기존 JVM이 있는지 자동으로 확인해야 합니다. 따라서 JVM 확인 프로세스는 다음과 같이 이루어집니다.

  1. 설치 프로그램이 JVM을 확인합니다.
  2. 없으면 JVM을 설치합니다.
  3. 설치 프로그램이 시작되고 사용자 라이센스 정보가 표시됩니다.
  4. 대상 JVM(위 1과 다른 버전을 애플리케이션이 요구하는 경우, 해당 버전)을 설치합니다.
  5. JWS 캐시를 가져옵니다.
  6. JWS 애플리케이션을 시작합니다.

JWS -import 옵션을 사용하기 위한 최소 JVM 버전이 자바 5라는 사실 때문에 몇 가지 문제가 더 발생합니다. 다시 말해, JVM이 설치되어 있고 애플리케이션에 사용할 수 있는 상태이더라도 이 import 옵션을 지정하려면 그 이상의 최신 JVM이 필요하게 됩니다. 둘째, 가져오기 프로세스에는 시간이 약간 걸리는데 애플리케이션을 시작하려면 먼저 이 프로세스를 마쳐야 합니다. 이렇게 실행이 지연되면 일반적인 다른 설치 프로그램과 같은 성능을 얻기가 어려워집니다.

이러한 문제를 고려한 결과, 위의 단계를 자동으로 수행할 수 있는 맞춤 시작형 애플리케이션을 제작하기로 했습니다.


설치 프로그램 실행

설치 프로세스의 실제 과정은 대부분 JWS -import 명령으로 처리되므로, 이 시작 애플리케이션의 주된 임무는 적절한 명령으로 JVM을 찾아 시작한 뒤 사용자에게 작업의 진행 과정을 GUI로 알려주는 것입니다.


JVM 찾기

Windows에서는 시스템 레지스트리의 HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment 키 아래에서 JVM을 찾을 수 있습니다. 이 키는 값이 여러 개일 수 있으며, 따라서 시작 애플리케이션은 레지스트리 항목을 반복하여 점검하면서 사용 가능한 최신 버전의 JVM을 찾아냅니다.

다음 메소드는 최소 및 최대 버전 번호의 인수를 대입하여 JVM 경로를 찾으려고 시도합니다.


private String getInstalledPath(
 
int majorMin, int minorMin, int revMin,
 
int majorMax, int minorMax, int revMax )
 
{
   
String installedPath = null;
   
int latestVersion = 0;

   
String keyRoot =
         
"HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft" +
         
"\\Java Runtime Environment";
   
Vector results = getRegEntries( "\"" + keyRoot + "\" /s" );
   
int numEntries = results.size();
   
for ( int i = 0; i < numEntries; i++ ) {
   
String key = results.get( i++ ).toString();
   
int pos = key.indexOf( "Java Runtime Environment" );
   
if ( pos > 0 ) {
      pos
+= "Java Runtime Environment".length() + 1;
     
String version = key.substring( pos );
     
String parts[] = version.split( "[._]" );
     
int majorVersion, minorVersion, revision;
      majorVersion
= Integer.parseInt( parts[ 1 ] );
     
if ( parts.length > 3 )
        minorVersion
= Integer.parseInt( parts[ 2 ] );
     
else
        minorVersion
= 0;
     
     
if ( parts.length > 4 )
        revision
= Integer.parseInt( parts[ 3 ] );
     
else
        revision
= 0;
       
     
if ((( majorVersion == -1 ) ||
           
( majorVersion >= majorMin )) &&

         
(( majorVersion == -1 ) ||
           
( majorVersion <= majorMax ))) {
       
if ((( minorMin == -1 ) ||
             
( minorVersion >= minorMin )) &&
           
(( minorMax == -1 ) ||
             
( minorVersion <= minorMax ))) {
         
if ((( revMin == -1 ) ||
               
( revision >= revMin )) &&
             
(( revMax == -1 ) ||
               
( revision <= revMax ))) {
           
// Prefer the neweset acceptable version
           
int thisVersion = majorVersion * 10000 +
                minorVersion
* 100 + revision;
           
if ( thisVersion > latestVersion ) {
             
String value = null;
             
while ( i < numEntries ) {
                value
= results.get( i++ ).toString().trim();
               
if ( value.startsWith( "JavaHome" ))
                 
break;
             
}
             
              installedPath
= value.substring(
                value
.indexOf( "REG_SZ" ) +  6 ).trim();
              latestVersion
= thisVersion;
           
}
         
}
       
}
     
}
   
}
 
}

 
return installedPath;
}

위 메소드의 키는 레지스트리 항목을 찾기 위한 키입니다. 레지스트리 값을 찾아주는 API는 여러 가지 있지만, 이 상황에서 가장 실용적인 것은 간단한 명령행 REG QUERY <key>입니다. 여기서 <key>는 쿼리할 레지스트리 경로입니다. 다음 메소드는 명령을 실행한 다음, 출력된 스트림 항목을 읽어 이를 Vector로 반환합니다.


private Vector getRegEntries( String key )
{
 
Vector results = new Vector();

 
try {
   
Process proc = Runtime.getRuntime().exec( "REG QUERY " +
      key
);
   
InputStream is = proc.getInputStream();
   
InputStreamReader isr = new InputStreamReader(is);


   
BufferedReader br = new BufferedReader(isr);
   
String result = "";
   
String line;

   
while (( line = br.readLine()) != null ) {
      line
= line.trim();
      results
.add( line );
   
}

   
return results;
 
}
 
catch ( Exception ex ) {
    message
( language.getString("6") + ex.getMessage() );
    ex
.printStackTrace();
 
}

 
return null;
}

JVM 설치

적당한 JVM을 찾을 수 없으면 CD에서 설치하면 됩니다. 단, 그 위치를 알고 있어야 합니다. 시작 프로그램은 properties 파일을 사용하여 필요한 여러 가지 리소스를 파악한 다음, 이 메커니즘을 통해 JVM 설치 패키지로 보낼 수 있습니다. 여기서도 마찬가지로, JVM 설치를 시작하려면 올바른 명령을 실행할 수 있어야 합니다. 그러나 이번에는 프로세스의 실행 시간이 비교적 길기 때문에 완료될 때까지 조금 기다려야 합니다. 실행(exec)된 프로세스의 waitFor 메소드는 완료될 때까지 스레드를 대기시키는 한편 UI 스레드의 차단을 방지하기 위해 이를 SwingWorker 안에 중첩시킵니다.


private void installJre()
{
 
try {
   
final String javaInstall = (String)props.get(
       
"jre_installer" );
    status
.setText( language.getString("3") );

   
SwingWorker worker = new SwingWorker()
   
{
     
public Object construct()
     
{
       
try {
         
Process process = Runtime.getRuntime().exec(
            workingDir
+ File.separatorChar +
            javaInstall
);
          process
.waitFor();
          exitValue
= new Integer( process.exitValue());
       
}
       
catch ( Exception ex ) {
          exitValue
= new Integer( -1 );
          ex
.printStackTrace();
       
}

       
return exitValue;
     
}

     
public void finished()
     
{
       
int ev = exitValue.intValue();
       
if ( exitValue != 0 ) {
          status
.setText( language.getString("Error:_") +
            exitValue
);
          message
( language.getString("4") );
       
}
       
else {
          installedJrePath
= getInstalledPath( 5, -1, -1,
                                               
5, -1, -1 );
          doInstall
( installedJrePath );
       
}
        status
.setText( "" );
     
}
   
};
    worker
.start();
 
}
 
catch ( Exception ex ) {
    status
.setText( language.getString( "5" ));
    ex
.printStackTrace();
 
}
}

JWS 시작 후 기다리기

JVM을 찾았으면 적절한 명령으로 이를 시작할 수 있습니다. JWS를 호출할 때, 첫 번째 가져오기 호출은 복사 및 완료하는 데 상당한 시간이 걸릴 수 있으므로 위와 같이 waitFor 메소드를 다시 사용합니다. 그러나 두 번째 호출부터는 애플리케이션을 시작하기만 하면 자동으로 처리됩니다. 일단 애플리케이션이 시작되면 시작 애플리케이션은 임무를 다한 것이므로 종료됩니다.


private void launchWebStart( String javaWSPath,
                             
String jnlpPath,
                             
String userDir )
{
 
try {
   
String webStartCommand = "\"" + javaWSPath + "\"" + " -wait
      -codebase file:///"
+ userDir + "\\"+ appDirectory +
     
" -import " + jnlpPath;
   
Process process = Runtime.getRuntime().exec(
      webStartCommand
);
    process
.waitFor();
   
int exitValue = process.exitValue();
   
if ( exitValue != 0 )
      status
.setText( language.getString("7") );
   
else
      status
.setText( language.getString("8") );


   
int rc = JOptionPane.showConfirmDialog( null,
      language
.getString("9"),
      language
.getString("10"),
     
JOptionPane.YES_NO_OPTION );
   
if ( rc == JOptionPane.YES_OPTION )
     
Runtime.getRuntime().exec( javaWSPath + " -offline " +
        jnlpPath
);

    status
.setText( language.getString("11") );
   
SwingWorker worker = new SwingWorker()
   
{
     
public Object construct()
     
{
       
try {
         
Thread.currentThread().sleep( 3000L );
       
}
       
catch ( Exception ex ) {
       
}

       
return null;
     
}

     
public void finished()
     
{
       
System.exit( 0 );
     
}
   
};
    worker
.start();
 
}
 
catch ( Exception ex ) {
    status
.setText( language.getString("12") );
    ex
.printStackTrace();
 
}
}


패키지 작성

설치를 위한 메커니즘은 모두 처리했습니다. 이제 시작 프로그램은 간단한 UI(아래 그림 1 참조)를 사용하여 사용자에게 진행 상황을 알려야 합니다.

사용자 삽입 이미지
그림 1. 설치 프로그램 시작 페이지(큰 그림으로 보려면 이미지 클릭)

UI는 현지화되어 있으며 로고, 애플리케이션 위치, JVM 버전 등을 config.properties 파일에서 구성할 수 있습니다. 소스를 포함한 전체 애플리케이션은 Aria project에서 오픈소스 라이센스로 구할 수 있습니다.

시작 애플리케이션을 제작하고 테스트까지 마친 뒤에는 완벽한 CD 설치 이미지를 작성하는 마지막 단계 하나만 남습니다. 그것이 바로 CD를 넣었을 때 설치를 시작하는 autorun 기능입니다. Windows용 autorun.inf 파일을 작성하는 방법에 대한 자세한 내용은 위키피디아의 Autorun 항목을 참조하십시오. 그러나 이 기능에는 원시 실행 파일이 필요합니다. 이러한 실행 파일을 작성하려면 Launch4J 래퍼를 사용하면 됩니다. Launch4J의 샘플 구성 파일은 다운로드한 소스에 포함되어 있습니다. Launch4J를 실행하면 해당 애플리케이션의 .exe 파일이 작성됩니다. 위에서 설명한 시작 애플리케이션은 자바 애플리케이션이고 사용자 시스템에는 JVM이 없을 수도 있으므로 여기서도 JVM을 래퍼와 함께 번들로 묶어야 합니다.

이렇게 .exe 파일을 작성한 다음, autorun.inf 파일을 작성하여 CD 이미지에 추가하면 됩니다.


[autorun]
open
=XXXX.exe
icon
=xxxx.ico
action
=Open XXXX
label
=My Application


플랫폼 간 문제

위에서 설명한 애플리케이션 시작 프로그램은 Windows에만 있는 몇 가지 기능을 사용하며, 따라서 Windows에서만 실행됩니다. 그러나 이 기사에서 설명한 방법은 여러 플랫폼에서 응용할 수 있는 방법이며, 다른 플랫폼을 사용하는 경우에는 기본 시작 프로그램을 사용할 수 있는지 여부만 확인하면 됩니다. 예를 들어, IzPack에는 몇 가지 플랫폼용의 시작 프로그램이 포함되어 있습니다. 또한 Mac OS X 처럼 문제의 플랫폼에 JVM이 있는 것이 확실하면 JVM에서 직접 시작 애플리케이션을 실행할 수 있으므로 기본 시작 프로그램은 필요가 없습니다.


결론

자바 웹 스타트와 Launch4J를 조합하여 CD를 작성하면 무역 박람회의 관람객 등에게 이 CD를 배포하여 빠르고 쉽게 애플리케이션을 설치하도록 할 수 있습니다. 사용자는 자바 웹 스타트의 전체 업데이트 기능을 사용하여 손쉽게 업데이트를 구할 수도 있고, 인터넷 연결이 불가능한 경우에는 애플리케이션 자체를 이용할 수도 있으므로 양쪽의 장점만 취하는 방법입니다.


참고 자료

Luan O'Carroll은 소프트웨어 개발자로서 Aria 프로젝트에 선임 연구원으로 참여하고 있습니다.


이 글의 영문 원본은
Distributing a Java Web Start Application via CD-ROM
에서 보실 수 있습니다.

크리에이티브 커먼즈 라이센스
Creative Commons License

<출처: http://www.sdnkorea.com/blog/638 >

+ Recent posts