저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의
자바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)를 구축할 경우 필요한 라이브러리의 캐시 기능, 동적인 라이브러리의 업데이트 등의 이유 때문에 커스텀 클래스로더의 구현은 필수적이라 할 수 있다. 다음에는 웹 어플리케이션에서 사용하는 라이브러리를 로컬에 캐싱하고 버전이 변경될 때 마다 자동으로 라이브러리를 로딩하는 예제를 살펴볼 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

동적인 클래스 로딩

자바는 동적으로 클래스를 읽어온다. 즉, 런타임에 모든 코드가 JVM에 링크된다. 모든 클래스는 그 클래스가 참조되는 순간에 동적으로 JVM에 링크되며, 메모리에 로딩된다. 자바의 런타임 라이브러리([JDK 설치 디렉토리]/jre/lib/rt.jar) 역시 예외가 아니다. 이러한 동적인 클래스 로딩은 자바의 클래스로더 시스템을 통해서 이루어지며, 자바가 기본적으로 제공하는 클래스로더는 java.lang.ClassLoader를 통해서 표현된다. JVM이 시작되면, 부트스트랩(bootstrap) 클래스로더를 생성하고, 그 다음에 가장 첫번째 클래스인 Object를 시스템에 읽어온다.

런타임에 동적으로 클래스를 로딩하다는 것은 JVM이 클래스에 대한 정보를 갖고 있지 않다는 것을 의미한다. 즉, JVM은 클래스의 메소드, 필드, 상속관계 등에 대한 정보를 알지 못한다. 따라서, 클래스로더는 클래스를 로딩할 때 필요한 정보를 구하고, 그 클래스가 올바른지를 검사할 수 있어야 한다. 만약 이것을 할 수 없다면, JVM은 .class 파일의 버전이 일치하지 않을 수 있으며, 또한 타입 검사를 하는 것이 불가능할 것이다. JVM은 내부적으로 클래스를 분석할 수 있는 기능을 갖고 있으며, JDK 1.1부터는 개발자들이 리플렉션(Reflection)을 통해서 이러한 클래스의 분석을 할 수 있도록 하고 있다.

로드타임 동적 로딩(load-time dynamic loading)과 런타임 동적 로딩(run-time dynamic loading)

클래스를 로딩하는 방식에는 로드타임 동적 로딩(load-time dynamic loading)과 런타임 동적 로딩(run-time dynamic loading)이 있다. 먼저 로드타임 동적 로딩에 대해서 알아보기 위해 다음과 코드를 살펴보자.

  public class HelloWorld {
     public static void main(String[] args) {
        System.out.println("안녕하세요!");
     }
  }

HelloWorld 클래스를 실행하였다고 가정해보자. 아마도, 명령행에서 다음과 같이 입력할 것이다.

  $ java HelloWorld

이 경우, JVM이 시작되고, 앞에서 말했듯이 부트스트랩 클래스로더가 생성된 후에, 모든 클래스가 상속받고 있는 Object 클래스를 읽어온다. 그 이후에, 클래스로더는 명령행에서 지정한 HelloWorld 클래스를 로딩하기 위해, HelloWorld.class 파일을 읽는다. HelloWorld 클래스를 로딩하는 과정에서 필요한 클래스가 존재한다. 바로 java.lang.String과 java.lang.System이다. 이 두 클래스는 HelloWorld 클래스를 읽어오는 과정에서, 즉 로드타임에 로딩된다. 이 처럼, 하나의 클래스를 로딩하는 과정에서 동적으로 클래스를 로딩하는 것을 로드타임 동적 로딩이라고 한다.

이제, 런타임 동적 로딩에 대해서 알아보자. 우선, 다음의 코드를 보자.

  public class HelloWorld1 implements Runnable {
     public void run() {
        System.out.println("안녕하세요, 1");
     }
  }
  public class HelloWorld2 implements Runnable {
     public void run() {
        System.out.println("안녕하세요, 2");
     }
  }

