java-school logo
Last Modified 2017.9.15

예외

예외(exception)란 일반적이지 않음을 의미한다. '에스컬레이터가 갑자기 멈추는 예외적인 상황'이라고 할 때 바로 그 예외다.

자바 API를 사용하면서 발생할 수 있는 모든 예외 상황마다 그에 해당하는 예외 클래스가 있다. 자바 API 클래스가 실행 도중 예외가 발생하면, JVM은 해당하는 예외 클래스로부터 객체를 생성하여 예외를 발생시킨 코드에 던진다. 예외가 발생한 코드에 예외 객체를 생성하여 던지는 것까지가 JVM의 몫이고, 발생한 예외 객체를 어떻게 핸들링할지는 프로그래머의 몫이다. 프로그래머는 생성된 예외 객체를 try와 catch 그리고 finally 블럭1을 적절히 사용하여 컨트롤 할 수 있다.

자바 프로그램에서 에러(error)란?
예외도 엄밀하게 에러이지만 자바에서는 예외와 에러를 구별한다. 자바에서 에러는 "프로그램적으로 컨트롤 할 수 없는 에러"라고 볼 수 있다. 에러가 발생할 때 프로그래머가 할 수 있는 일은 거의 없다. 에러가 발생하면 프로그램은 종료된다.

예외를 컨트롤하는 코드

try {
   //A나 B 예외가 발생할 수 있는 코드
   //여기서 B는 계층적으로 A보다 위에 있다고 가정
} catch (A_익셉션_클래스 e) { //e는 A_익셉션 객체의 레퍼런스가 할당된다.
   //A 예외가 발생했을 때 실행되는 코드
} catch (B_익셉션_클래스 e) {
   //B 예외가 발생했을 때 실행되는 코드
} finally {
   //예외가 발생하던 안하던 무조건 실행되는 코드
}
try 블록
예외가 발생할 수 있는 코드를 감싸는 블록으로 단독으로 쓸 수 없다. catch 블록이나 finally 블록과 함께 사용한다.
catch 블록
에러와 마찬가지로 예외 역시 아무런 조치가 없다면 프로그램은 멈추게 된다. try 블록에서 발생한 예외를 핸들링하려면 catch 블록을 적절하게 사용해야 한다. 위처럼 catch 블록 여러 개를 중첩해서 발생한 익셉션에 따라 섬세한 컨트롤이 가능하다. 만약 A_익셉션_클래스가 B_익셉션_클래스보다 상위에 있다면(익셉션 클래스는 상속을 이용해서 계층적으로 만들어졌다), B_익셉션_클래스 타입의 예외를 잡는 catch 문이 실행될 수 없다는 논리적 오류때문에 컴파일 에러가 발생한다.
finally 블록
익셉션이 발생하든 발생하지 않든 무조건 실행되는 블록이다. finally 블록은 사용하지 않거나 하나만 사용할 수 있다.

익셉션 클래스 계층 구조

Exception API

자주 발생하는 익셉션

발생하는 익셉션
ArithmeticException
	int a = 12/0;
	
NullPointerException
	Integer d = null;
	int val = d.intValue();
	
NegativeArraySizeException
	int arr = new int[-1];
	
ArrayIndexOutOfBoundException
	int[] arr = new int[2];
	arr[2] = 1;
	

예외 매커니즘

아래와 같이 MethodA에서 MethodB를, MethodB에서 MethodC을 호출한다고 가정하자. 익셉션 매커니즘 MethodC에서 익셉션이 발생했고 MethodC에 발생한 익셉션을 잡는 catch 문이 있다면 프로그램은 멈추지 않고 계속 진행된다. MethodC에서 해당 익셉션을 잡는 catch 문이 없다면 발생한 익셉션은 MethodC를 호출한 MethodB에 전달된다. MethodB에 전달된 익셉션을 잡는 catch 문이 없다면 MethodB를 호출한 MethodA로 익셉션이 전달된다.

