예외 (Exception)
예외는 컴파일 에러와는 다르다. 컴파일 에러는 문법 상의 오류이지만 예외는 문법 상 오류가 아닌 실행 중에 발생되는 오류 상황을 일컫는다.
일반적으로는 예외가 발생하는 경우에 예외 관련 메시지를 출력하고 프로그램을 종료한다.
예외 예시
ArithmeticException
0으로 나누었을 때 발생하는 예외이다.
ClassCastException
타입 변환(Casting)은 상위 클래스와 하위 클래스 간 또는 구현 클래스와 인터페이스 간에서 발생한다.
두 클래스가 위와 같은 관계가 아닌 상황에서 타입 변환을 시도하는 경우 발생하는 예외이다.
ArrayIndexOutBoundsException
배열 사용 시, 배열 사이즈를 벗어난 인덱스에 접근하는 경우 발생하는 예외이다.
NegativeArraySizeException
배열 사용 시, 음수 값 인덱스로 접근하는 경우에 발생한다.
NullPointerException
null인 값을 사용하려 할 때 발생하는 예외이다.
String str = null; 이와 같이 null로 초기화된 str 변수의 length를 구하려고 하거나, str.equals("abc"); 등의 null 값과 비교하려 할 때 발생한다.
InputMismatchException
Scanner로 받는 값의 자료형이 일치하지 않을 때 발생하는 예외이다.
예를 들어, Scanner.nextInt();를 사용했을 때, 입력받는 값이 문자인 경우 발생한다.
NumberFormatException
문자열을 숫자 데이터로 변환할 때, 문자열에 숫자로 변환할 수 없는 문자가 포함되어있는 경우 발생한다.
예외 클래스의 상속 관계
java.lang.Object
└ java.lang.Throwable
└ java.lang.Exception : 복구 가능
└ java.lang.RuntimeException
└ ArithmeticException, NullPointerException, ...
└ IOException, ParseException, ...
java.lang.Throwable
└ java.lang.Error : 복구 불가
OutOfMemoryError, StackOverFlowError, ...
getMessage(), printStackTrace() 등의 메서드는 Throwable 클래스에 정의되어 있다.
따라서 자식 클래스들은 Throwable에 정의된 메서드를 사용할 수 있다.
예외 처리
자바에서는 예외 처리를 하기 위해 try-catch문을 사용한다.
if문으로도 가능하지만 if 문은 예외 처리 이외의 용도로 사용되므로 프로그램 코드 상에서 if문 인지, 예외 처리 부분인지 구분하기가 쉽지 않다.
try {} 블록에서는 일반적인 흐름을 작성하고, catch {} 블록에서 예외 처리 부분을 작성함으로써 코드 분석 시 용이하게 할 수 있다.
multi-catch
Java 7 버전에서부터 한 개의 catch 문에서 여러 예외 처리를 할 수 있는 방법을 제공한다.
같은 상속 레벨의 exception들만 multi-catch 해야 한다.
finally
예외 발생 여부와 상관없이 항상 실행되는 코드가 작성되는 부분이다.
코드들은 finally 블록 안에 작성된다.
예외가 발생하지 않는 경우에는 try 블록 안의 코드들이 모두 실행된 후에 finally 블록이 실행되고, 예외가 발생한 경우에는 발생한 예외에 해당하는 catch 블록 안 코드들이 실행된 후 finally 블록이 실행된다.
보통 자원 반납 같은 것들을 할 때 finally를 사용한다.
try 블록이나 catch 블록 안에 return이 있어도 finally 블록 안 코드가 모두 실행된 이후에 return이 수행된다.
try 블록, catch 블록, finally 블록 등 여러 부분에서 사용되는 변수는 try 블록 전에 초기화하는 것이 좋다.
💻 예제 1
📝 소스 코드
public class Exception02Main {
public static void main(String[] args) {
int num1 = 100;
int num2 = 0;
int result = 0;
if (num2 != 0) {
result = num1 / num2;
} else {
System.out.println("0으로 나눌 수 없습니다. ");
}
System.out.println("result = " + result);
System.out.println();
try {
result = num1 / num2;
System.out.println("result = " + result);
} catch (ArithmeticException ex) {
System.out.println("0으로 나누면 발생하는 Exception");
System.out.println(ex.getMessage());
ex.printStackTrace();
}
}
}
처음에는 if문을 사용해 예외가 발생하지 않도록 하였다.
그러나 이 코드를 보면 이게 예외 처리 때문에 작성한 코드인지 정말 해당 조건이 필요해서 작성한 코드인지 헷갈릴 수 있다.
두 번째는 try-catch문을 사용했다.
try 문에서 일반적으로 실행할 코드를 작성하고, catch 문에서 명시한 예외가 발생하는 경우 수행할 코드를 작성한다.
📄 실행 결과
💻 예제 2
예제 1에서는 catch문에서 ArithmeticException 예외만을 처리했다.
catch문을 여러 개 사용하여 여러 개의 예외 처리를 할 수 있다.
단, 상위 예외 클래스를 하위 예외 클래스보다 먼저 사용하는 경우 컴파일 에러가 발생한다.
📝 소스 코드
public class Main {
public static void main(String[] args) {
int num1 = 100, num2 = 2, result = 0;
String str = "Java";
int[] numbers = new int[10];
try {
result = num1 / num2;
System.out.println("result = " + result);
int length = str.length();
System.out.println("length = " + length);
numbers[10] = 11111;
System.out.println("numbers = " + numbers[10]);
} catch (ArithmeticException ex) {
System.out.println("산술 연산 예외 : " + ex.getMessage());
} catch (NullPointerException ex) {
System.out.println("NPE : " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Exception : " + ex.getMessage());
}
}
}
세 개의 catch 문을 사용하였다.
이때 Exception은 다른 예외(ArithmeticException, NullPointerException) 보다 상위 클래스에 속해있으므로 가장 마지막에 작성해야 한다.
📄 실행 결과
num2를 0으로 하면 산술 연산 예외가 먼저 뜰 것이다.
String str에 저장된 값을 "Java"가 아닌 null로 한다면 NPE가 먼저 뜰 것이다.
💻 예제 3
📝 소스 코드
public class Main {
public static void main(String[] args) {
try {
String str = null;
str.length();
int n = 100 / 0;
} catch (ArithmeticException | NullPointerException | ArrayIndexOutOfBoundsException ex) {
System.out.println(ex.getClass());
System.out.println(ex.getMessage());
}
}
}
| 연산자를 이용해 multi-catch를 사용할 수 있다.
📄 실행 결과
💻 예제 4
📝 소스 코드
import java.util.InputMismatchException;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
System.out.println("# 1 try {} 직전 ");
try {
System.out.println("# 2 try {} 시작");
int[] numbers = new int[10];
numbers[10] = 100;
System.out.println("# 3 try {} 종료");
} catch (ArrayIndexOutOfBoundsException ex) {
System.out.println("# 4 catch {}");
System.out.println("예외 메시지 : " + ex.getMessage());
} finally {
System.out.println("# 5 finally {}");
}
System.out.println("# 6 try 종료 후");
System.out.println();
Scanner sc = new Scanner(System.in);
try {
System.out.println("정수를 입력하세요.");
sc.nextInt();
System.out.println("try 블록 종료");
} catch (InputMismatchException ex) {
System.out.println("예외 메시지 : " + ex.getMessage());
return;
} finally {
System.out.println("finally 수행");
sc.close();
}
}
}
numbers[10]에 접근했을 때 예외가 발생하므로 # 3은 실행되지 않고 catch 문으로 넘어가게 된다.
catch 문을 실행한 뒤 finally 문을 실행하고 끝난다.
이후 다시 try-catch-finally 문을 작성해본다.
정수를 입력받기 위해 nextInt()를 사용했으나 정수가 아닌 값을 입력하여 예외가 발생하게 한다.
catch 문을 실행하게 되는데 이때 catch문 내에서 return; 하였어도 finally문은 실행된다.
Scanner 같은 경우는 자원의 하나이기 때문에 finally 문에서 close()를 통해 반납해준다.
📄 실행 결과