이 두 클래스를 Runnable 인터페이스를 구현한 간단한 클래스이다. 이제 실제로 런타임 동적 로딩이 일어나는 클래스를 만들어보자.

  public class RuntimeLoading {
     public static void main(String[] args) {
        try {
           if (args.length < 1) {
              System.out.println("사용법: java RuntimeLoading [클래스 이름]");
              System.exit(1);
           }
           Class klass = Class.forName(args[0]);
           Object obj = klass.newInstance();
           Runnable r = (Runnable) obj;
           r.run();
        } catch(Exception ex) {
           ex.printStackTrace();
        }
     }
  }

위 코드에서, Class.forName(className)은 파리미터로 받은 className에 해당하는 클래스를 로딩한 후에, 그 클래스에 해당하는 Class 인스턴스(로딩한 클래스의 인스턴스가 아니다!)를 리턴한다. Class 클래스의 newInstance() 메소드는 Class가 나타내는 클래스의 인스턴스를 생성한다. 예를 들어, 다음과 같이 한다면 java.lang.String 클래스의 객체가 생성된다.

  Class klass = Class.forName("java.lang.String");
  Object obj = klass.newInstance();

따라서, Class.forName() 메소드가 실행되기 전까지는 RuntimeLoading 클래스에서 어떤 클래스를 참조하는 지 알수 없다. 다시 말해서, RuntimeLoading 클래스를 로딩할 때는 어떤 클래스도 읽어오지 않고, RuntimeLoading 클래스의 main() 메소드가 실행되고 Class.forName(args[0])를 호출하는 순간에 비로서 args[0]에 해당하는 클래스를 읽어온다. 이처럼 클래스를 로딩할 때가 아닌 코드를 실행하는 순간에 클래스를 로딩하는 것을 런타임 동적 로딩이라고 한다.

다음은 RuntimeLoading 클래스를 명령행에서 실행한 결과를 보여주고 있다.

  $ java RuntimeLoading HelloWorld1
  안녕하세요, 1

Class.newInstance() 메소드와 관련해서 한 가지 알아둘 점은 해당하는 클래스의 기본생성자(즉, 파라미터가 없는)를 호출한다는 점이다. 자바는 실제로 기본생성자가 코드에 포함되어 있지 않더라도 코드를 컴파일할 때 자동적으로 기본생성자를 생성해준다. 이러한 기본생성자는 단순히 다음과 같이 구성되어 있을 것이다.

  public ClassName() {
     super();
  }

ClassLoader

자바는 클래스로더를 사용하고, 클래스를 어떻게 언제 JVM으로 로딩하고, 언로딩하는지에 대한 특정한 규칙을 갖고 있다. 이러한 규칙을 이해해야, 클래스로더를 좀 더 유용하게 사용할 수 있으며 개발자가 직접 자신만의 커스텀 클래스로더를 작성할 수 있게 된다.

클래스로더의 사용

이 글을 읽는 사람들은 거의 대부분은 클래스로더를 프로그래밍에서 직접적으로 사용해본 경험이 없을 것이다. 클래스로더를 사용하는 것은 어렵지 않으며, 보통의 자바 클래스를 사용하는 것과 완전히 동일하다. 다시 말해서, 클래스로더에 해당하는 클래스의 객체를 생성하고, 그 객체의 특정 메소드를 호출하기만 하면 된다. 간단하지 않은가? 다음의 코드를 보자.

  ClassLoader cl = . . . // ClassLoader의 객체를 생성한다.
  Class klass = null;
  try {
     klass = cl.loadClass("java.util.Date");
  } catch(ClassNotFoundException ex) {
     // 클래스를 발견할 수 없을 경우에 발생한다.
     ex.printStackTrace();
  }

일단 클래스로더를 통해서 필요한 클래스를 로딩하면, 앞의 예제와 마찬가지로 Class 클래스의 newInstance() 메소드를 사용하여 해당하는 클래스의 인스턴스를 생성할 수 있게 된다. 형태는 다음과 같다.

  try {
     Object obj = klass.newInstance();
  } catch(InstantiationException ex) {
     ....
  } catch(IllegalAccessException ex) {
     ....
  } catch(SecurityException ex) {
     ....
  } catch(ExceptionIninitializerError error) {
     ...
  }