메서드 선언 부에 쓰인 throws 익셉션_클래스

메서드 선언 부에서 throws 익셉션_클래스 문은 메서드가 실행 중, throws 다음의 익셉션이 발생하면 발생한 익셉션이 메소드 밖으로 나올 수 있게 한다. 따라서, 이 메서드를 호출하는 곳에서 throws 다음에 나오는 익셉션을 적절히 처리하는 코드가 없다면 컴파일 에러를 만나게 된다. 익셉션을 적절히 처리하는 첫 번째 방법은, 익셉션을 반환할 수 있는 메서드를 호출하는 메서드는 메서드 선언부에 throws 키워드를 사용하여 전달받은 익셉션을 자신을 호출한 메서드에 전달하는 방법이다. 두 번째 방법은 자신에게 전달된 익셉션을 자신의 메서드 몸체에서 try ~ catch 문을 사용하여 직접 처리하는 방법이다. 첫 번째나 두 번째 방법 중 하나를 선택하지 않으면 컴파일 에러가 난다. 발생하는 익셉션이 RuntimeException이거나 RuntimeException의 서브 클래스인 경우, 위 제약을 모두 무시해도 컴파일 에러는 발생하지 않는다.2

실습

RuntimeException이나 RuntimeException의 서브 클래스를 unchecked 익셉션이라 한다. 다음은 unchecked 익셉션이 발생하는 예제다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
        
	public static void method2() {
		method3();
	}

	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
        
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

예제를 실행하면 ArithmeticException 익셉션이 JVM까지 도달한다. JVM은 아래처럼 익셉션이 발생한 스택을 위에서부터 차례로 출력한다. 메인 메서드 마지막 줄이 실행돼서 '정상종료'을 출력하지 못했기에 정상적인 종료가 아니다.

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at net.java_school.exception.Test.method3(Test.java:14)
	at net.java_school.exception.Test.method2(Test.java:10)
	at net.java_school.exception.Test.method1(Test.java:6)
	at net.java_school.exception.Test.main(Test.java:19)

method3()에 try ~ catch 문을 사용하여 익셉션을 핸들링하는 코드를 추가한다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	public static void method2() {
		method3();
	}
	public static void method3() {
		try {
			int a = 12 / 0;
			System.out.println(a);
		} catch (ArithmeticException e) {
			System.out.println(e.getMessage());
		}
	}
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");	
	}
}

실행하면 catch 블록의 System.out.println(e.getMessage());문이 실행되어 '/ by zero'가 출력되고 method1()이 정상 종료된다. 그다음 메인 메소드의 마지막 줄이 실행되고 '정상종료'를 출력한다.

/ by zero
정상종료

Ctrl + Z로 코드를 되돌리고 method2()에 try ~ catch 블록에서 익셉션을 잡도록 수정한다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	public static void method2() {
		try {
			method3();
		} catch (ArithmeticException e) {
			System.out.println(e.getMessage());
		}
	}
	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}
}

method3()에서 발생한 익셉션 객체가 method2()까지 전달된다. 반환값이 있는 메서드가 반환값을 반환하는 것과 비슷하다. method2()의 catch 블록이 실행되어 method2()가 종료된다. 이어서 메인 메소드의 마지막 줄이 실행된다.

/ by zero
정상종료

Ctrl + Z로 코드를 되돌리고 이번에는 method1()에서 익셉션을 다루도록 수정한다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		try {
			method2();
		} catch (ArithmeticException e) {
			System.out.println(e.getMessage());
		}
	}
	public static void method2() {
		method3();
	}
	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}
}

method3()에서 발생한 익셉션이 method1()까지 전달되고 method1()에서 익셉션을 제대로 잡았기 때문에 비정상적인 종료를 피할 수 있었다.

/ by zero
정상종료

