자바(JAVA) - 예외 처리(Exception Handling)
자바에서 예외(exception) 란 사용자의 잘못된 조작이나 개발자의 코딩 실수로 인해 발생하는 프로그램 오류를 말합니다. 예외가 발생되면 프로그램은 곧바로 종료된다는 점에서 에러와 동일하나, 예외는 예외 처리를 통해 프로그램을 종료하지 않고 정상 실행 상태가 유지되도록 할 수 있습니다.
자바의 예외에는 일반 예외와 실행 예외가 있고, 각각 Checked Exception
과Unchecked Exception
으로 부를 수 있습니다. 전자인 일반 예외
, 즉 Checked Exception은 개발자가 반드시 예외 처리를 직접 진행해야 합니다.
후자인 실행 예외
, 즉 Unchecked Exception은 개발자가 예외처리를 직접 하지 않아도 됩니다. 명시적인 예외 처리가 강제되는 것이 아니므로 unchecked 라고 부릅니다.
모든 예외 클래스는 java.lang.Exception 클래스를 상속받습니다.
- Exception 클래스 자체는 checked exception 이다.
- Exception 클래스는 Throwable 클래스의 자식 클래스이다.
- Exception 클래스의 자식 클래스 중 RuntimeException 클래스는 Unchecked 이다.
- 그 외 checked exception이 있다.
실행 예외 (Unchecked Exception)의 종류
RuntimeException의 자식 클래스들 모두 포함, Unchecked Exception이다. try-catch문으로 예외 처리를 직접 하기보다는 예외가 발생하지 않도록 프로그래머가 주의해야 한다.
1-1. NullPointerException(java.lang.NullPointerException) 객체 참조가 없는 상태일 때 발생합니다. null 값을 갖는 참조 변수로 객체 접근 연산자인 도트(.)를 사용했을 때 발생합니다. 객체가 없는 상태에서 객체를 사용하려 했으니 당연히 예외가 발생합니다.
1-2. ArrayIndexOutOfBoundsException 배열에서 인덱스 범위를 초과하여 사용할 때 발생합니다. int[] arr = new int[3];
이라 해 놓고 arr[4]=5;
같은 대입 연산을 시도할 때 발생할 수 있습니다.
1-3. NumberFormatException 문자열로 되어 있는 데이터를 숫자로 변경하는 경우가 많은데, 문자열을 숫자로 변환하는 방법 중 가장 많이 사용되는 코드는 Integer.parseInt(String s) 메소드와 Double.parseDouble(String s) 메소드입니다. 그런데 아래 코드를 확인해 보겠습니다.
1
2
3
4
5
....
String data1 = "100";
String data2 = "이건안돼요";
int val1 = Integer.parseInt(data1);
int val2 = Integer.parseInt(data2); //NumberFormatException 발생
매개값인 문자열이 숫자로 변환될 수 있다면 숫자를 정상적으로 리턴하지만, 숫자로 변환할 수 없는 문자열이 포함되어 있으면 java.lang.NumberFormatException
을 발생시킵니다.
1-4. ClassCastException 허용되지 않는데 억지로 타입 변환을 시도할 경우 발생합니다. 추상클래스 Animal을 상속하는 Dog, Cat 클래스와 RemoteControl 인터페이스를 상속하는 Television, Audio 클래스가 있다고 가정해 보겠습니다. 그리고 예외가 발생하는 상황은 아래 코드와 같습니다.
1
2
3
4
5
6
7
8
Animal animal = new Dog();
Dog dog = (Dog) animal; // 문제 없음
RemotControl ex1 = new Television();
Television ex2 = (Television) ex1; // 문제 없음
Animal animal = new Animal();
Dog dog = (Dog) animal; // ClassCastException 발생
예외 처리 코드 (try~catch~finally~…)
본격적으로 예외를 처리(handling) 하는 것에 대해 알아보겠습니다. 기본 형태는 아래와 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
try{
///예외가 발생할 가능성이 있는 코드
.... ......... ;
.............. .. ; << 1. 여기서 예외가 발생했을 때
.............. .... ; << 2. 예외 발생한 곳 아래는 실행하지 않고,
}
catch(예외클래스 e) {
예외 처리; << 3. catch문에서 예외처리를 한다.
}
finally {
///무슨 일이 있든 항상 실행
}
- 다중 catch문
1
2
3
4
5
6
7
8
9
.....
try{
// 예외 1 발생위치
// 예외 2 발생위치
}
catch(예외1을 잡는 곳){
}
catch(예외2를 잡는 곳){
}
발생하는 예외별로 다중 catch문을 구성할 수 있지만, catch 블록이 여러개여도 먼저 발생한 예외에 대하여 단 하나의 catch 블록만 실행됩니다. 그 이유는 위에서 예외 1이 발생했다면, 그 밑의 예외 2 발생 코드는 실행하지 않고 바로 예외 1을 catch하는 곳으로 이동하기 때문입니다.
그러므로 다중 catch 블록을 작성할 때에는 상위 예외 클래스가 하위 예외 클래스보다 밑에 위치하게 작성해야 합니다.
try에서 예외가 발생했을 때에는 catch블록이 작성된 순서대로 위에서 아래대로 차례로 검사하는데, 만일 상위 예외 클래스가 더 위에 있었다면 하위 예외 클래스를 실행하지 않게 됩니다. 즉, 하위 예외 클래스가 상위 예외 클래스를 상속했기 때문에, 상위 예외 클래스를 잡는 catch 블록이 실행되게 됩니다.
이 말이 무슨 의미인지, 예를 들어 FileNotFoundException
예외가 발생했다고 가정해 보겠습니다.
1
2
3
4
5
6
7
8
9
10
.....
try{
// FileNotFoundException 예외 발생
}
catch(Exception e){
// FileNotFoundException은 Exception의 자식 클래스이므로 이 부분의 catch가 실행됨
}
catch(FileNotFoundException e){
// 정작 FileNotFoundException을 처리하려는 이 catch 블록은 실행되지 않음
}
` FileNotFoundException의 상위 예외 클래스인
Exception을 처리하는 catch 블록이 더 위에 있기 때문에,
FileNotFoundException` 이 생겼을 때 처리하기 위한 두 번째 catch 블록은 실행되지 않습니다.
따라서 Exception
처럼 상위 예외 클래스를 처리하는 코드는 catch문 맨 마지막에 작성합니다.
- 멀티 catch
자바 7부터 하나의 catch 블록에서 여러 개의 예외를 처리할 수 있는 기능이 추가되었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
try{
// 예외 1 발생위치 (혹은 예외 3 발생위치)
// 예외 2 발생위치
}
catch( 예외 1 | 예외 3 e) // | 는 or 로 보면 된다. 예외 1 혹은 예외 3이 발생하면
해당한 catch 블록에서 처리하게 된다.
{
예외 처리
}
catch( 예외2 e)
{
예외 처리
}
예외 떠넘기기
메소드 내부에서 예외가 발생할 수 있는 코드를 작성할 때, try-catch 블록으로 예외 처리 하는 것이 기본이지만, 경우에 따라서는 메소드를 호출한 곳으로 예외 처리를 떠넘길 수도 있습니다. 이 때 사용하는 키워드가 throws
입니다.
throws 키워드는 메소드 선언부 맨 끝에 작성하며, 메소드에서 try-catch를 통해 처리하지 않은 예외를 호출한 곳으로 떠넘기는 역할을 합니다. throws 뒤에는 떠넘길 에외 클래스를 쉼표로 여러개 구분해서 나열할 수 있습니다. 발생할 수 있는 예외별로 throws 뒤에 나열하는 것이 보통이나, Exception 만 작성해서 모든 예외를 간단히 떠넘길 수도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
public void method1(){
try {
method2(); // throws가 붙은 method2는 반드시 이렇게 try문 안에서 호출되어야 함.
// method2가 떠넘긴 예외를 아래 catch문을 통해 처리해 주고 있다.
}
catch (ClassNotFoundException e1) {
System.out.println("클래스가 존재하지 않음.");
}
public void method2() throws ClassNotFoundException {
Class clazz = Class.forName("java.lang.String22");
}
만일 method1도 예외를 떠넘긴다면 어떻게 될까요? 만일 method1을 main 메소드에서 호출했다면, main 메소드에서 try-catch를 통해 예외 처리를 해 주어야 할 것입니다. 그런데 main() 메소드에서도 예외를 떠넘길 수 있습니다. 그러면 그 예외는 JVM이 최종적으로 처리를 하게 되지만, 이는 권장 사항은 아닙니다.
사용자 정의 예외와 예외 발생
사용자 정의 예외 클래스란 개발자가 직접 정의하여 만드는 예외를 말합니다. 일반 예외(Checked Exceptino)나 실행 예외(RuntimeException, UnChecked Exception) 중 하나로 만들 수 있습니다. 전자는 Exception을 상속하면 되고, 후자는 RuntimeException을 상속하면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class AnyCreateException extends Exception {
public AnyCreateException() {};
public AnyCreateException(String message) {
super(message);
// 사용자 정의 예외의 생성. 생성자는 보통 두 가지를 선언하는데
// 두 번째의 경우 에외 메시지를 전달하기 위해 String 타입의 매개 변수를 갖는 생성자이다.
}
}
.....
public class Account{
int balance = 100;
public void withdraw(int money) throws AnyCreateException {
if(balance < money){
throw new AnyCreateException("Excpetion 발생!");
// AnyCreateException 의 두 번째 생성자를 선택한 것이다.
// 모든 예외는 Exception이 가지고 있는 메소드들을 호출 할 수 있다.
// 그 중 getMessage 메소드가 이 예외 메시지를 리턴한다. 사용은 아래에 구현했다.
}
}
....
public static void main(String[] args){
Account account = new Account();
try{
account.wtihdraw(30000);
}
catch ( AnyCreateException e ) {
String message = e.getMessage(); // 메시지를 담은 생성자를 선택했을 때, 그 메시지를
// getMessage() 메소드가 리턴한다.
System.out.println(message);
e.printStackTrace(); // 예외가 어디에서 발생했는지 출력해준다.
}