주요글: 도커 시작하기
반응형
클래스로더를 이용하여 어플리케이션의 라이브러리를 캐싱하는 기술에 대해서 살펴본다.

애플릿과 캐싱(Caching)

지난 3회까지의 아티클에서는 클래스로더가 무엇이며, 어떻게 동작하는 지 그리고 커스텀 클래스로더를 작성하는 방법에 대해서 알아보았다. 이번 4번째 아티클에서는 자바 애플릿이 사용하는 클래스 라이브러리를 로컬 시스템에 캐싱한 후 클래스로더를 사용하여 캐싱한 라이브러리를 사용하는 방법에 대해서 살펴보도록 하자.

지금은 플래쉬와 DHTML과 같은 기술덕에 애플릿이 많이 사용되고 있지는 않지만, 인트라넷 환경을 구축하거나 ASP(Application Service Provider)를 구현하는 경우에 애플릿은 여전히 매력적인 솔루션 중의 하나이다.

현재 자바 애플릿을 이용하여 ASP 서비스를 제공하는 대표적인 경우가 바로 넷피스(http://www.netffice.com)이다. 넷피스는 최초로 실행할 경우 필요한 모듈을 로컬 시스템에 캐싱한다. 애플릿의 특징 중의 하나는 동적으로 필요한 클래스를 서버로부터 다운로드 받아 클라이언트 웹 브라우저에서 애플릿을 실행하는 것인데, 캐싱의 경우는 이러한 애플릿의 특징에 역행한다고 할 수 있다. 하지만, 캐싱을 하게 되면 다음과 같은 장점을 얻을 수 있게 된다.

  • 애플릿이 필요로 하는 모듈의 다운로드 시간을 줄일 수 있다.
애플릿이 필요로 하는 클래스 파일(또는 Jar나 Zip 파일)을 다운로드 하는 시간을 없앨 수 있다는 것은 곧 애플릿의 수행속도가 그 만큼 빨라진다는 것을 의미한다. 하지만, 캐싱을 할 때에는 캐싱되어 있는 데이터가 변경될 경우 그 변경된 내용을 캐시에 반영헤 주어야 한다. 즉, 애플릿이 필요로 하는 모듈이 변경될 경우 캐시를 위한 저장소에 저장되어 있는 모듈을 변경해줄 수 있는 방법이 필요하다.

로컬 시스템에 저장되어 있는 모듈이 변경되었는 지의 여부를 판단하는 방법에는 여러 가지가 있을 수 있으나, 그 중 가장 손쉬운 방법은 버전을 사용하는 것이다. 일반적으로 버전은 나중에 나온 버전에 기존에 있던 버전보다 더 큰값을 갖는다. 예를 들어, 많은 개발자들이 선호하는 에디터인 울트라 에디터는 6.0이나 7.0과 같이 숫자를 사용하여 버전을 표시하며, 울트라 에디터 뿐만 아니라 대부분의 어플리케이션이 숫자를 사용하여 버전을 표시하고 있다. 이처럼 숫자로 버전을 표시하게 되면 변경되었는지의 여부를 쉽게 판단할 수 있다는 장점이 있다.

애플릿은 이러한 버전을 사용하여 현재 로컬에서 사용되고 있는 모듈의 변경여부를 판단할 수 있게 되며, 따라서 버전이 변경되었을 경우(또는 버전이 높아졌을 경우) 서버로부터 가장 최신의 모듈을 읽어올 수 있게 된다.

애플릿과 클래스로더 생성 그리고 보안

로컬 시스템에 캐시되어 있는 모듈(Jar 파일 또는 Zip 파일)로부터 필요한 클래스를 로딩하기 위해서는 URL로부터 클래스로더를 읽어올 수 있는 java.net.URLClassLoader 클래스를 사용해야 한다. 하지만, 애플릿은 새로운 클래스로더를 생성할 수 없도록 제한되어 있다. 또한, 애플릿은 로컬 파일시스템으로부터 파일을 읽을 수 없고 로컬 파일시스템에 파일을 저장할 수도 없다.

이러한 애플릿과 관련된 제약은 애플릿과 관련된 보안 문제 때문에 발생한다. 이러한 보안과 관련된 애플릿의 제약은 서명된(Signed) 애플릿을 통해서 해결할 수 있다. 현재 웹 브라우저는 웹 브라우저만의 인증 방식을 사용하고 있으며, 자바2 부터는 보안 관련 부분을 개발자가 세밀하게 설정할 수 있도록 하고 있다. 또한, 자바2 부터는 인증서를 사용하여 서명된 애플릿은 어플리케이션과 완전히 같은 권한을 갖도록 하고 있다. 특히, 자바 플러그인 1.3 부터는 RSA를 기반으로한 서명된 애플릿을 실행할 수 있도록 하고 있다. 이를 통해 좀더 쉽게 서명된 애플릿을 배포할 수 있게 되었다.

이 글은 클래스로더와 관련된 글이므로 보안과 관련된 문제에 대해서는 자세히 언급하지 않겠다. 서명된 애플릿과 관련된 내용이 궁금한 독자들은 관련 링크를 참조하기 바란다.

캐싱의 구현

애플릿에서 캐싱을 구현하는 것은 크게 다음과 같은 과정을 통해서 이루어진다.

  1. 로컬 파일 시스템에 모듈이 캐시되어 있는 지 검사한다.
  2. 캐시되어 있다면 모듈의 버전을 검사한다.
    1. 로컬의 모듈 버전이 서버의 모듈 버전보다 낮다면 3-i, 3-ii를 실행한다.
    2. 로컬의 모듈 버전과 서버의 모듈 버전이 같다면 과정 4를 실행한다.
  3. 캐시되어 있지 않다면,
    1. 모듈을 서버로부터 읽어와 로컬 파일시스템에 저장한다.
    2. 버전 정보를 로컬 파일 시스템에 저장한다.
  4. 캐시된 파일로부터 클래스를 읽어오는 클래스 로더를 생성한다.
  5. 새로 생성한 클래스로더를 사용하여 모듈로부터 필요한 클래스를 읽어온다.
애플릿은 1에서 5까지의 과정만을 책임지게 되며, 실제 어플리케이션은 애플릿이 읽어오는 모듈을 통해서 실행되게 된다.

애플릿의 구현

애플릿은 크게 초기화(init() 메소드), 캐싱 검사 및 버전 검사(start() 메소드와 isUpdated() 메소드), 모듈 캐싱(cacheApplicationJar() 메소드), 클래스로더 생성(initClassLoader() 메소드) 그리고 실제 어플리케이션의 실행(startApplication() 메소드)의 5가지 부분으로 구성된다.

먼저 초기화 부분은 다음과 같다.

public void init() {
   // 화면 구성
   setLayout(new BorderLayout());
   messageLabel = new Label();
   add(messageLabel, BorderLayout.CENTER);
   
   cacheDirectory = System.getProperty("user.home");;
   appVersion = getParameter("appVersion");
   
   applFile = new File(cacheDirectory+File.separator+"appl.jar");
   versionFile = new File(cacheDirectory+File.separator+"appl.version");   
}

위 코드에서 앞 부분은 간단하게 진행 상황을 표시해줄 수 있는 GUI를 생성하고, 그 이후로는 모듈의 캐싱 및 버전과 관련된 필드를 초기화한다. 여기서 보여줄 에제는 해당 모듈을 사용자의 홈 디렉티리에 appl.jar 라는 이름으로 캐싱하며, 버전 정보는 홈 디렉토리의 appl.version 파일에 기록한다. System.getProperty("user.home") 을 이용하여 사용자의 홈 디렉토리를 구할 수 있다. 여기서 getParameter("appVersion") 메소드를 호출하는 데, 이는 서버에 있는 모듈의 버전을 구할 때 사용된다.

init() 메소드를 통해 초기화 과정이 이루어지면 애플릿의 start() 메소드가 호출된다. 예제에서는 start() 메소드에서 모듈의 캐싱여부와 버전을 체크하게 된다. start() 메소드는 다음과 같다.

public void start() {
   try {
      if (!checked) {
         // 현재 appl.jar가 캐쉬되어 있는 지 검색한다.
         if (!applFile.exists()) { // 캐쉬되어 있지 않다면,
            cacheApplicationJar();
         } else {
            // 캐쉬되어 있다면, 버전을 검사한다.
            if(isUpdated()) { // 버전이 올라갔다면
               cacheApplicationJar();
            }
         }
         checked = true;
      }
      initClassLoader();
      startApplication();
      messageLabel.setText("어플리케이션을 실행하였습니다.");
   } catch(IOException ex) {
      messageLabel.setText("IOException 발생");
   } catch(SecurityException ex) {
      ex.printStackTrace();
      messageLabel.setText("SecurityException 발생");
   } catch(Exception ex) {
      messageLabel.setText("기타 예외: "+ex.getMessage());
   }
}

위 코드에서 applFile.exists() 메소드는 현재 모듈이 로컬 파일시스템에 존재하는 지 판단한다. 존재할 경우에는 isUpdate() 메소드를 사용하여 캐시되어 있는 모듈의 버전과 서버에 있는 모듈의 버전을 검사한다. isUpdate() 메소드는 다음과 같다.

private boolean isUpdated() throws IOException {
   BufferedReader br = null;
   try {
      br = new BufferedReader(new FileReader(versionFile));
      String version = br.readLine();
      if (version != null) {
         int localVersion = Integer.parseInt(version);
         int serverVersion = Integer.parseInt(appVersion);
         if (localVersion < serverVersion)
            return true;
         else
            return false;
      }
      return false;
   } finally {
      if (br != null) try { br.close(); } catch(IOException ex) {}
   }
}

isUpdate() 메소드에서 로컬 파일 시스템의 사용자 홈디렉토리에 있는 appl.version 파일에 기록되어 있는 버전과 파라미터를 통해서 입력받은 버전을 비교한다. 만약 로컬 파일 시스템에 캐싱되어 있는 모듈이 서버에 있는 모듈보다 이전 버전이라면 true를 리턴하고 그렇지 않을 경우 false를 리턴하게 된다. 참고로 버전은 정수형으로 표시된다.

start() 메소드에서 모듈을 새로 캐싱해야 할 경우에는 cacheApplicationJar() 메소드를 호출한다. cacheApplicationJar() 메소드는 서버로부터 필요한 모듈을 읽어와 로컬 파일 시스템에 기록하는 역할을 한다. cacheApplicationJar() 메소드는 다음과 같다.

private void cacheApplicationJar() throws IOException {
   // 서버로부터 최신 버전의 appl.jar 파일을 읽어온 후,
   // 로컬 파일 시스템에 저장한다.
   BufferedInputStream is = null;
   BufferedOutputStream os1 = null;
   BufferedWriter os2 = null;
   
   try {
      messageLabel.setText("캐싱하고 있습니다.");
      System.out.println("캐싱하고 있습니다.");
      URL applJar = new URL(getDocumentBase(), "appl.jar");
      is = new BufferedInputStream(applJar.openStream());
      os1 = new BufferedOutputStream(new FileOutputStream(applFile) );
      byte[] buffer = new byte[1024]; // 1K 버퍼 사용
      int readByte = -1;
      while( (readByte = is.read(buffer, 0, buffer.length)) != -1) {
         os1.write(buffer, 0, readByte);
      }
      os2 = new BufferedWriter(new FileWriter(versionFile));
      os2.write(appVersion);
      messageLabel.setText("캐싱을 완료했습니다.");
      System.out.println("캐싱을 완료했습니다.");
   } finally {
      if (is != null) try { is.close(); } catch(IOException ex) {}
      if (os1 != null) try { os1.close(); } catch(IOException ex) {}
      if (os2 != null) try { os2.close(); } catch(IOException ex) {}
   }
}

cacheApplicationJar() 메소드는 URL.openStream() 클래스를 사용하여 필요한 모듈을 서버로부터 읽어온다. URL.openStream() 메소드를 통해서 구해진 IOStream으로부터 읽혀진 데이터는 출력 스트림을 통해서 로컬 파일 시스템에 저장된다. 그리고 버전 정보 역시 이 시점에서 같이 저장된다.

캐싱 작업이 끝나면 start() 메소드는 initClassLoader() 메소드를 호출한다. initClassLoader() 메소드는 캐싱된 모듈을 읽을 때 사용된다. initClassLoader() 메소드는 다음과 같다.

private void initClassLoader() throws MalformedURLException {
   if (classLoader == null) {
      URL[] urls = { applFile.toURL() };
      classLoader = new URLClassLoader(urls);
   }
}

캐싱된 모듈을 읽어올 때 사용하는 클래스로더는 java.net.URLClassLoader 클래스이다. 일단, 클래스로더를 생성하면 모듈로부터 필요한 클래스를 읽어와 어플리케이션을 구동하면 된다. 이는 startApplication() 메소드를 통해서 이루어진다. startApplication() 메소드는 다음과 같다.

private void startApplication() {
   try {
      Class applExecutor = classLoader.loadClass("com.javacan.appl.ApplicationDriver");
      applExecutor.newInstance();
   } catch(ClassNotFoundException ex) {
      messageLabel.setText("클래스 발견 못함");
   } catch(InstantiationException ex) {
      messageLabel.setText("인스턴스 생성 못함");
   } catch(IllegalAccessException ex) {
      messageLabel.setText("에외 발생: "+ex.getMessage());
   }
}

여기서 startApplication() 메소드는 이름이 "com.javacan.appl.ApplicationDriver"인 클래스를 로딩한 후, 그 클래스의 새로운 인스턴스를 생성한다. 따라서 모듈에 있는 com.javacan.appl.ApplicationDriver 클래스는 생성자에서 필요한 어플리케이션을 시작하면 된다.

이로써 애플릿은 필요한 모든 역할을 수행하게 된다. 이제 애플릿과 관련된 보안 문제를 해결하자. 이 글은 클래스로더와 모듈 개념을 설명하기 위한 것이므로 서명된 애플릿을 작성하는 방법을 사용하지 않고, 대신 설정 파일을 통해서 보안 문제를 해결해보도록 하자. 자바2 SDK에 포함되어 있는 appletviewer는 기본적으로 자바2의 보안 모델을 따르기 때문에, 다음과 같은 설정 파일을 사용함으로써 애플릿 테스트와 관련된 보안 문제를 해결할 수 있다. 이 파일을 all.policy 라고 하자.

grant {
   permission java.io.FilePermission "<<ALL FILES>>", "write";
   permission java.io.FilePermission "<<ALL FILES>>", "read";
   permission java.util.PropertyPermission "user.home", "read";
   permission java.lang.RuntimePermission "createClassLoader";
};

보안과 관련된 설정 파일을 지정하는 것에 대한 내용은 관련 링크의 자바 보안 관련 링크를 참조하기 바란다.

애플릿은 다음과 같은 HTML을 통해서 실행된다. 이 HTML 문서를 appl.html이라고 하자.

<html>
<head><title>Test</title></head>
<body>
<H1>애플릿 테스트</H1>
<applet code="ReadyApplet.class" archive="readyapplet.jar"
        width="500" height="50">
<param name="appVersion" value="1">
</applet>
</body>
</html>

readapplet.jar 파일은 ReadyApplet.class를 JAR 파일로 묶은 것이다. 위 HTML 코드에서 이름이 'appVersion'인 파라미터의 값이 서버에 있는 모듈의 버전을 나타낸다. 즉, 이 값에 따라 로컬 파일 시스템에 캐싱되어 있는 모듈을 새로운 모듈로 대체할 것인가가 결정된다.

테스트 모듈1

지금까지 애플릿에서 모듈을 처리하는 것에 대해서 알아보았으므로, 이제 실제로 어떻게 동작하는 지 알아보기 위해 예제를 작성해보자. 다음은 모듈에서 사용될 com.javacan.appl.ApplicationDriver 클래스이다.

package com.javacan.appl;

import java.awt.*;
import java.awt.event.*;

public class ApplicationDriver {
    public ApplicationDriver() {
        startApplication();
    }
   
    private void startApplication() {
        ApplFrame appl = new ApplFrame();
        appl.show();
    }
}

ApplicationDriver 클래스의 생성자는 startApplication() 메소드를 호출하고, startApplication() 메소드는 ApplFrame 클래스의 인스턴스를 생성한 후, 화면에 보여준다. com.javacan.appl.ApplFrame 클래스는 다음과 같다.

package com.javacan.appl;

import java.awt.*;
import java.awt.event.*;

public class ApplFrame extends Frame implements ActionListener {
   
   public ApplFrame() {
      super("Version 1");
      setLayout(new BorderLayout());
      TextArea ta = new TextArea(10, 30);
      add(ta, BorderLayout.CENTER);
      Button btn = new Button("닫기");
      add(btn, BorderLayout.SOUTH);
      
      btn.addActionListener(this);
      
      pack();
   }
   
   public void actionPerformed(ActionEvent e) {
      this.setVisible(false);
   }
}

ApplFrame 클래스는 java.awt.Frame을 상속받았으면, 내부에 한개의 TextArea와 한개의 Button을 갖고 있다. ApplicationDriver.java와 ApplFrame.java를 컴파일 하자. 이 때, c:\mywork\com\javacan\appl에 이 두 클래스 파일이 존재한다고 할 경우 c:\mywork 디렉토리에서 다음과 같이 JAR 실행파일을 이용하여 이 두 클래스를 Jar 파일로 묶도록 하자.

c:\mywork>jar cvf appl.jar com\*

그러면, c:\mywork 디렉토리에 appl.jar 파일이 생성될 것이다. 이 appl.jar 파일을 웹 서버가 문서의 루트디렉토리에 복사해 넣도록 하자. 그리고 앞에서 작성한 appl.html 파일을 같은 디렉토리에 복사하다. 그런 후, 다음과 같이 AppletViewer를 사용하여 appl.html을 실행해보자.

appletviewer -J"-Djava.security.policy=all.policy" http://localhost:8080/appl.html

여기서 -Djava.security.policy=all.policy 는 보안과 관련된 부분을 설정하기 위해 all.policy 파일을 사용하겠다는 것을 의미한다. all.policy 파일은 앞에서 작성한 파일이다.

이와 같이 실행하면 도스창에 다음과 같은 내용이 출력될 것이다.

캐싱하고 있습니다.
캐싱을 완료했습니다.

그리고 애플릿은 다음과 같은 새로운 Frame을 생성한다.


이제 AppletViewer를 종료했다가 다시 실행해보자. 그러면 캐싱 과정이 실행되지 않을 것이다. 실제로 캐싱된 파일은 윈도우즈98을 사용할 경우 c:\windows 디렉토리에 위치하게 된다.

테스트 모듈2

이제 모듈을 수정한 후, 변경된 모듈을 캐싱하는 지 살펴보자. 새로운 모듈의 ApplFrame 클래스는 다음과 같다.

package com.javacan.appl;

import java.awt.*;
import java.awt.event.*;

public class ApplFrame extends Frame implements ActionListener {
   
   private TextArea ta;
   private Button btn1;
   private Button btn2;
   
   public ApplFrame() {
      super("Version 2");
      setLayout(new BorderLayout());
      ta = new TextArea(10, 30);
      add(ta, BorderLayout.CENTER);

      btn1 = new Button("지우기");
      btn2 = new Button("닫기");
      
      Panel panel = new Panel();
      panel.setLayout(new GridLayout(1,2));
      panel.add(btn1);
      panel.add(btn2);
      
      add(panel, BorderLayout.SOUTH);
      
      btn1.addActionListener(this);
      btn2.addActionListener(this);
      
      pack();
   }
   
   public void actionPerformed(ActionEvent e) {
      if (e.getSource() == btn1) {
         ta.setText("");
      } else {
         this.setVisible(false);
      }
   }
}

앞에서 작성한 ApplFrame과 달리 새로운 ApplFrame 클래스는 Button을 두개 갖고 있다. 이제 새로운 ApplFrame.java를 컴파일 한 후, 앞에서 만찬가지로 ApplicationDriver.class와 ApplFrame.class 파일을 appl.jar로 묶도록 하자. 새로 생성한 appl.jar 파일을 웹서버가 문서를 읽을 때 사용하는 디렉토리에 알맞게 위치시킨 후 appl.html의 <param> 태그를 다음과 같이 변경하자.

<param name="appVersion" value="2">

모듈의 버전을 "1"에서 "2"로 변경한 것을 알 수 있다. 이제 다시 AppletViewer로 appl.html을 실행해보자. 그러면 다음과 같은 결과가 출력될 것이다.


위 결과를 보면 새로 변경된 모듈이 실행된 것을 알 수 있다. 도스창을 보면 캐싱하고 있다는 메시지가 출력되는 것을 알 수 있다.

결론

이번 클래스로더와 관련된 네번째 아티클에서는 클래스로더를 이용하여 캐싱한 모듈을 읽어와서 실행하는 것에 대해서 살펴봤다. 모듈을 캐싱함으로써 매번 모듈을 로딩할 필요가 없어졌으며, 애플릿을 실행할 때에 변경된 모듈을 새로 캐싱함으로써 늘 새로운 모듈을 사용하여 애플릿을 실행할 수 있다는 것을 알게 되었을 것이다.

이처럼 클래스로더와 캐싱을 사용하는 것은 애플릿 뿐만 아니라 어플리케이션에서도 사용할 수 있으며, 좀 더 발전시켜 어플리케이션을 실행하는 도중에, 즉, 런타임에 모듈을 동적으로 변경하도록 할 수도 있을 것이다.

관련링크:

+ Recent posts