코드를 되돌리고 이번에는 메인 메서드에서 익셉션을 핸들링한다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} catch (ArithmeticException e) {
			System.out.println(e.getMessage());
		}
	
		System.out.println("정상종료");
	}

}

콘솔의 결과는 같지만 메인까지 익셉션이 전달되었고 메인에서 익셉션을 잡았기에 정상종료될 수 있었다.

/ by zero
정상종료

try 블록과 대부분 같이 쓰이지만 catch블록은 필수는 아니다. 하지만 익셉션 객체를 잡을려면 반드시 필요하다. 다음 예제는 catch 블록을 제거해 보았다. try 블록은 단독으로 쓰일 수 없으므로 대신 finally 블록을 함께 사용했다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} finally {
			System.out.println("finally블록 실행");
		}
		
		System.out.println("정상종료");
	}

}

finally 블록은 익셉션이 발생하건 안 하건 실행된다. 하지만 catch 블록이 없으니 익셉션 객체는 JVM까지 전달되고 프로그램은 비정상적으로 종료된다. 콘솔 화면에서 finally 블록이 실행되고 익셉션이 JVM까지 전달되는 것을 확인한다.

finally블록 실행
Exception in thread "main" java.lang.ArithmeticException: / by zero
        at net.java_school.exception.Test.method3(Test.java:14)
        at net.java_school.exception.Test.method2(Test.java:10)
        at net.java_school.exception.Test.method1(Test.java:6)
        at net.java_school.exception.Test.main(Test.java:20)

다시 catch 블록을 사용했는데, 이번에는 Exception 형을 잡도록 catch 블록이 만들었다. 이러면 try 블록안의 어떠한 익셉션이 발생해도 이 catch 블록이 모두 잡을 수 있다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		} finally {
			System.out.println("finally블록 실행");
		}
		
		System.out.println("정상종료");
	}

}

익셉션이 발생했기에 catch 블록이 실행되고 finally 블록이 실행된다. catch블록에서 발생한 익셉션을 잡았기 때문에 익셉션은 더 이상 어디에도 전달되지 않는다.

/ by zero
finally블록 실행
정상종료

catch블록이 마치 if ~ else if문처럼 겹겹이 쓸 수 있다. 하지만 이때 익셉션 클래스의 계층 관계에 주의해야 한다. 다음 코드는 컴파일이 되지 않는다. 익셉션이 try블록에서 발생하면 위에서 아래로 순서대로 catch블록이 해당 익셉션을 잡으려 하는데 먼저 나오는 익셉션이 나중에 나오는 익셉션에 비해 계층적으로 위에 있다면 아래까지 코드가 진행될 이유가 없다는 컴파일 에러이다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		} catch (ArithmeticException e) { 
			System.out.println(e.getMessage());
		} finally {
			System.out.println("finally블록 실행");
		}
		
		System.out.println("정상종료");
	}

}

아래처럼 catch 블록의 순서를 바꾸면 컴파일이 된다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() {
		int a = 12 / 0;
		System.out.println(a);
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} catch (ArithmeticException e) {
			System.out.println(e.getMessage());
		} catch (Exception e) {
			System.out.println(e.getMessage());
		} finally {
			System.out.println("finally블록 실행");
		}
		
		System.out.println("정상종료");
	}

}

실행하면 catch(ArithmeticException e) {.. }블록이 실행되고 이 블록의 e.getMessage()가 표준출력메서드를 통해 콘솔에 출력된다. 그 다음 finally블록이 실행되고 메인의 마지막 라인이 실행된다.

/ by zero
finally블록 실행
정상종료

같은 예제를 checked 익셉션 예제로 아래와 같이 수정한다. 강조된 부분에서 컴파일 에러가 난다. 이클립스의 컴파일 에러 메시지는 Unhandled exception type ClassNotFoundException이다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() {
		Class.forName("java.lang.Boolean");
	}
	
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

