주요글: 도커 시작하기
반응형
자바2의 딜리게이션 클래스로딩에 알맞은 커스텀 클래스로더를 작성해본다.

커스텀 클래스로더의 구현

지난 두 번의 기사에서는 자바의 동적인 클래스 로딩과 자바2의 클래스로더 딜리게이션 모델, 그리고 자바2에서 제공하는 기본적은 클래스로더에 대해서 알아보았다. 이 두 기사를 통해서 여러분은 어느 정도 자바에서 클래스로딩이 어떻게 이루어지며 클래스로더가 어떻게 동작하는지에 대해서 이해했을 것이다.

커스텀 클래스 로더는 이미 서블릿 엔진이나 JSP 엔진에서 사용되고 있으며, 대부분의 어플리케이션 서버는 자신만의 커스텀 클래스로더를 구현하여 사용하고 있다. 특히, 서블릿 리로딩은 커스텀 클래스 로더를 사용하지 않으면 구현하기 힘든 기능이다. 또한, ASP(Application Service Provider)에 대한 관심이 높아지면서 자바를 이용하여 ASP를 구현하는 사례가 늘고 있으며, 이 경우 커스텀 클래스 로더의 사용은 필수적이라 할 수 있다. (실제로 초기에 나온 넷피스의 경우 커스텀 클래스 로더를 사용하여 구현되었다.) 이 글에서는 커스텀 간단한 커스텀 클래스 로더를 구현해볼 것이며, 다음 번에 커스텀 클래스 로더를 이용한 어플리케이션의 동적 업그레이드에 대해서 살펴볼 것이다.

'클래스로더 1, 동적인 클래스 로딩과 클래스로더'에서 살펴봤듯이, 자바2에서 커스텀 클래스로더를 구현하기 위해서는 다음의 두 가지가 필요하다.

  1. java.lang.ClassLoader 클래스를 상속받는다.
  2. findClass() 메소드를 알맞게 구현한다.
실제로 이 과정은 그렇게 어렵지 않으며, findClass() 메소드에서 알맞게 클래스를 읽어오는 것이 가장 힘든 부분이라면 힘든 부분이다. 여기서는 파일 시스템의 특정 디렉토리로부터 클래스를 읽어오는 FileClassLoader 클래스를 작성해보자. FileClassLoader 클래스의 소스 코드는 다음과 같다.

package javacan.custom.classloader;

import java.io.*;

public class FileClassLoader extends ClassLoader {
   
   private String root;
   
   /**
    * @param rootDir 클래스를 읽어올 루트 디렉토리
    */
   public FileClassLoader(String rootDir) throws FileNotFoundException {
      super(FileClassLoader.class.getClassLoader() );
      
      File f = new File(rootDir);
      if (f.isDirectory())
         root = rootDir;
      else
         throw new FileNotFoundException(rootDir+" isn't a directory");
   }
   
   /**
    * @param name 검색할 클래스 이름
    */
   public Class findClass(String name) throws ClassNotFoundException {
      try {
         String path = root + File.separatorChar + 
                       name.replace('.', File.separatorChar) + ".class";
         
         FileInputStream file = new FileInputStream(path);
         byte[] classByte = new byte[file.available()];
         file.read(classByte);
         
         return defineClass(name, classByte, 0, classByte.length);
      } catch(IOException ex) {
         throw new ClassNotFoundException();
      }
   }
}

FileClassLoader 클래스의 생성자는 FileClassLoader가 클래스를 읽어올 디렉토리를 파라미터로 입력받는다. 예를 들어, FileClassLoader 클래스의 생성자에 파라미터로 "c:\classes\custom"을 넘겨주었다고 할 경우, "somepackage.AnyClass" 라는 클래스를 읽어올 때에는 "c:\classes\custom\somepackage" 디렉토리에 있는 AnyClass.class 파일로부터 클래스의 정의를 읽어온다.

생성자에서 눈여겨 볼 부분은 상위 클래스의 생성자를 호출하는 부분이다.

super(FileClassLoader.class.getClassLoader() );

첫번째 글에서 자바2의 클래스로더는 딜리게이션 모델을 사용하고 있으며, 부모 클래스로더에게 클래스 로딩 작업을 위임한다고 한 것을 기억할 것이다. 위 코드는 바로 부모 클래스로더를 지정하는 부분다. 부모 클래스로더를 지정함으로써 FileClassLoader는 오직 자신과 관련된 클래스로딩만 신경쓰면 되며, 나머지 클래스의 로딩은 부모 클래스로더에게 위임하면 된다.

