[Java] Class class를 이용한 패키지 내부의 클래스 가져오기(클래스 동적 로딩)

자바는 뭔가 C#보다 엄격한 면이 있다. 자바 파일(.java)당 퍼블릭클래스가 하나뿐이라던가 하는? 그거 말고도 패키지로 묶으면 해당 패키지의 이름과 똑같은 디렉터리 내부에 해당 클래스가 존재해야한다는 것도 그렇다.

가령 다음과 같은 코드를 보자.
[code language=”java” title=”MainClass.java”]
import mypackage.*;

public class MainClass {
public static void main(String[] args){
SuperClass s = new SubClass1();
SuperClass s2 = new SubClass2();
s.test();
s2.test();
}
}
[/code]

[code language=”java” title=”mypackage/SuperClass.java”]
package mypackage;

public abstract class SuperClass{
public void test(){
System.out.println("SuperClass test method");
}
}
[/code]

[code language=”java” title=”mypackage/SubClass1.java”]
package mypackage;

public class SubClass1 extends SuperClass {
@Override
public void test(){
System.out.println("SubClass1 test method");
}
}
[/code]

[code language=”java” title=”mypackage/SubClass2.java”]
package mypackage;

public class SubClass2 extends SuperClass {
@Override
public void test(){
System.out.println("SubClass2 test method");
}
}
[/code]

처음 컴파일 할때에는 SubClass1이나 SubClass2가 SuperClass에 대한 정보를 알아야 하므로 다음과 같이 컴파일한다.
[code language=”bash”]
javac SubClass1.java SuperClass.java
javac SubClass2.java Superclass.java
[/code]

그러면 .class파일이 생성된다.
뭔가 자바를 배울 때 이클립스로 슈슈슝하다보니 클래스를 분할하면 어떡해야하나…하고 보니 저렇게 하면 된다. 근데 만약 패키지 내부의 클래스들로 일관적인 작업을 하는데, 패키지 내부의 클래스가 늘어날 수도 있다면, 메인클래스를 재컴파일하지 않으려면 어떡해야할까에 대해서 고민을 해봤는데 Class 클래스를 이용하면 가능하다고 한다.
Class 클래스는 클래스에 대한 정보를 가지고 있는 클래스이다.(클래스 클래스는 클래스의 정보를 담고있는 클래스다 (?))
패키지 내부에는 자바 바이너리파일(.class)가 위치하게 되는데, 해당 파일들을 찾아 읽어들여서 이 Class 클래스를 이용해서 인스턴스화 하면 된다.
아직 jar파일에 대해서는 안해보고 디렉터리로 된 패키지를 불러와보았다.

1. 패키지의 URL 가져오기
현재 실행되는 스레드(보통 메인스레드)의 클래스로더를 이용해서 특정 패키지(아마도 import 된 패키지만 가능할 듯 싶다)의 URL을 가져온다.
보통 패키지 import시에 패키지와 패키지, 클래스의 구분자로 .을 사용하는데, .은 모두 /로 바꾸어야한다.
[code language=”java”]
import java.net.URL;
// In main thread
packageName = "(가져오려는 패키지 경로, e.g. java.util)";
String packageNameSlashed =
"./" + packageName.replace(".", "/");
URL packageDirURL =
Thread.currentThread().getContextClassLoader().getResource(packageNameSlashed);
[/code]

2. 패키지 디렉터리 URL을 이용해서 내부의 파일들을 불러오기
위에서 구한 디렉터리의 URL을 이용해서 디렉터리의 전체 경로를 가져올 수 있다.
[code language=”java”]
String directoryString = packageDirURL.getFile();
[/code]

3. 패키지 디렉터리안의 자바 바이너리파일(.class)의 이름을 가져와 패키지 이름과 합쳐서 클래스를 로드
디렉터리의 위치를 알아냈으니 해당 디렉터리를 열어서, 그 중에 자바 바이너리 파일인 것(파일의 확장자가 .class인것)만 골라서 Class 클래스를 이용해 로드한다.
[code language=”java”]
import java.io.File;