Class.forName("문자열");은 새로운 자바 문법이 아니다. Class란 클래스가 있고 이 클래스에는 forName("문자열")이라는 static 메서드가 있다. forName() 메서드의 인자는 자바 클래스의 공식 이름FQCN을 표현하는 문자열이다. forName("문자열")는 "문자열"에 해당하는 클래스를 클래스 로더로 하여금 로딩하게 한다. 그와같은 클래스를 찾지 못하면, JVM은 ClassNotFoundException 객체를 생성, Class.forName("문자열"); 라인에 던진다. 위 예제에서 forName("문자열") 메서드에 전달한 문자열 "java.lang.Boolean"은 자바 API에 속한 클래스이니 별도의 환경설정 없이 클래스 로더가 이 클래스를 찾을 것이다. 이 예제에서 Boolean 클래스의 용도는 중요하지 않다.

forName() 메서드는 throws ClassNotFoundException로 선언되어 있다. ClassNotFoundException은 RuntimeException을 상속하지 않은 checked 익셉션이다. 따라서 forName()을 호출하는 메서드는 ClassNotFoundException 익셉션을 핸들링하지 않으면 컴파일 에러가 발생한다. 컴파일러는 forName("문자열")에서 문자열에 해당하는 클래스가 클래스 패스에 있는지 검사하지 않는다.

이클립스의 코드 어시스트 기능을 이용하여 ClassNotFoundException 익셉션을 핸들링하는 코드를 구현하자. Class.forName()에 커서를 올리면 이클립스가 해결책을 제시하는데 두번째 방법을 클릭하면 코드는 아래와 같이 바뀌면서 컴파일 에러는 사라진다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() {
		try {
			Class.forName("java.lang.Boolean");
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

실행하면 어떠한 익셉션도 발생하지 않고 정상종료된다. java.lang.Boolean라는 클래스가 자바API에 존재하기 때문이다.

정상종료

Ctrl + ZUndo Text Change로 코드를 되돌된다. method2()에서 method3();에 컴파일 에러가 발생한다. 이클립스에서 Unhandled exception type ClassNotFoundException 에러 메시지를 확인할 수 있다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean");
	}
	
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

method3()가 throws ClassNotFoundException로 선언했기 때문에 method3()을 호출하는 메서드는 반드시 ClassNotFoundException을 핸들링해야 한다. 이번에도 컴파일 에러가 나는 부분에 커서를 두고 이클립스가 제시하는 해결책 중 두번째 방법을 클릭한다. 그러면 소스는 아래처럼 변경되고 컴파일 에러는 사라진다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() {
		try {
			method3();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean");
	}
	
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

코드를 되돌리고 이번에는 이클립스가 제시하는 첫 번째 방법을 클릭한다. 그러면 소스는 아래처럼 변경되는데 method1() 메서드의 method2();에서 컴파일 에러가 발생한다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		method2();
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean");
	}
	
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

커서를 컴파일 에러가 발생하는 곳에 두고 이클립스가 제시하는 해결책 중 두 번째 방법을 클릭한다. 코드는 아래처럼 변경된다.

package net.java_school.exception;

public class Test {

	public static void method1() {
		try {
			method2();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean");
	}
	
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

실행하면, 어떤 익셉션도 발생하지 않고 다음처럼 정상적으로 종료된다.

정상종료

코드를 되돌리고 method1()의 method2();에 커서를 대고 첫 번째 방법을 클릭하면 다음과 같이 코드가 변경되면서 메인 메서드에서 컴파일 에러가 발생한다.

package net.java_school.exception;

public class Test {

	public static void method1() throws ClassNotFoundException {
		method2();
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean");
	}
	
	public static void main(String[] args) {
		method1();
		System.out.println("정상종료");
	}

}

메인 메서드에서 컴파일 에러가 발생하는 곳에 커서를 두고 첫 번째 방법을 클릭한다. 소스는 다음과 같이 변경된다.

package net.java_school.exception;

public class Test {

	public static void method1() throws ClassNotFoundException {
		method2();
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean");
	}
	
	public static void main(String[] args) throws ClassNotFoundException {
		method1();
		System.out.println("정상종료");
	}

}

실행하면 어떤 익셉션도 없이 정상적으로 종료된다.

정상종료

익셉션이 발생하도록 코드를 수정해 보자. java.lang.Boolean을 java.lang.Boolean2로 수정한다. java.lang.Boolean2 클래스는 자바 API에 없다.

package net.java_school.exception;

public class Test {

	public static void method1() throws ClassNotFoundException {
		method2();
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean2");
	}
	
	public static void main(String[] args) throws ClassNotFoundException {
		method1();
		System.out.println("정상종료");
	}

}

실행하면 익셉션 객체가 메인 메서드까지 도달하고 메인 메서드에서도 익셉션을 잡지 않으므로 결국 JVM까지 전달된다. 그 결과 비정상적으로 프로그램은 종료된다.

Exception in thread "main" java.lang.ClassNotFoundException: java.lang.Boolean2
        at java.net.URLClassLoader$1.run(URLClassLoader.java:217)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:205)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:323)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:294)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:268)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:190)
        at net.java_school.exception.Test.method3(Test.java:13)
        at net.java_school.exception.Test.method2(Test.java:10)
        at net.java_school.exception.Test.method1(Test.java:6)
        at net.java_school.exception.Test.main(Test.java:18)