위 코드를 보면, Class.newInstance()를 호출할 때 몇개의 예외와 에러가 발생하는 것을 알 수 있다. 이것들에 대한 내용은 Java API를 참고하기 바란다.

자바 2의 클래스로더

자바 2 플랫폼에서 클래스로더의 인터페이스와 세만틱(semantic)은 개발자들이 자바 클래스로딩 메커니즘을 빠르고 쉽게 확장할 수 있도록 하기 위해 몇몇 부분을 재정의되었다. 그 결과로, 1.1이나 1.0에 맞게 작성된 (커스텀 클래스로더를 포함한) 클래스로더는 자바 2 플랫폼에서는 제기능을 하지 못할 수도 있으며, 클래스로더 사용하기 위해 작성했던 코드를 재작성하는 것이 그렇게 간단하지만은 않다.

자바 1.x와 자바 2에서 클래스로더에 있어서 가장 큰 차이점은 자바 2의 클래스로더는 부모 클래스로더(상위 클래스가 아니다!)를 갖고 있다는 점이다. 자바 1.x의 클래스로더와는 달리, 자바 2의 클래스로더는 부모 클래스로더가 먼저 클래스를 로딩하도록 한다. 이를 클래스로더 딜리게이션 모델(ClassLoader Delegation Model)이라고 하며, 이것이 바로 이전 버전의 클래스로더와 가장 큰 차이점이다.

자바 2의 클래스로더 딜리게이션 모델에 대해 구체적으로 알아보기 위해 로컬파일시스템과 네트워크로부터 클래스를 읽어와야 할 필요가 있다고 가정해보자. 이 경우, 쉽게 로컬파일시스템의 jar 파일로부터 클래스를 읽어오는 클래스로더와 네트워크로부터 클래스를 읽어오는 클래스로더가 필요하다는 것을 생각할 수 있다. 이 두 클래스로더를 각각 JarFileClassLoader와 NetworkClassLoader라고 하자.

JDK 1.1에서, 커스텀 클래스로더를 만들기 위해서는 ClassLoader 클래스를 상속받은 후에 loadClass() 메소드를 오버라이딩하고, loadClass() 메소드에서 바이트코드를 읽어온 후, defineClass() 메소드를 호출하면 된다. 여기서 defineClass() 메소드는 읽어온 바이트코드로부터 실제 Class 인스턴스를 생성해서 리턴한다. 예를 들어, JarFileClassLoader는 다음과 같은 형태를 지닐 것이다.

  public class JarFileClassLoader extends ClassLoader {
     ...
     private byte[] loadClassFromJarFile(String className) {
        // 지정한 jar 파일로부터 className에 해당하는 클래스의
        // 바이트코드를 byte[] 배열로 읽어온다.
        ....
        return byteArr;
     }
     
     public synchronized class loadClass(String className, boolean resolveIt)
        throws ClassNotFoundException {
        
        Class klass = null;
        
        // 클래스를 로드할 때, 캐시를 사용할 수 있다.
        klass = (Class) cache.get(className);
        
        if (klass != null) return klass;
        
        // 캐시에 없을 경우, 시스템 클래스로더로부터
        // 지정한 클래스가 있는 지 알아본다.
        try {
           klass = super.findSystemClass(className);
           return klass;
        } catch(ClassNotFoundException ex) {
           // do nothing
        }
        
        // Jar 파일로부터 className이 나타내는 클래스를 읽어온다.
        byte[] byteArray = loadClassFromJarFile(className);
        klass = defineClass(byteArray, 0, byteArray.length);
        if (resolve)
           resolveClass(klass);
        cache.put(className, klass); // 캐시에 추가
        return klass;
     }
  }