File directory = new File(directoryString);
if(directory.exists()){
String[] files = directory.list();
for(String fileName : files){
fileName = fileName.substring(0, fileName.length() – 6); // 확장자 삭제
try{
Class c = Class.forName(packageName + "." + fileName); // Dynamic Loading
list.add(c); // List<Class> list 에 넣는다
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
[/code]

이제 list를 반환해서 사용하면 된다.

4. Class 인스턴스의 newInstance() 메서드를 이용하여 인스턴스를 생성한다.
그러나 여기서 문제는 인스턴스를 생성할 수 없는 클래스(abstract, interface)가 존재한다는 점이다.

* 리플렉션을 이용하여 Class의 종류 판별하기
java.lang.reflect.Modifier를 이용하면 해당 클래스에 어떤 modifier가 붙어있는 지 알아낼 수 있다.
Class 인스턴스의 메서드 중에 getModifiers()라는 메서드가 존재하는데, 얘는 4바이트 정수로 된 Modifier 정보를 반환하는 메서드이다. 이 정보는 java.lang.reflect.Modifier내에 정의 된 다음과 같은 상수들이 OR연산으로 합쳐진 것이다.

  • ABSTRACT
  • FINAL
  • INTERFACE
  • NATIVE
  • PRIVATE
  • PROTECTED
  • PUBLIC
  • STATIC
  • STRICT
  • SYNCHRONIZED
  • TRANSIENT
  • VOLATILE

native, strict, volatile빼고는 다 배웠는데 transient가 뭐였는지 기억이 안난다. 직렬화랑 관련된거 였던거 같은데…
그리고 Modifier클래스의 static 메서드로 어떤 modifier(int)가 위의 것들을 포함하는지 알아낼 수 있다. 가령, 어떤 abstract class AbstractTest에 대해
[code language=”java”]
java.lang.reflect.Modifier.isAbstract(AbstractTest.class);
[/code]
는 true를 반환하게 된다.
따라서 abstract를 걸러내려면 위에서 파일별로 가져올 때 isAbstract 메서드를 이용해서 구분하면 된다.
기타 자세한 메서드는 이곳을…

그래서 작성해본 메서드는 이렇게 생겼다.

[code language=”java”]
import java.util.Set;
import java.util.HashSet;
import java.net.URL;
import java.io.File;
import java.lang.reflect.Modifier;

private static Set<Class> getClasses(String packageName){
Set<Class> classes = new HashSet<Class>();
String packageNameSlash = "./" + packageName.replace(".", "/");
URL directoryURL = Thread.currentThread().getContextClassLoader().getResource(packageNameSlash);
if(directoryURL == null){
System.err.println("Could not retrive URL resource : " + packageNameSlash);
return null;
}

String directoryString = directoryURL.getFile();
if(directoryString == null){
System.err.println("Could not find directory for URL resource : " + packageNameSlash);
return null;
}

File directory = new File(directoryString);
if(directory.exists()){
String[] files = directory.list();
for(String fileName : files){
if(fileName.endsWith(".class")){
fileName = fileName.substring(0, fileName.length() – 6); // remove .class
try{
Class c = Class.forName(packageName + "." + fileName);
if(!Modifier.isAbstract(c.getModifiers())) // add a class which is not abstract
classes.add(c);
} catch (ClassNotFoundException e){
System.err.println(packageName + "." + fileName + " does not appear to be a valid class");
e.printStackTrace();
}
}
}
} else {
System.err.println(packageName + " does not appear to exist as a valid package on the file system.");
}

return classes;
}
[/code]

여기까지는 이 페이지의 코드를 보고 썼습니다.

이제 Class 인스턴스들을 로드했으니 이를 통해 newInstance로 인스턴스를 생성해서 사용하면 된다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다