메인 메서드에서 익셉션을 잡도록 코드로 변경한다.

package net.java_school.exception;

public class Test {

	public static void method1() throws ClassNotFoundException {
		method2();
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean2");
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("정상종료");
	}

}

실행하면 e.printStackTrace(); 메서드는 콘솔에 아래와 같이 출력한다.

java.lang.ClassNotFoundException: java.lang.Boolean2
        at java.net.URLClassLoader$1.run(URLClassLoader.java:217)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:205)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:323)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:294)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:268)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:190)
        at net.java_school.exception.Test.method3(Test.java:14)
        at net.java_school.exception.Test.method2(Test.java:10)
        at net.java_school.exception.Test.method1(Test.java:6)
        at net.java_school.exception.Test.main(Test.java:19)
정상종료

메인에서 다음과 같이 catch 블록을 제거하면 컴파일 에러를 만난다. ClassNotFoundException은 checked 익셉션이므로, 발생할 수 있는 익셉션을 처리하지 코드가 없으면 컴파일 에러가 발생한다. checked 익셉션이 메서드 밖으로 나오려면, 메서드가 throws 익셉션_클래스로 선언해야 한다는 사실을 기억하자.

package net.java_school.exception;

public class Test {

	public static void method1() throws ClassNotFoundException {
		method2();
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean2");
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} finally {
			System.out.println("finally블록 실행");
		}
		System.out.println("정상종료");
	}

}

코드에 다시 catch 블록을 넣어 컴파일 에러를 피하도록 한다.

package net.java_school.exception;

public class Test {

	public static void method1() throws ClassNotFoundException {
		method2();
	}
	
	public static void method2() throws ClassNotFoundException {
		method3();
	}
	
	public static void method3() throws ClassNotFoundException {
		Class.forName("java.lang.Boolean2");
	}
	
	public static void main(String[] args) {
		try {
			method1();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} finally {
			System.out.println("finally블록 실행");
		}
		System.out.println("정상종료");
	}

}

실행하면 java.lang.Boolean2란 클래스는 없으므로 ClassNotFoundException 익셉션이 발생한다. catch 블록에서 e.printStackTrace();가 실행되고 finally 블록이 실행되고 메인의 마지막 라인이 실행된다.

java.lang.ClassNotFoundException: java.lang.Boolean2
        at java.net.URLClassLoader$1.run(URLClassLoader.java:217)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:205)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:323)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:294)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:268)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:190)
        at net.java_school.exception.Test.method3(Test.java:14)
        at net.java_school.exception.Test.method2(Test.java:10)
        at net.java_school.exception.Test.method1(Test.java:6)
        at net.java_school.exception.Test.main(Test.java:19)
