 |
|
난이도 : 중급 Simon Burns, Java Technology Center Development Team, IBM Hursley Labs Lakshmi Shankar, Java Technology Center Development Team, IBM Hursley Labs
2007 년 4 월 03 일 네 편의 기술자료 시리즈를 통해 자바 개발 중에 발생할 수 있는 문제들을 이해하고 해결하는데 도움이 되는 자바™ 클래스 로딩 문제에 대해 설명합니다. Part 2에서는, IBM Hursley Labs의 Lakshmi Shankar와 Simon Burns가 매우 단순하지만, 개발자들을 당황시키는 몇 가지 예외(Exception)들을 분석합니다.
이 글에서는 애플리케이션을 실행할 때 생기는 다양한 클래스 로딩 예외(Exception)에 대해 살펴보도록 하겠다. 이러한 예외(Exception)는 일상적으로 보는 것이지만 자바 개발자들은 잘 이해를 못하는 경우가 있다. 각각의 예외들을 보면서, 이들이 어떻게 작동하는지, 원인은 무엇인지, 해결책은 어떤 것이 있는지를 상세한 예제들을 통해 설명하겠다. 가장 일반적인 ClassNotFoundException부터 시작해서 ExceptionInInitializerError 같은 잘 알려지지 않은 예외(Exception)로 옮겨가도록 하겠다.
이 글을 시작하기 전에, 클래스 로더 델리게이션 모델(class loader delegation model)과 클래스 링크(class linking)의 단계를 이해해야 한다. 본 시리즈의 첫 번째 기술자료를 참조하기 바란다.
ClassNotFoundException
ClassNotFoundException은 로딩 단계 동안 발생하는 가장 일반적인 유형의 클래스 로딩 예외(Exception)이다. 자바 스팩은 다음과 같이 ClassNotFoundException을 설명하고 있다.
애플리케이션이 다음과 같은 것을 사용하여, 스트링 이름을 통해 클래스에서 로딩을 시도할 때 예외(Exception)가 발생한다:
-
Class 클래스의 forName() 메소드
-
ClassLoader 클래스의 findSystemClass method()
-
ClassLoader 클래스의 loadClass() 메소드
하지만, 지정된 이름을 가진 클래스에 대한 정의는 찾을 수 없다.
따라서, ClassNotFoundException은 클래스를 로딩하려는 분명한 시도가 실패할 경우에 던져진다. Listing 1의 테스트 케이스는 ClassNotFoundException 예외(Exception)의 코드 예제이다.
Listing 1. ClassNotFoundExceptionTest.java
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassNotFoundExceptionTest {
public static void main(String args[]) {
try {
URLClassLoader loader = new URLClassLoader(new URL[] { new URL(
"file://C:/CL_Article/ClassNotFoundException/")});
loader.loadClass("DoesNotExist");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
} |
이 테스트 케이스는 클래스 로더(MyClassLoader)를 정의하는데, 이것은 존재하지 않는 클래스(DoesNotExist)를 로딩할 때 사용된다. 이것이 실행되면 다음과 같은 예외(Exception)가 발생한다.
java.lang.ClassNotFoundException: DoesNotExist
at java.net.URLClassLoader.findClass(URLClassLoader.java:376)
at java.lang.ClassLoader.loadClass(ClassLoader.java:572)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
at ClassNotFoundExceptionTest.main(ClassNotFoundExceptionTest.java:11) |
이 테스트에서는 loadClass()에 대한 호출을 사용하여 로드를 시도하기 때문에 ClassNotFoundException이 던져진다.
ClassNotFoundException을 통해서 이 클래스 로더는 클래스를 정의하는데 필요한 바이트코드(bytecode)가 클래스 로더가 찾고 있는 장소에 존재하지 않음을 알려준다. 이러한 예외(Exception)는 픽스가 간단하다. 이것이 IBM verbose 옵션으로 체크하여 사용 중인 classpath가 설정되는지를 확인한다. (이 옵션에 대한 자세한 내용은 본 시리즈의 첫 번째 기술자료를 참조하기 바란다.) classpath가 정확하게 설정되었지만, 계속 에러가 나타난다면, 원하는 클래스가 classpath에 없는 것이다. 이를 픽스하려면, 클래스를 디렉토리 또는 classpath에 지정된 JAR 파일로 옮기거나, 클래스가 classpath에 저장된 장소를 추가한다.
NoClassDefFoundError
NoClassDefFoundError는 로딩 단계 동안 클래스 로더에 의해 던져진 또 다른 예외(Exception)이다. JVM 스팩은 NoClassDefFoundError를 다음과 같이 정의하고 있다.
자바 가상 머신(Java virtual machine) 또는 ClassLoader 인스턴스가 클래스 정의 중 로딩을 시도하고(정상적인 메소드 호출의 일환으로, 또는 새로운 식을 사용하여 새로운 인스턴스를 생성하는 것의 일환으로), 이 클래스에 대한 어떤 정의도 찾을 수 없을 때 예외(Exception)가 던져진다.
현재 실행 중인 클래스가 컴파일 될 때 searched-for 클래스 정의가 존재했지만, 그 정의는 더 이상 찾을 수 없다.
NoClassDefFoundError는 실패한 클래스 로드의 결과로 생기는 예외(Exception)이다.
Listing 2에서 Listing 4까지의 테스트 케이스는 클래스 B의 로딩이 실패했기 때문에 만들어진 NoClassDefFoundError를 보여주고 있다.
Listing 2. NoClassDefFoundErrorTest.java
public class NoClassDefFoundErrorTest {
public static void main(String[] args) {
A a = new A();
}
} |
Listing 3. A.java
public class A extends B {
} |
Listing 4. B.java
이러한 리스팅에서 코드를 컴파일 했다면, B의 classfile을 제거하라. 이 코드가 실행되면 다음과 같은 에러가 생긴다.
Exception in thread "main" java.lang.NoClassDefFoundError: B
at java.lang.ClassLoader.defineClass0(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:810)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:147)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:475)
at java.net.URLClassLoader.access$500(URLClassLoader.java:109)
at java.net.URLClassLoader$ClassFinder.run(URLClassLoader.java:848)
at java.security.AccessController.doPrivileged1(Native Method)
at java.security.AccessController.doPrivileged(AccessController.java:389)
at java.net.URLClassLoader.findClass(URLClassLoader.java:371)
at java.lang.ClassLoader.loadClass(ClassLoader.java:572)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:442)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
at NoClassDefFoundErrorTest.main(NoClassDefFoundErrorTest.java:3)
|
Class A는 Class B를 오버라이드(override)한다. 따라서, Class A가 로딩될 때, 클래스 로더는 Class B를 내재적으로 로딩한다. Class B는 존재하지 않기 때문에, NoClassDefFoundError가 던져진다. 클래스 로더가 Class B를 로딩 명령을 받으면 (예를 들어, loadClass("B") 호출), ClassNotFoundException이 던져진다.
확실히, 이 특수한 예제에서 문제를 해결하려면, Class B는 합당한 클래스 로더의 classpath에 존재해야 한다. 이 예제는 단순하고도 비현실적이다. 많은 클래스들을 갖고 있는 복잡한 실제 시스템에서는, 패키징 또는 전개 동안 클래스가 소실될 때 이 같은 상황이 발생할 수 있다.
이 예제에서 A는 B를 확장한다. 하지만, A가 메소드 매개변수 또는 인스턴스 필드로서 B를 참조한다면 같은 에러가 여전히 발생한다. 두 클래스들 간 관계가 상속이 아닌 참조라면, A를 로딩하는 동안이 아닌, A를 처음 사용할 때 예외(Exception)가 던져진다.
ClassCastException
클래스 로더가 던질 수 있는 또 다른 예외(Exception)는 ClassCastException이다. 이것은 유형 비교에서 비호환 유형의 결과로 던져진다. JVM 스팩인 이 ClassCastException 예외(Exception)를 다음과 같이 정의하고 있다.
이는 코드가 인스턴스가 아닌 것의 하위 클래스로 객체를 캐스팅(cast)할 때 생기는 예외(Exception)이다.
Listing 5는 ClassCastException 예외(Exception)의 예제이다.
public class ClassCastExceptionTest {
public ClassCastExceptionTest() {
}
private static void storeItem(Integer[] a, int i, Object item) {
a[i] = (Integer) item;
}
public static void main(String args[]) {
Integer[] a = new Integer[3];
try {
storeItem(a, 2, new String("abc"));
} catch (ClassCastException e) {
e.printStackTrace();
}
}
} |
Listing 5에서, storeItem() 메소드가 호출되고, Integer 어레이, int, 그리고 스트링으로 전달한다. 하지만, 내부적으로는 메소드는 두 가지 일을 수행한다.
-
String 객체 유형을 Object 유형으로 던진다. (매개변수 리스트)
-
Object 유형을 Integer 유형으로 던진다. (메소드 정의)
프로그램이 실행될 때, 다음과 같은 예외(Exception)가 발생한다.
java.lang.ClassCastException: java.lang.String
at ClassCastExceptionTest.storeItem(ClassCastExceptionTest.java:6)
at ClassCastExceptionTest.main(ClassCastExceptionTest.java:12) |
이 테스트 케이스는 String 유형을 Integer로 변환하려고 했기 때문에 예외(Exception)가 생겼다.
테스트되고 있는 객체(Listing 5의 item)과 캐스팅 될 대상 클래스(Integer)가 있다면, 클래스 로더는 다음 규칙들을 체크한다.
-
정상 객체 (non-array): 이 객체는 대상 클래스의 인스턴스이거나 대상 클래스의 하위 클래스여야 한다. 대상 클래스가 인터페이스라면, 그 인터페이스를 구현할 경우 하위 클래스로 간주된다.
-
어레이 유형: 대상 클래스는 어레이 유형 또는
java.lang.Object, java.lang.Cloneable, 또는 java.io.Serializable이 되어야 한다.
위 규칙들 중 하나라도 위반하면, 클래스 로더는 ClassCastException을 던진다. 이 같은 예외(Exception)를 해결하는 가장 쉬운 방법은 객체가 캐스팅 될 유형이 위에 언급한 규칙에 순응하는지를 검사하는 것이다. 어떤 경우에는 클래스 캐스팅을 수행하기 전에 instanceof 체크를 사용하는 것이 좋다.
UnsatisfiedLinkError
클래스 로더는 네이티브 호출을 적절한 정의로 연결할 때 중요한 역할을 한다. UnsatisfiedLinkError는 프로그래밍이 존재하지 않는 라이브러리나 잘못 배치된 기본 라이브러리를 로딩할 때 링크 단계(linking phase)의 끝에서 발생한다. JVM 스팩은 UnsatisfiedLinkError를 다음과 같이 설명하고 있다:
JVM이 native로 선언된 메소드의 적절한 기본 언어 정의를 찾을 수 없을 때 발생한다.
기본 메소드가 호출될 때, 클래스 로더는 그 메소드를 정의하는 기본 라이브러리를 로딩하려고 한다. 그 라이브러리를 찾을 수 없을 경우 에러가 생긴다.
Listing 6은 UnsatisfiedLinkError의 테스트 케이스이다.
Listing 6. UnsatisfiedLinkError.java
public class UnsatisfiedLinkErrorTest {
public native void call_A_Native_Method();
static {
System.loadLibrary("myNativeLibrary");
}
public static void main(String[] args) {
new UnsatisfiedLinkErrorTest().call_A_Native_Method();
}
} |
이 코드는 call_A_Native_Method() 메소드를 호출하는데, 이것은 기본 라이브러리인 myNativeLibrary에서 정의된다. 이 라이브러리는 존재하지 않기 때문에, 프로그램이 실행될 때 다음과 같은 에러가 발생한다.
The java class could not be loaded. java.lang.UnsatisfiedLinkError:
Can't find library myNativeLibrary (myNativeLibrary.dll)
in sun.boot.library.path or java.library.path
sun.boot.library.path=D:\sdk\jre\bin
java.library.path= D:\sdk\jre\bin
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:2147)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:2006)
at java.lang.Runtime.loadLibrary0(Runtime.java:824)
at java.lang.System.loadLibrary(System.java:908)
at UnsatisfiedLinkErrorTest.<clinit>(UnsatisfiedLinkErrorTest.java:6)
|
기본 라이브러리의 로딩은 Listing 6에서 나온 UnsatisfiedLinkErrorTest의 클래스 로더인 System.loadLibrary()를 호출하는 클래스의 클래스 로더에 의해 초기화 된다. 이것이 어떤 클래스 로더인지에 따라, 다른 위치가 검색된다.
- 부트스트랩 클래스 로더에 의해 로딩된 클래스의 경우,
sun.boot.library.path가 검색된다.
- 확장 클래스 로더에 의해 로딩된 클래스의 경우,
java.ext.dirs가 검색되고, 그 뒤를 이어 sun.boot.library.path와 java.library.path가 검색된다.
- 시스템 클래스 로더에 의해 로딩된 클래스의 경우,
sun.boot.library.path가 검색되고, 그 뒤를 이어 java.library.path가 검색된다.
Listing 6에서, UnsatisfiedLinkErrorTest 클래스는 시스템 클래스 로더에 의해 로딩된다. 참조된 기본 라이브러리를 로딩하기 위해 이 클래스 로더는 sun.boot.library.path와 java.library.path를 검색한다. 이 라이브러리는 이 두 개의 위치에서 사용할 수 없으므로, 클래스 로더는 UnsatisfiedLinkageError를 던진다.
라이브러리 로딩에 사용된 클래스 로더를 이해했다면, 이 라이브러리를 올바른 위치에 두어서 이러한 유형의 문제를 해결할 수 있다.
ClassCircularityError
JVM 스팩은 ClassCircularityError를 다음과 같이 설명하고 있다:
클래스나 인터페이스는 자기 자신의 하위 클래스나 하위 인터페이스가 되므로 클래스나 인터페이스는 로딩될 수 없다.
이 에러는 링크 단계에서 생긴다. 자바 컴파일러는 이와 같은 순환적인 상황이 발생하지 않도록 하기 때문에 약간 이상한 에러이다. 하지만, 이 에러는, 클래스들을 개별적으로 컴파일하고 이들을 한데 모을 때 발생할 수 있다. 다음과 같은 시나리오를 생각해 보자. 우선, Listing 7과 8에서 클래스를 컴파일 한다.
Listing 7. A.java
public class A extends B {
} |
Listing 8. B.java
그리고 나서, Listing 9와 10에서 클래스를 개별적으로 컴파일 한다.
Listing 9. A.java
Listing 10. B.java
public class B extends A {
} |
마지막으로, Listing 7에서 Class A와 Listing 10에서 Class B를 가져다가, A 또는 B를 로딩하는 애플리케이션을 실행한다. 있을 법 하지 않는 상황처럼 보이지만, 다양한 많은 부분들이 결합된 복잡한 시스템에서는 가능한 일이다.
이 문제를 해결하려면 순환적인(cyclic) 클래스 계층(hierarchy)을 피해야 한다.
ClassFormatError
JVM 스팩에서는 ClassFormatError를 다음과 같이 설명하고 있다:
요청 받은 컴파일 된 클래스 또는 인터페이스를 지정하는 바이너리 데이터가 잘못 구성되었다.
이 예외(Exception)는 클래스 로딩의 링크 단계 중 확인 단계(verification stage) 동안 발생한다. 바이너리 데이터는 바이트코드가 변경되었을 경우, 예를 들어, 메이저 넘버 또는 마이너 넘버가 변경되었을 경우에 잘못 구성될 수 있다. 바이트코드가 교묘하게 해킹되었거나, 네트워크를 통해서 클래스 파일을 전송할 때 에러가 발생할 경우 이러한 상황이 생길 수 있다.
이러한 문제를 해결하는 유일한 방법은 재컴파일을 통해서, 수정된 bytecode를 얻는 것이다.
ExceptionInInitializerError
JVM 스팩은 ExceptionInInitializer를 다음과 같이 설명하고 있다:
- 이니셜라이저가
E라는 예외(Exception)를 갑자기 던지고 종료하고, E의 클래스가 Error 또는 이것의 하위 클래스들 중 하나가 아니라면, ExceptionInInitializerError 클래스의 인스턴스가 E라는 인자와 함께 생성되어 E를 대신하여 사용된다.
- JVM이
ExceptionInInitializerError 클래스에서 새로운 인스턴스를 만들기를 시도하지만 Out-Of-Memory-Error가 발생하여 그렇게 할 수 없을 경우, OutOfMemoryError 객체가 대신 던져진다.
Listing 8의 코드는 ExceptionInInitializerError 예제이다.
Listing 8. ExceptionInInitializerErrorTest.java
public class ExceptionInInitializerErrorTest {
public static void main(String[] args) {
A a = new A();
}
}
class A {
// If the SecurityManager is not turned on, a
// java.lang.ExceptionInInitializerError will be thrown
static {
if(System.getSecurityManager() == null)
throw new SecurityException();
}
} |
정적 코드 블록에 예외(Exception)가 발생할 때, ExceptionInInitializerError를 사용하여 예외(Exception)는 자동으로 포착되고 래핑된다. 아웃풋은 아래와 같다.
Exception in thread "main" java.lang.ExceptionInInitializerError
at ExceptionInInitializerErrorTest.main(ExceptionInInitializerErrorTest.java:3)
Caused by: java.lang.SecurityException
at A.<clinit>(ExceptionInInitializerErrorTest.java:12)
... 1 more |
이 에러는 클래스 로딩의 초기화 단계 동안에 발생한다. 이를 픽스하는 방법은 ExceptionInInitializerError(Caused by:: 아래 스택 트레이스에서 볼 수 있음)를 일으켰던 예외(Exception)를 검사하여 이 예외(Exception)를 중지하는 방법을 찾을 수 있다.
다음 편 예고 이 글에서는 다양한 클래스 로딩 예외(Exception)들에 대해 설명했다. 다음 글에서는 보다 복잡한 애플리케이션을 실행할 때 발생하는 클래스 로딩 문제들에 대해 알아보기로 하겠다.
기사의 원문보기
참고자료 교육
제품 및 기술 얻기
토론
필자소개  | 
|  | Simon Burns는 IBM Hursley Labs의 Java Technology Centre 내 Persistant Reusable JVM 팀의 리더이자 컴포넌트 소유자이다. 3년 동안 JVM 개발 분야에서 일했으며, Persistant Reusable JVM 기술과 z/OS 플랫폼 전문가이다. CICS와 긴밀히 협력하여 이러한 기술들을 활용하고 있다. 오픈 소스 프로젝트의 일부인 OSGI 프레임웍 작업을 하고 있다. 현재 컴포넌트화 분야에서 일하고 있다. |
 | 
|  | Lakshmi Shankar는 IBM Hursley Labs(영국) 소속 소프트웨어 엔지니어이다. IBM에 입사한 지 3년이 지났으며 자바 성능, 테스트, 개발 분야에서 다양한 경력을 쌓았다. 최근까지 IBM 자바의 Class Loading 소유자였다. 현재 Information Management 팀의 개발자로 근무하고 있다. |
기사에 대한 평가
|