위의 개략적인 코드를 보면, 시스템 클래스로더에게 이름이 className인 클래스가 존재하는 지 요청한다. (여기서 시스템 클래스로더 또는 primordial 시스템 클래스로더는 부트스트랩 클래스로더이다). 그런 후에, 시스템 클래스로더로부터 클래스를 읽어올 수 없는 경우 Jar 파일로부터 읽어온다. 이 때, className은 완전한 클래스 이름(qualified class name; 즉, 패키지이름을 포함한)이다. NetworkClassLoader 클래스 역시 이 클래스와 비슷한 형태로 이루어져 있을 것이다. 이 때, 시스템 클래스로더와 그 외의 다른 클래스로더와의 관계는 다음 그림과 같다.


위 그림을 보면, 각각의 클래스로더는 오직 시스템 클래스로더와 관계를 맺고 있다. 다시 말해서, JarFileClassLoader는 NetworkClassLoader나 AppletClassLoader와는 관계를 맺고 있지 않다. 이제, A라는 클래스가 내부적으로 B라는 클래스를 사용한다고 가정해보자. 이 때, 만약 A 클래스는 네트워크를 통해서 읽어오고, B라는 클래스는 Jar 파일을 통해서 읽어와야 한다면? 이 경우에 어떻게 해야 하는가? 쉽사리 해결책이 떠오르지 않을 것이다. 이러한 문제는 JarFileClassLoader와 NetworkClassLoader 간에 유기적인 결합을 할 수 없기 때문에 발생한다.

자바 2에서는 이러한 문제를 클래스로더 딜리게이션 모델을 통해서 해결하고 있다. 즉, 특정 클래스로더 클래스를 읽어온 클래스로더(이를 부모 클래스로더라고 한다)에게 클래스 로딩을 요청하는 것이다. 다음의 그림을 보자.


이 그림은 자바 2에서 클래스로더간의 관계를 보여주고 있다. 이 경우, NetworkClassLoader 클래스는 JarFileClassLoader가 로딩하고, JarFileClassLoader 클래스는 AppClassLoader가 로딩하였음을 보여준다. 즉, JarFileClassLoader는 NetworkClassLoader의 부모 클래스로더가 되고, AppClassLoader는 JarFileClassLoader의 부모 클래스로더가 되는 것이다.

이 경우, 앞에서 발생했던 문제가 모두 해결된다. A 클래스가 필요하면, 가장 먼저 NetworkClassLoader에 클래스로딩을 요청한다. 그럼, NetworkClassLoader는 네트워크로부터 A 클래스를 로딩할 수 있으므로, A 클래스를 로딩한다. 그런 후, A 클래스는 B 클래스를 필요로 한다. B 클래스를 로딩하기 위해 NetworkClassLoader는 JarFileClassLoader에 클래스 로딩을 위임(delegation)한다. JarFileClassLoader는 Jar 파일로부터 B 클래스를 읽어온 후 NetworkClassLoader에게 리턴할 것이며, 따라서 NetworkClassLoader는 Jar 파일에 있는 B 클래스를 사용할 수 있게 된다. 앞의 JDK 1.1에서의 클래스로더 사이의 관계에 비해 훨씬 발전적인 구조라는 것을 알 수 있다.

앞에서 말했듯이, 자바 2에서는 몇몇 클래스로더 메커니즘을 재정의하였다. 이 때문에, JDK 1.1에서의 클래스로더에 관한 몇몇개의 규칙이 깨졌다. 먼저, loadClass() 메소드를 더 이상 오버라이딩(overriding) 하지 않고, 대신 findClass()를 오버라이딩한다. loadClass() 메소드는 public에서 protected로 변경되었으며, 실제 JDK1.3의 ClassLoader 클래스의 소크 코드를 보면 다음과 같이 정의되어 있다.

  // src/java/lang/ClassLoader.java
  public abstract class ClassLoader {
      /*
       * The parent class loader for delegation.
       */
      private ClassLoader parent;
      
      protected synchronized Class loadClass(String name, boolean resolve)
      throws ClassNotFoundException
      {
          // First, check if the class has already been loaded
          Class c = findLoadedClass(name);
          if (c == null) {
              try {
                  if (parent != null) {
                      c = parent.loadClass(name, false);
                  } else {
                      c = findBootstrapClass0(name);
                  }
              } catch (ClassNotFoundException e) {
                  // If still not found, then call findClass in order
                  // to find the class.
                  c = findClass(name);
              }
          }
          if (resolve) {
              resolveClass(c);
          }
          return c;
      }
      ....
  }