finally블록 실행
정상종료

앞으로 e.printStackTrace();가 출력하는 형태의 메시지는 자주 보게 된다. 이럴 때는 자바 문법을 토대로 추측해서 문제를 해결해 가야 한다. 경우에 따라서는 구글이 답일 수 있다.

사용자 정의 익셉션

익셉션이 발생할 상황이 되면 JVM이 자바 API의 익셉션 클래스로부터 익셉션 객체를 생성하고 익셉션이 발생한 코드에 던진다고 했다. 그런데 이런 익셉션 클래스를 프로그래머가 필요에 따라 만들 수 있다. 이를 '사용자 정의 익셉션'이라 부른다. 아래는 사용자 정의 익셉션의 예제로, 은행 프로그램에서 잔고가 부족할 때 사용될 익셉션이다.3

package net.java_school.bank;

public class InsufficientBalanceException extends Exception {

	public InsufficientBalanceException() {
		super();
	}
	
	public InsufficientBalanceException(String message, Throwable cause,
		boolean enableSuppression, boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
	}
	
	public InsufficientBalanceException(String message, Throwable cause) {
		super(message, cause);
	}
	
	public InsufficientBalanceException(String message) {
		super(message);
	}
	
	public InsufficientBalanceException(Throwable cause) {
		super(cause);
	}

}

사용자 정의 익셉션은 JVM이 객체를 만들어 던지지 않는다. 그러므로 사용자 정의 익셉션은 코드로서 명시적으로 익셉션을 발생시켜야 한다. 사용자 정의 익셉션으로 부터 익셉션 객체를 만들어 던지는 코드는 다음과 같다.

throw new InsufficientBalanceException("잔액이 부족합니다.");

문제

아래와 같이 실행되는 클래스를 작성한다.

C:\ Command Prompt
C:\java\Exception\bin>java net.java_school.bank.Test
1000원 입금: java net.java_school.bank.Test d 1000
1000원 출금: java net.java_school.bank.Test w 1000

C:\java\Exception\bin>java net.java_school.bank.Test d 2100
최대 잔고액을 넘게 입금할 수 없습니다.
잔고는 1000

C:\>java net.java_school.bank.Test w 2100
잔액이 부족합니다.
잔고는 1000

메인 메서드 아래 //구현부를 추측해 보자.

package net.java_school.bank;

public class Test {
	public static void main(String[] args) {
		int MAX_BALANCE = 3000; //최대 잔고금액
		int balance = 1000; //초기 잔고
		int amount = 0; //입금액 또는 출금액
		
		if (args.length < 2) {
		System.out.println("1000원 입금: java net.java_school.bank.Test d 1000");
		System.out.println("1000원 출금: java net.java_school.bank.Test w 1000");
		return;
	}
	
	//구현부

}        

위의 문제에서 적절한 사용자 익셉션 클래스를 만들고 적용해 보자.

주석
  1. 블록이란 {로 시작해서 }로 끝나는 코드 단위를 의미한다. 자바에선 블록안에서 선언된 변수는 블록안에서만 유효하다.
  2. RuntimeException과 그 서브 클래스를 unchecked 익셉션, 그 외 익셉션을 checked 익셉션이라고 부른다.
  3. InsufficientBalanceException 사용자 정의 익셉션 클래스를 작성할 때 이클립스의 코드 어시스트 기능을 이용하면 쉽게 작성할 수 있다. 패키지와 클래스 선언부를 위에서처럼 작성한 후에 클래스 바디에 커서를 위치하고 오른쪽 마우스를 클릭한 후 나타난 메뉴에서 Source, Generate constructor from superclass...를 차례로 선택하면 위와 같은 코드를 얻을 수 있다.
참고