클래스의 UNLOADING 과 RELOADING
지난 2003/7/22 Tech Tip인 "Compiling Source Directly From a Program"에서는 자바 프로그램으로부터 소스 파일을 직접적으로 컴파일하는 방법에 대해 설명하며, 클래스의 이름을 짓고 소스를 제공할 수 있는 간단한 편집기를 소개했다. 이 접근방법에는 별 문제가 없지만,여기서 제공된 클래스는 프로그램의 실행 중에 사용자가 컴파일하고 로드했던 클래스가 아니라는 것을 알아야 한다. 만약 이미 클래스를 컴파일하고 로드한 상태라면, 지난 Tech Tip 에서 소개했던 접근방법을 이용해서 클래스를 편집하고 리컴파일하는 것은 불가능하다. 이번 Tech Tip 에서는 underlying class loader 에 관해서 이야기 하고자 한다. 시스템 클래스 로더를 통해서 로드된 모든 클래스는 절대로 unload될 수 없기 때문에 프로그램 상에서 클래스의 소스를 재편집한 후, 리컴파일(recompile)하고 재생하고자 한다면, 다른 클래스 로더(class loader)를 사용해야 한다. 이 글에서는 이미 컴파일된 클래스(또는 어떤 클래스라도)를 로드하게끔 해주는 커스텀 클래스 로더의 생성법을 알아보고, 클래스를 컴파일하고, 이 컴파일된 클래스가 로드되었을 때, 기존의 클래스를 버리는 방법에 대해서 배우게 될 것이다.
모든 클래스의 로딩을 책임지는 것은 java.lang
패키지 내에 있는 ClassLoader
클래스의 한 인스턴스이다. 시스템 클래스의 경우에는 ClassLoader
의 getSystemClassLoader
메소드를 통해서 클래스 로더를 사용할 수 있고, 사용자 클래스의 경우에는 이미 클래스가 로드되어 있는 상태라면, Class
클래스의 getClassLoader
메소드를 통해 ClassLoader
에게 요청할 수가 있다.
지난 Tech Tip의 RunIt
프로그램에서는 클래스의 인스턴스를 만들어 내는 클래스 데이터를 로드하기 위해 forName
메소드를 사용하였다. 하지만 forName
메소드가 아닌 다른 클래스 로더를 사용하고자 한다면, ClassLoader
의 loadClass
메소드를 사용할 수 있다.
다시 말해서,
String className = ...; Class aClass = Class.forName(className);
위의 코드 대신에,
다음과 같이 코딩한다.
String className = ...; Class aClass = loader.loadClass(className);
위의 두 코드가 같은 클래스 로더를 통해 로딩되었을 때에는 기능적으로 동일하지만, 첫번째 블럭은 코드가 위치해 있는 로더를 통해서 클래스를 로드하는 반면, 두번째 블럭은 로더 변수가 명시한 클래스 로더를 통해 클래스를 로드하게 된다. 이 두 경우 모두, 클래스의 인스턴스를 생성하기 위해서는 newInstance
와 같은 메소드를 호출해야 한다.
호출하는 클래스의 클래스 로더가 변하지 않는다는 가정하에, Class.forName
메소드를 반복호출하면 같은 클래스를 로드하게 된다. 마찬가지로 loader.loadClass
를 반복적으로 호출해도 같은 클래스를 로드한다. 하지만 두번째 블럭은 클래스를 리로드할 수 있게 한다. 이를 위해서 새로운 ClassLoader
를 생성한다.
String className = ...; // create new loader instance ClassLoader loader = ...; Class aClass = loader.loadClass(className);
위의 코드 블럭에서는 loadClass
의 호출들 사이에 새로운 로더가 만들어진다. 만약 호출 중에 className
의 클래스 정의가 바뀐다면 두번째 호출에서는 클래스의 새로운 버전이 로드된다.
이 메카니즘을 사용하기 위해 RunIt
프로그램을 변경하면, 프로그램을 컴파일, 실행한 후에 소스를 편집하는 것이 가능하고, 이 때 출력값은 이전의 소스가 아닌 새로운 소스에 부합하는 값이다.
이제 남은 것은 어디에서 추상 클래스인 ClassLoader
를 받느냐 하는 것이다. permission support가 추가된 java.security
의 SecureClassLoader
와 java.net
의 URLClassLoader
는 미리 정의된 로더들이다. 이렇게 미리 정의된 두개의 로더 중에서 URLClassLoader
만이 그것의 생성자(constructor) 또는 static newInstance
메소드를 통해서 public construction을 지원하게 된다. URLClassLoader
에 관한 자세한 정보를 원하면 documentation을 참고한다.
URLClassLoader
를 생성하는 것은 URL
객체들의 배열의 생성을 수반한다. 이 URL
객체들은 커스텀 클래스 로더가 클래스를 찾기 위해 사용하는 장소의 역할을 한다. 배열의 요소를 지정할 때에는 CLASSPATH
환경변수의 패스 요소(path elements)를 지정했던 것과 비슷한 방법을 써야 한다. (이때 패스 요소는 윈도우에서는 a ;
, 유닉스에서는 a :
로 분리된다.) URL
배열의 개개의 요소는 지역적으로 혹은 원거리 호스트에 위치시킬 수 있다. "/
"으로 끝나는 모든 것들은 디렉토리로 추정할 수 있으며, 이 외의 모든 것들은 JAR 파일로 추정된다.
가령, 현재 디렉토리에서만 찾아지는 디폴트 클래스패스(classpath)와 같은 역할을 하는 ClassLoader
를 만들고 싶다면, 다음과 같이 코딩하면 된다.
File file = new File("."); ClassLoader loader = new URLClassLoader( new URL[] {file.toURL()} );
이 코딩의 첫번째 라인은 현재 디렉토리를 참고해서 File
객체를 만들고, 두번째 라인은 URLClassLoader
생성자를 호출하고 있다. 이렇게 넘겨진 생성자는 URL
객체(File
에 대한URL
)의 배열이 생성자의 인수로 넘겨진다.
RunIt
프로그램에 다음의 코드를 포함하여 리컴파일하고 실행시키면, RunIt
프로그램은 이를 실행하고 리로드하는 중에 로드된 클래스를 버릴 것이다. 지난 Tech Tip에서 소개했던 RunIt
프로그램과는 다르게 밑의 코드는 메인메소드를 불러내는 클래스의 인스턴스를 생성하지 않는다. 왜냐하면 메인메소드가 static이기 때문에 클래스의 인스턴스를 만들 필요가 없기 때문이다.
// Create new class loader // with current dir as CLASSPATH File file = new File("."); ClassLoader loader = new URLClassLoader( new URL[] {file.toURL()} ); // load class through new loader Class aClass = loader.loadClass(className.getText()); // run it Object objectParameters[] = {new String[]{}}; Class classParameters[] = {objectParameters[0].getClass()}; Method theMethod = aClass.getDeclaredMethod( "main", classParameters); // Static method, no instance needed theMethod.invoke(null, objectParameters);
다음은 RunItReload
라고 새롭게 이름붙인 코드이다.
import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.io.*; import java.net.*; import java.lang.reflect.*; public class RunItReload extends JFrame { JPanel contentPane; JScrollPane jScrollPane1 = new JScrollPane(); JTextArea source = new JTextArea(); JPanel jPanel1 = new JPanel(); JLabel classNameLabel = new JLabel("Class Name"); GridLayout gridLayout1 = new GridLayout(2,1); JTextField className = new JTextField(); JButton compile = new JButton("Go"); Font boldFont = new java.awt.Font( "SansSerif", 1, 11); public RunItReload() { super("Editor"); setDefaultCloseOperation(EXIT_ON_CLOSE); contentPane = (JPanel) this.getContentPane(); this.setSize(400, 300); classNameLabel.setFont(boldFont); jPanel1.setLayout(gridLayout1); compile.setFont(boldFont); compile.setForeground(Color.black); compile.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { try { doCompile(); } catch (Exception ex) { System.err.println( "Error during save/compile: " + ex); ex.printStackTrace(); } } }); contentPane.add( jScrollPane1, BorderLayout.CENTER); contentPane.add(jPanel1, BorderLayout.NORTH); jPanel1.add(classNameLabel); jPanel1.add(className);
// 아래와 같이, 컴포넌트를 panel에 붙이고 panel을 붙여야 diplay된다.
// --> contentPane.add(jPanel1, BorderLayout.NORTH);
// --> jPanel1.add(classNameLabel);
// --> jPanel1.add(className); jScrollPane1.getViewport().add(source); contentPane.add(compile, BorderLayout.SOUTH); } public static void main(String[] args) { Frame frame = new RunItReload(); // Center screen Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Dimension frameSize = frame.getSize(); if (frameSize.height > screenSize.height) { frameSize.height = screenSize.height; } if (frameSize.width > screenSize.width) { frameSize.width = screenSize.width; } frame.setLocation( (screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2); frame.show(); } private void doCompile() throws Exception { // write source to file String sourceFile = className.getText() + ".java"; FileWriter fw = new FileWriter(sourceFile); fw.write(source.getText()); fw.close(); // compile it int compileReturnCode = com.sun.tools.javac.Main.compile( new String[] {sourceFile}); if (compileReturnCode == 0) { // Create new class loader // with current dir as CLASSPATH File file = new File("."); ClassLoader loader = new URLClassLoader(new URL[] {file.toURL()}); // load class through new loader Class aClass = loader.loadClass( className.getText()); // run it Object objectParameters[] = {new String[]{}}; Class classParameters[] = {objectParameters[0].getClass()}; Method theMethod = aClass.getDeclaredMethod( "main", classParameters); // Static method, no instance needed theMethod.invoke(null, objectParameters); } } }
이 프로그램을 컴파일하고 실행할 때는 지난 Tech Tip 의 RunIt
프로그램에서 했던 것과는 약간 다른 방법을 써야 한다. 왜냐하면 커스텀 클래스 로더 는 현재 디렉토리를 리로드 가능한 클래스들의 출처가 되는 장소로 사용하고 있기 때문에 사용자가 같은 클래스패스로부터 실제 RunItReload
클래스를 로드할 수가 없다. 그렇지 않으면, 시스템클래스 로더는 같은 위치로부터 온 클래스 로더와 컴파일된 클래스를 로드하게 될 것이다. 따라서 사용자는 컴파일러가 RunItReload
를 위해서 컴파일된 클래스를 다른 위치로 보낼 수 있도록 해줘야 한다. 또한 ".
"을 포함하지 않은 클래스패스의 지점에서 프로그램을 실행시켜야 한다. 클래스패스를 컴파일하기 위해서는 tools.jar
파일을 포함시켜야 함을 잊지 말자. 밑의 명령문은 RunItReload
의 컴파일된 클래스 파일들을 XYZ
서브디렉토리로 보내고 있다. 여기서 subdirectory name은 마음대로 지정해 줘도 상관없다. (여기서는 커맨드 라인이 여러줄로 보이지만 실제로는 한줄에 기입되도록 한다.)
윈도우에서는,
mkdir XYZ javac -d XYZ -classpath c:\j2sdk1.4.2\lib\tools.jar RunItReload.java
유닉스에서는,
mkdir XYZ javac -d XYZ -classpath /homedir/jdk14/j2sdk1.4.2/lib/tools.jar RunItReload.java
homedir
을 실제 홈 디렉토리로 대체하라.
이 때 시스템이 지정된 패스를 찾을 수 없다는 에러가 뜨면, 컴파일 전에 XYZ
디렉토리를 생성했었는지 확인하라.
방금 전에 했던 것처럼 런타임 클래스패스에 tools.jar
파일을 포함시키고 실제 RunItReload
프로그램을 위한 XYZ
디렉토리 또한 포함시켜라. 프로그램을 실행하기 위해서는, 다음과 같은 커맨드를 첨부한다. (다시, 여기서는 커맨드 라인이 여러줄로 보이지만 실제로는 한줄에 기입되도록 한다.)
윈도우에서는,
java -classpath c:\j2sdk1.4.2\lib\tools.jar;XYZ RunItReload
유닉스에서는,
java -classpath /homedir/jdk14/j2sdk1.4.2/lib/tools.jar: XYZ RunItReload
XYZ 디렉토리는 이전의 javac
과정으로부터 영향을 받는다. 따라서 컴파일하기 위한 타겟 디렉토리(-d
뒤에 명시된 디렉토리)는 반드시 런타임 클래스패스와 매치되어야 한다.
이 프로그램은 실행되면서 GUI를 디스플레이하기 때문에 다음과 같은 사항을 할 수가 있다.
JTextField
안에 클래스를 컴파일하고자 한다면,Sample2
와 같은 클래스의 이름을 입력한다.JTextArea
에 소스코드를 입력한다. 여기Sample2
의 소스코드를보자.public class Sample2 { public static void main(String args[]) { System.out.println(new java.util.Date()); // System.out.println("Hello, World!"); } }
- Go 버튼을 클릭한다.
출력값은 콘솔창으로 보내진다. 가령 Sample2
는 다음과 같은 출력값을 산출한다.
Tue Aug 19 11:25:16 PDT 2003
날짜를 프린트하는 라인을 주석에 달고, "Hello World
"를 프린트하는 라인의 주석을 없앤다. 그리고 Go버튼을 클릭하면 콘솔창에 다음을 보게 된다.
Hello, World!
이전에 로드된 클래스를 unload한 새로운 클래스 로더가 생성되었기 때문에 여러분은 다른 라인이 디스플레이되는 것을 보게 되는 것이다.