위 코드를 보면 부모 클래스로더로부터 먼저 클래스 로딩을 요청하고, 그것이 실패할 경우(즉, catch 블럭)에 비로소 직접 클래스를 로딩한다. 여기서 그렇다면 부모 클래스는 어떻게 결정되는 지 살펴보자. 먼저 JDK 1.3의 ClassLoader 클래스는 다음과 같은 두 개의 생성자를 갖고 있다.

  protected ClassLoader(ClassLoader parent) {
      SecurityManager security = System.getSecurityManager();
      if (security != null) {
          security.checkCreateClassLoader();
      }
      this.parent = parent;
      initialized = true;
  }
  protected ClassLoader() {
      SecurityManager security = System.getSecurityManager();
      if (security != null) {
          security.checkCreateClassLoader();
      }
      this.parent = getSystemClassLoader();
      initialized = true;
  }

이 두 코드를 살펴보면, 부모 클래스로더를 지정하지 않을 경우, 시스템 클래스로더를 부모 클래스로더로 지정하는 것을 알 수 있다. 따라서 커스텀 클래스로더에서 부모 클래스로더를 지정하기 위해서는 다음과 같이 하면 된다.

  public class JarFileClassLoader extends ClassLoader {
     public JarFileClassLoader () {
        super(JarFileClassLoader.class.getClassLoader());
        // 다른 초기화 관련 사항
     }
     ....
     public Class findClass(String name) {
        // 지정한 클래스를 찾는다.
     }
  }

모든 클래스는 그 클래스에 해당하는 Class 인스턴스를 갖고 있다. 그 Class 인스턴스의 getClassLoader() 메소드를 통해서 그 클래스를 로딩한 클래스로더를 구할 수 있다. 즉, 위 코드는 JarFileClassLoader 클래스를 로딩한 클래스로더를 JarFileClassLoader 클래스로더의 부모 클래스로더로 지정하는 것이다. (실제로 커스텀 클래스로더를 구현하는 것에 대한 내용은 이 Article의 시리중에서 3번째에 알아보기로 한다).

JVM에서 부모 클래스로더를 갖지 않은 유일한 클래스로더는 부트스트랩 클래스로더이다. 부트스트랩 클래스로더는 자바 런타임 라이브러리에 있는 클래스를 로딩하는 역할을 맡고 있으며, 항상 클래스로더 체인의 가장 첫번째에 해당한다. 기본적으로 자바 런타임 라이브러리에 있는 모든 클래스는 JRE/lib 디렉토리에 있는 rt.jar 파일에 포함되어 있다.

결론

이번 Article에서는 자바에서 클래스 로딩이 동적으로 이루어지면, 클래스 로딩 방식에서는 로드타임 로딩과 런타임 로딩의 두 가지 방식이 있다는 것을 배웠다. 그리고 자바 2에서의 클래스로딩이 클래스로더 딜리게이션 모델(Classloader Delegation Model)을 통해서 이루어진다는 점과 이 모델에 자바 1.x에서의 클래스로딩 메커니즘과 어떻게 다르며, 어떤 장점이 있는 지 알아보았다. 다음 Article에서는 자바 2에서 기본적으로 제공하는 클래스로더에 대해서 알아보기로 한다.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. JunkMan 2013.05.10 09:51 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 정보 감사합니다.

  2. bakgaksi 2015.03.03 03:21 신고  댓글주소  수정/삭제  댓글쓰기

    초보가 보기엔 무지막지 하네요 한... 6시간 동안 본듯. 그래도 무슨내용인지 모르겠어요 >.<

  3. 들개 2015.07.30 13:41 신고  댓글주소  수정/삭제  댓글쓰기

    다시봐도 정말 멋진 설명입니다. 최범균님 책보면서 jsp를 배웠었는데 ㅎㅎ.
    이렇게 지식공유해주시는 멋진 고수님들이 있어 정말 다행입니다.
    감사한 마음으로 봤습니다. 꾸벅.