하드웨어 고장이나 JVM의 자원 부족 같은 심각한 문제는 프로그램으로 해결할 수 없다. 이처럼 개발자가 해결할 수 없는 치명적인 상황을 자바는 오류(error)라고 한다. 하지만 가볍거나 예상할 수 있는 문제라면 프로그램을 종료하지 않고 해결하는 것이 좋다. 개발자가 해결할 수 있는 이런 문제를 자바는 예외(exception)라고 하며, 처리할 수 있는 수단을 제공한다.
이를 처리해 비정상적인 종료를 막고, 프로그램을 계속 진행할 수 있도록 우회 경로를 제공하면 좋다.
예외는 다음과 같이 java.lang 패키지에 있는 Throwable 클래스의 자식 객체로, 프로그램 실행 중에 발생하는 일종의 이벤트 이다. Throwable 클래스에는 다양한 자식 클래스가 있다.
예외는 다음과 같이 실행 예외(runtime exception)와 일반 예외로 구분한다.
런타임 예외 : 개발자의 실수로 발생할 수 있으며, 예외 처리를 하지 않아도 컴파일할 수 있는 비검사형(unchecked exception)이다.
컴파일 예외 : 예외 처리를 하지 않으면 컴파일 오류가 발생하므로 꼭 처리해야하는 검사형 예외(checked exception)이다.
논리적 예외 : 실행은 되지만, 의도와 다르게 동작하는 것.
에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
try ~ catch 문 예제
public class TryCathch2Demo {
public static void main(String[] args) {
int dividend = 10;
try {
int divisor = Integer.parseInt(args[0]);
System.out.println(dividend / divisor);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("원소가 존재하지 않습니다.");
} catch (NumberFormatException e) {
System.out.println("숫자가 아닙니다.");
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없습니다.");
} finally {
System.out.println("항상 실햅됩니다.");
}
System.out.println("종료.");
}
}
자바에서는 실행 시 발생할 수 있는 오류(Exception과 Error)를 클래스로 정의하였다. 모든 클래스의 조상은 Object클래스이므로 Exception과 Error클래스 역시 Object클래스의 자손들이다.
모든 예외의 최고 조상은 Exception클래스이다.
예외 클래스들은 다음과 같이 두 그룹으로 나눠질 수 있다.
1. Exception클래스와 그 자손들(RuntimeException과 자손들 제외)
2. RuntimeException클래스와 그 자손들
RuntimeException 클래스의 녀석들은 개발자가 Check를 해줘야 함.
앞으로 RuntimeException클래스와 그 자손 클래스들을 'RuntimeException클래스들'이라 하고, RuntimeException클래스들을 제외한 나머지 클래스들을 'Exception클래스들'이라고 한다.
왠만하면 외워보자.
메인 메서드까지 예외를 떠넘겼을때 메인에서도 처리가 안된다면 프로그램이 비정상적으로 꺼졌다는 호출스택의 내용과 함께 프로그램이 종료 된다.
이처럼 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 수는 있지만, 이것으로 예외가 처리된 것은 아니고, 예외를 단순히 전달만 하는 것이다.
결국 어디서 든 간에 반드시 try-catch문을 사용해서 예외처리를 해주어야 한다.
예외 떠넘기기 예제
import java.util.Scanner;
public class ThrowsDemo {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
try {
square(in.nextLine());
} catch (NumberFormatException e) {
System.out.println("정수가 아닙니다.");
}
}
// 예외를 호출한 곳으로 떠넘기기.
private static void square(String s) throws NumberFormatException {
int n = Integer.parseInt(s);
System.out.println(n * n);
}
}
----------------------------------------------------------------------------
// 예외 떠넘기기를 하지않고 직접 예외 처리하는 예제
import java.util.Scanner;
public class ThrowsDemo {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
square(in.nextLine());
}
private static void square(String s) {
int n = 0;
try { // 예외가 일어날만한 곳에서 직접 예외 처리
n = Integer.parseInt(s);
System.out.println(n * n);
} catch (NumberFormatException e) {
System.out.println("정수가 아닙니다.");
}
}
}
예외처리(exception handling)의
정의 - 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
try블럭 내에서 예외가 발생한 경우,
1. 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.
2. 일치하는 catch블럭을 찾게 되면, 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면, 예외는 처리되지 못한다.
try블럭 내에서 예외가 발생하지 않은 경우,
1. catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.
try-catch 의 예제
public class Java_bible {
public static void main(String[] args) {
try {
try { } catch (Exception e) { }
} catch (Exception e) {
// try { } catch(Exception e) { } // 에러, 변수 e가 중복 선언되었다.
} // try-catch의 끝
try {
} catch (Exception e) {
}
}
}
catch블럭은 괄화()와 블럭{} 두 부분으로 나눠져 있는데, 괄호()내에는 처리하고자 하는 예외와 같은 타입의 참조변수 하나를 선언해야한다.
예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어 진다.
아래의 코드에서는 ArithmeticException이 발생했으므로 ArithmeticException인스턴스가 생성된다. 예외가 발생한 문장이 try블럭에 포함되어 있다면, 이 예외를 처리할 수 있는 catch블럭이 있는지 찾게 된다.
첫 번째 catch블럭부터 차례로 내려가면서 catch블럭의 괄호()내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof연산자를 이용해서 검사하게 되는데, 검사결과가 true인 catch블럭을 만날 때까지 검사는 계속된다.
검사결과가 true인 catch블럭을 찾게 되면 블럭에 있는 문장들을 모두 수행한 후에 try-catch문을 빠져나가고 예외는 처리되지만, 검사결과가 true인 catch블럭이 하나도 없으면 예외는 처리되지 않는다.
만약 ArithmeticException외의 다른 Exception이 뜬다면 가장 조상 클래스인 Exception이 처리 된다.
public class Java_bible {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0 / 0);
System.out.println(4);
} catch (ArithmeticException ae) {
if (ae instanceof ArithmeticException) {
System.out.println("true");
}
System.out.println("ArithmeticException");
} catch (Exception e) {
System.out.println("Exception");
}
System.out.println(6);
}
}
예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.
catch블럭의 괄호()에 선언된 참조변수를 통해 이 인스턴스에 접근할 수 있다. 이 참조변수는 선언된 catch블럭 내에서만 사용 가능하다
printStackTrace() : 예외 발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와
예외메시지를 화면에 출력한다.
getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
public class Java_bible {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0 / 0); // 예외 발생!
System.out.println(4); // 실행되지 않는다.
} catch (ArithmeticException ae) {
ae.printStackTrace();
System.out.println("예외메시지 : " + ae.getMessage());
} // try-catch의 끝
System.out.println(6);
} // main의 끝
}
메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다. 그리고 예외가 여러 개일 경우에는 쉼표(,)로 구분한다.
void method() throws Exception1, Exception2 ... ExceptionN {
// 메서드 내용
}
예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.
예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있으며, 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main메서드 에서도 예외가 처리되지 않으면, main메서드마저 종료되어 프로그램 전체가 종료된다.
이처럼 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호푸한 메서드에게 예외를 넘겨줄 수는 있지만, 이것으로 예외가 처리된 것은 아니고 예외를 단순히 전달만 하는 것이다. 결국 어느 한 곳에서는 반드시 try-catch문으로 예외처리를 해주어야 한다.
finally 블럭은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용 된다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally의 순서로 구성된다.
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception e) {
// 예외처리를 위한 문장을 적는다.
} finally {
// 예외의 발생여부에 관계없이 항상 수행되어야 하는 문장들을 넣는다.
// finally블럭은 try-catch문의 맨 마지막에 위치해야 한다.
}
예외가 발생한 경우애는 'try-catch-finally'의 순으로 실행되고, 예외가 발생하지 않은 경우에는 'try-finally' 순으로 실행된다.
public static void main(String[] args) {
// method1()은 static메서드이므로 인스턴스 생성없이 직접 호출이 가능하다.
method1();
System.out.println("method1()의 수행을 마치고 main메서드로 돌아왔습니다.");
} // main의 끝
static void method1() {
try {
System.out.println("method1()이 호출되었습니다.");
return; // 현재 실행중인 메서드를 종료한다.
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("method1()의 finally블력이 실행되었습니다.");
}
}
위의 결과에서 알 수 있듯이, try블럭에서 return문이 실행되는 경우에도 finally블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다.
이와 마찬가지로 catch블럭의 문장 수행 중에 return문을 만나도 finally블럭의 문장들은 수행된다.