실제로 요청한 클래스를 읽어오는 부분은 findClass() 메소드이다. 일반적으로 findClass() 메소드는 다음과 같은 두 과정을 처리하게 된다.

  • 특정 자원(예를 들어, JAR, ZIP, 네트워크)으로부터 클래스의 정의를 읽어와 byte[] 배열에 저장한다.
  • defineClass() 메소드를 사용하여 Class 객체를 생성한 후 리턴한다.
FileClassLoader 클래스의 findClass() 메소드를 살펴보면, 생성자를 통해서 입력받은 루트 디렉토리 이름과 파라미터로 전달받은 클래스 이름을 사용하여 읽어올 파일을 결정한다. 그런 후, 그 파일로부터 클래스의 정의를 읽어와 byte[] 배열에 저장한 다음 defineClass() 메소드를 호출하여 Class 객체를 생성하고 리턴한다.

커스텀 클래스로더 테스트

실제로 FileClassLoader가 어떻게 동작하는 지 알아보기 위해 간단한 예제를 살펴보자. 이 글에서는 FileClassLoader가 CLASSPATH가 아닌 다른 디렉토리에 있는 FirstHello 클래스와 SecondHello 클래스를 로딩하도록 할 것이다. FirstHello 클래스는 다음과 같다.

package javacan.test;

public class FirstHello {

   public FirstHello() {
      printInfo();
      this.init();
   }
   
   public void printInfo() {
      System.out.println("FirstHello");
   }
   
   protected void init() {
      SecondHello sh = new SecondHello();
      sh.printInfo();
   }
}

FirstHello 클래스는 init() 메소드에서 SecondHello 클래스를 사용하며, SecondHello 클래스는 다음과 같다.

package javacan.test;

public class SecondHello {
   
   public SecondHello() {
   }
   
   public void printInfo() {
      System.out.println("SecondHello");
   }
}

FirstHello 클래스와 SecondHello 클래스는 단순히 문자열을 출력해준다. 이제 실제로 FileClassLoader를 사용하여 FirstHello 클래스와 SecondHello 클래스를 로딩하는 TestProgram 클래스를 살펴보자.

import javacan.custom.classloader.FileClassLoader;

public class TestProgram {

   public static void main(String[] args) throws Exception {
      if (args.length < 1) {
         System.out.println("[Usage] java TestProgram <directory>");
         System.exit(1);
      }
      
      FileClassLoader loader = new FileClassLoader(args[0]);
      
      Class klass = loader.loadClass("javacan.test.FirstHello");
      Object obj = klass.newInstance();
      
      System.out.println(obj.getClass().getName() );
   }
}

TestProgram 클래스의 main() 메소드를 살펴보면, FileClassLoader 인스턴스를 생성한 후, loadClass() 메소드를 사용하여 javacan.test.FirstHello 클래스를 로딩하는 것을 알 수 있다. loadClass() 메소드에 대한 내용은 이 연재의 첫번째 기사를 참조하기 바란다. 로딩한 클래스는 Class 객체의 레퍼런스인 klass에 할당되며, klass.newInstance() 를 통해서 FirstHello 클래스의 인스턴스를 생성한다.

이제 테스트 환경을 꾸며보자. 먼저 지금까지 작성한 네 개의 클래스가 각각 다음과 같은 디렉토리에 위치시키자.

FileClassLoader.class : c:\classes\normal\javacan\custom\classloader
TestProgram.class : c:\classes\normal
FirstHello.class : c:\classes\custom\javacan\test
SecondHello.class : c:\classes\custom\javacan\test

이제 CLASSPATH 환경 변수를 다음과 같이 설정하자.

SET CLASSPATH=c:\classes\normal

즉, JVM이 기본적으로 사용하는 클래스로더는 c:\classes\normal에 위치한 FileClassLoader 클래스와 TestProgram 클래스로더만 로딩할 수 있을 뿐, FirstHello와 SecondHello 클래스는 로딩할 수 없도록 해 놓는 것이다. 이제 TestProgram 클래스를 실행시켜보자. 그럼 다음과 같은 결과가 출력될 것이다.

C:\>java -cp c:\classes\normal TestProgram c:\classes\custom
FirstHello
SecondHello
javacan.test.FirstHello

여기서 'FirstHello'와 'SecondHello'는 모두 FileClassLoader에 의해 로딩된 FirstHello와 SecondHello 클래스에서 출력한 문장이다. TestProgram.main() 메소드에서 FirstHello 클래스의 인스턴스를 생성하는 문장은 다음 부분이다.

Object obj = klass.newInstance();

이를 통해 FirstHello 클래스의 인스턴스가 생성된다. 인스턴스를 생성한다는 것은 생성자가 호출된다는 것을 의미하며, 따라서 FirstHello 클래스의 생성자가 실행된다. 여기서 중요한 점은 TestProgram 클래스에서는 FirstHello와 SecondHello 클래스를 직접적으로 사용할 수 없다는 점이다. 왜냐면 FirstHello 클래스와 SecondHello 클래스는 TestProgram 클래스를 로딩한 클래스로더(즉, sun.misc.Launcher$AppClassLoader)가 로딩할 수 없기 때문이다.

자원의 로딩

마지막으로 클래스로더가 자원을 로딩할 때 사용하는 findResource() 메소드에 대해서 살펴보자. 여기서 자원은 클래스들이 사용하는 이미지 파일이나 오디오 파일 등이 될 수 있다. 커스텀 클래스로더가 완벽해지기 위해서는 알맞게 자원을 찾을 수 있는 기능을 제공해야 한다.

자바2에서, ClassLoader 클래스는 클래스로딩과 비슷한 방법으로 딜리게이션 모델을 사용하여 자원을 위치시킨다. 커스텀 클래스로더는 클래스 로딩과 마찬가지로 자신만의 자원 검색 방법을 findResource() 메소드에 구현하기만 하면 된다. 예를 들어, 앞에서 작성한 커스텀 클래스로더인 FileClassLoader에 자원을 찾을 수 있는 기능을 추가하고자 할 경우 다음과 같이 findResource() 메소드를 알맞게 오버라이딩 하면 된다.

   protected java.net.URL findResource(String name) {
      File resource = new File(root + File.separatorChar + name);
      if (resource.exists()) {
         try {
            resource.toURL();
         } catch(MalformedURLException ex) {
            // do nothing
         }
      }
      return null;
   }

실제로 클래스로더를 이용하여 자원의 URL을 읽어오는 어플리케이션은 getResource() 메소드를 사용한다. 이 메소드는 loadClass() 메소드와 마찬가지로 먼저 부모 클래스로더로부터 자원을 검색한 후, 없다면 findResource() 메소드를 사용하여 요청한 자원을 검색한다. 사용자들은 클래스로더의 getResource() 메소드 뿐만 아니라 getResources() 메소드를 사용하여 동시에 여러개의 자원을 참조할 수도 있다.

시스템의 클래스패스에 잇는 자원을 검색하고자 할 경우에는 ClassLoader.getSystemResource() 메소드를 사용하면 된다. 예를 들어, 클래스패스에 있는 "javacan.properties"라는 파일을 참조하길 원할 경우 다음과 같이 하면 된다.

java.net.URL dbURL = ClassLoader.getSystemResource("tpage.properties");

이 메소드는 시스템 클래스로더(일반적으로, sun.misc.Launcher$AppClassLoader)를 사용하여 필요한 자원을 검색하며, 자원이 발견되지 않더라도 커스텀 클래스로더에 자원 검색을 위임(딜리게이션)하지 않는다.

결론

이번 글에서는 지정한 디렉토리로부터 클래스를 로딩하는 FileClassLoader 클래스를 작성해보았다. 비록 이 클래스가 간단하긴 하지만 커스텀 클래스로더를 구현하는 방법을 보여주고 있으며, 프로젝트에 커스텀 클래스로더가 필요할 경우 어렵지 않게 커스텀 클래스로더를 구현할 수 있을 것이다.

실제로 자바를 이용하여 웹 기반의 ASP(Application Service Provier)를 구축할 경우 필요한 라이브러리의 캐시 기능, 동적인 라이브러리의 업데이트 등의 이유 때문에 커스텀 클래스로더의 구현은 필수적이라 할 수 있다. 다음에는 웹 어플리케이션에서 사용하는 라이브러리를 로컬에 캐싱하고 버전이 변경될 때 마다 자동으로 라이브러리를 로딩하는 예제를 살펴볼 것이다.

관련링크:

+ Recent posts