====== 예외처리 (Exception Handling) ======
===== 프로그램 오류 =====
* 발생시점에 따른 구분
- 컴파일 에러 (compile-time error)
- 런타임 에러 (runtime error)
* Java에서는 실행시 발생할 수 있는 프로그램 오류를 __**에러(Error)**__와 __**예외(Exception)**__, 두가지로 구분하였다.
- 에러(Error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
- 예외(Exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
===== 예외처리의 정의와 목적 =====
* 정의 : 프로그램 실행시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
* 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
===== 예외처리구문 : try-catch =====
* 예외를 처리하기 위해서는 try-catch 문을 사용하며, 그 구조는 다음과 같다.
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
// Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception2 e2) {
// Exception2이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
...
} catch (ExceptionN eN) {
// ExceptionN이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}
☛ if 문과 달리 try블럭이나 catch블럭 내에 포함된 문장이 하나라고 해서 괄호{}를 생략할 수는 없다.
* [예제 8-1] /ch8/ExceptionEx1.java
class ExceptionEx1 {
public static void main(String args) {
try {
try { } catch (Exception e) { }
} catch (Exception e) {
try { } catch (Exception e) { } // 컴파일 에러 발생 !!!
} // try-catch 의 끝
try {
} catch (Exception e) {
} // try-catch 의 끝
// main메서드의 끝
}
}
catch블럭 내에 또 하나의 try-catch문이 포함된 경우, 같은 이름의 참조변수를 사용해서는 안된다. 각 catch블럭에 선언된 두 참조변수의 영역이 서로 겹치기 때문에 다른 이름을 사용해서 구별해야하기 때문이다.
* [예제 8-2] /ch8/ExceptionEx2.java
class ExceptionEx2 {
public static void main(String[] args) {
int number = 100;
int result = 0;
for( int i = 0 ; i < 10 ; ++i ) {
result = number / (int)(Math.random() * 10); // 7번째 라인, 예외 발생
System.out.println(result);
}
}
}
위 프로그램을 실행하면, 7번째 라인에서 예외가 발생하며 비정상적으로 멈출것이다. 또한, 어떤 예외가 발생했는지도 화면에 나타난다.
* 그러면, ArithmeticException을 처리하는 예제를 살펴보자. \\ [예제 8-3] /ch8/ExceptionEx3.java
class ExceptionEx3 {
public static void main(String[] args) {
int number = 100;
int result = 0;
for( int i = 0 ; i < 10 ; ++i ) {
try {
result = number / (int)(Math.random() * 10); // 7번째 라인, 예외 발생
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("0"); // ArithmeticException이 발생하면 실행되는 코드
} // try-catch 의 끝
} // for 의 끝
}
}
===== try-catch문에서의 흐름 =====
* try블럭 내에서 예외가 발생한 경우,
- 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.
- 일치하는 catch블럭을 찾게 되면, 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면, 예외는 처리되지 못한다.
* try블럭 내에서 예외가 발생하지 않은 경우,
- catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.
* [예제 8-4] /ch8/ExceptionEx4.java
class ExceptionEx4 {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(4);
} catch (Exception e) {
System.out.println(5);
} // try-catch의 끝
System.out.println(6);
// main메서드의 끝
}
}
위의 예제에서는 예외가 발생하지 않았으므로, catch블럭 문장이 실행되지 않는다.
* [예제 8-4] /ch8/ExceptionEx5.java
class ExceptionEx5 {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0/0); // 0으로 나누어서 고의로 ArithmeticException을 발생시킨다.
System.out.println(4); // 실행되지 않는다.
} catch (ArithmeticException ae) {
System.out.println(5);
} // try-catch의 끝
System.out.println(6);
// main메서드의 끝
}
}
===== 예외 클래스의 계층구조 =====
* 예외클래스 계층도
Object --- Throwable -+- Error -+- OutOfMemoryError
| |
| +- ...
|
+- Exception -+- IOException
|
+- ...
|
+- RuntimeException
* 예외클래스는 다음과 같이 두 개의 그룹으로 나눠질 수 있다.
- RuntimeException클래스와 그 자손클래스들
- Exception클래스와 그 자손클래스들
* Exception클래스와 RuntimeException클래스 중심의 상속게층도
Exception -+- IOException
|
+- ClassNotFoundException
|
+- ...
|
+- RuntimeException -+- ArithmeticException
|
+- ClassCastException
|
+- NullPointException
|
+- ...
|
+- IndexOutBoundsException
- RuntimeException클래스들 : 프로그래머의 실수로 발생하는 예외
- Exception클래스들 : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
* RuntimeException클래스들 그룹에 속하는 예외가 발생할 가능성이 있는 코드에는 예외처리를 해주지 않아도 컴파일 시에 문제가 되지 않지만, Exception클래스들 그룹에 속하는 예외가 발생할 가능성이 있는 예외는 반드시 처리를 해주어야 하며, 그렇지 않으면 컴파일 시에 에러가 발생한다.
* [예제 8-6] /ch8/ExceptionEx6.java
class ExceptionEx6 {
public static void main(String[] args) {
throw new Exception(); // Exception을 강제로 발생시킨다.
}
}
위 예제를 컴파일하면 에러가 발생한다.
* 위 예제에서 예외를 처리하여 정상 동작하도록 해보자. \\ [예제 8-7] /ch8/ExceptionEx7.java
class ExceptionEx7 {
public static void main(String[] args) {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("Exception이 발생하였습니다.");
}
}
}
* 아래 예제에서는 컴파일은 무사히 넘어가지만, 실행하면 RuntimeException이 발생하여 비정상 종료되는 것을 확인할 수 있다. \\ [예제 8-8] /ch8/ExceptionEx8.java
class ExceptionEx8 {
public static void main(String[] args) {
throw new RuntimeException(); // RuntimeException을 강제로 발생시킨다.
}
}
===== 예외 발생시키기 =====
* 키워드 throw 를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있으며, 방법은 아래의 순서를 따르면 된다.
- 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든다음
Exception e = new Exception("고의로 발생시켰음");
- 키워드 throw를 이용해서 예외를 발생시킨다.
throw e;
* [예제 8-9] /ch8/ExceptionEx9.java
class ExceptionEx9 {
public static void main(String[] args) {
try {
Exception e = new Exception("고의로 발생시켰음.");
throw e; // 예외를 발생시킴.
// throw new Exception("고의로 발생시켰음.");
} catch (Exception e) {
System.out.println("에러 메시지 : " + e.getMessage());
e.printStackTrace();
}
System.out.println("프로그램이 정상 종료되었음.");
}
}
===== 예외의 발생과 catch블럭 =====
* 첫 번째 catch블럭부터 차례로 내려가면서 catch블럭의 괄호()내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 **''instanceof''** 연산자를 이용해서 검사하게 되는데, 검사결과가 true인 catch블럭을 만날 때까지 검사는 계속된다.
* 검사결과가 true인 catch블럭을 찾게 되면 블럭에 있는 문장들을 모두 수행한 후에 try-catch문을 빠져나가고 예외는 처리되지만, 검사결과가 true인 catch블럭이 하나도 없으면 예외는 처리되지 않는다.
* 모든 예외 클래스는 Exception클래스의 자손이므로, catch블럭의 괄호()에 Exception클래스 타입의 참조변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch블럭에 의해서 처리된다.
* [예제 8-10] /ch8/ExceptionEx10.java
class ExceptionEx10 {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0/0); // 0으로 나누어서 ArithmeticException을 발생시킨다.
System.out.println(4); // 실행되지 않는다.
} catch (Exception e) { // ArithmeticException대신 Exception을 사용.
System.out.println(5);
}
System.out.println(6);
}
}
* [예제 8-11] /ch8/ExceptionEx11.java
class ExceptionEx11 {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0/0); // 0으로 나누어서 ArithmeticException을 발생시킨다.
System.out.println(4); // 실행되지 않는다.
} catch (ArithmeticException ae) {
if(ae instanceof ArithmeticException)
System.out.println("true");
System.out.println("ArithmeticException");
} catch (Exception e) { // ArithmeticException을 제외한 모든 예외가 처리된다.
System.out.println(5);
}
System.out.println(6);
}
}
* 예외가 발생했을 때 생성되는 예외클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.
- printStackTrace() : 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
- getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
* [예제 8-12] /ch8/ExceptionEx12.java
class ExceptionEx12 {
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(); // 참조변수 ae를 통해, 생성된 ArithmeticException인스턴스에 접근할 수 있다.
System.out.println("예외메시지 : " + ae.getMessage());
}
System.out.println(6);
}
}
위에 언급된 메서드들을 통해서 예외의 발생원인을 알 수 있다.
* 그리고 예외 정보를 파일로 저장할 수도 있다. \\ [예제 8-13] /ch8/ExceptionEx13.java
import java.io.*;
class ExceptionEx13 {
public static void main(String[] args) {
PrintStream ps = null;
FileOutputStream fos = null;
try {
fos = new FileOutputStream("error.log");
ps = new PrintStream(fos);
System.out.println(1);
System.out.println(2);
System.out.println(3);
System.out.println(0/0); // 예외발생!!!
System.out.println(4); // 실행되지 않는다.
} catch (ArithmeticException ae) {
ae.printStackTrace(ps); // 인자로 ae를 넘겨주었다.
ps.println("예외메시지 : " + ae.getMessage()); // 화면대신 error.log파일에 출력한다.
System.out.println("예외가 발생하였으니 error.log 파일을 확인하시오.");
}
System.out.println(6);
}
}
===== finally블럭 =====
* finally블럭은 try-catch문과 함께 예외의 발생여부와 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.
* try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally의 순으로 구성된다.
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
// 예외처리를 위한 문장을 적는다.
} finally {
// 예외의 발생여부에 관계없이 항상 수행되어야 하는 문장들을 넣는다.
// finally블럭은 try-catch문의 맨 마지막에 위치해야한다.
}
* [예제 8-15] /ch8/FinallyTest.java
class FinallyTest {
public static void main(String[] args) {
try {
startInstall();
copyFiles();
deleteTempFiles(); // 프로그램 설치에 사용된 임시파일을 삭제한다.
} catch (Exception e) {
e.printStackTrace();
deleteTempFiles(); // 프로그램 설치에 사용된 임시파일을 삭제한다.
}
}
static void startInstall() {
// 프로그램 설치에 필요한 준비를 하는 코드를 적는다.
}
static void copyFiles() {
// 파일들을 복사하는 코드를 적는다.
}
static void deleteTempFiles() {
// 임시파일들을 삭제하는 코드를 적는다.
}
}
위 프로그램에서 deleteTempFiles()의 경우 예외가 발생하던 안하던 실행이 되도록 하고 있다. 이렇게 중복되는 코드를 finally 를 이용하여 단순화 할 수 있다.
* [예제 8-16] /ch8/FinallyTest2.java
class FinallyTest2 {
public static void main(String[] args) {
try {
startInstall();
copyFiles();
} catch (Exception e) {
e.printStackTrace();
} finally {
deleteTempFiles(); // 프로그램 설치에 사용된 임시파일을 삭제한다.
}
}
static void startInstall() {
// 프로그램 설치에 필요한 준비를 하는 코드를 적는다.
}
static void copyFiles() {
// 파일들을 복사하는 코드를 적는다.
}
static void deleteTempFiles() {
// 임시파일들을 삭제하는 코드를 적는다.
}
}
* 특히, try나 catch 블럭의 문장 수행중에 return을 만나도 finally블럭의 문장들은 수행된다. \\ [예제 8-17] /ch8/FinallyTest3.java
class FinallyTest3 {
public static void main(String[] args) {
// method1()은 static메서드이므로 인스턴스 생성없이 직접 호출이 가능하다.
FinallyTest3.method1();
System.out.println("method1()의 수행을 마치고 main메서드로 돌아왔습니다.");
}
static void method1() {
try {
System.out.println("method1()이 호출되었습니다.");
return; // 현재 실행중인 메서드를 종료한다.
} catch (Exception e) {
e.printStatckTrace();
} finally {
System.out.println("method1()의 finally블럭이 실행되었습니다.");
}
}
}
===== 메서드에 예외 선언하기 =====
* 예외를 처리하는 방법에는 지금까지 배워 온 try-catch문을 사용하는 것 이외에, 예외를 메서드에 선언하는 방법이 있다.
* 메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다. 그리고, 예외가 여러 개일 경우에는 쉼표(,)로 구분한다.
void method() throws Exception1, Exception2, ... ExceptionN {
// 메서드의 내용
}
이렇게 메서드의 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 때, 이 메서드를 사용하기 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있다.
* 사실, 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.
* [예제 8-18] /ch8/Exception18.java
class Exception18 {
public static void main(String[] args) throws Exception {
method1(); // 같은 클래스내의 static 멤버이므로 객체생성없이 직접 호출가능.
}
static void method1() throws Exception {
method2();
}
static void method2() throws Exception {
throw new Exception();
}
}
위의 예제를 실행하면 비정상 종료되는 것을 확인할 수 있다.
- 예외가 발생했을 때, 모두 3개의 메서드(main, method1, method2)가 호출스택에 있었으며,
- 예외가 발생한 곳은 제일 위줄에 있는 method2()라는 것과
- main메서드가 method1()를, 그리고 method2()은 method2()를 호출했다는 것을 알 수 있다.
* 결국, 어디서 든 간에 반드시 try-catch문을 사용해서 예외처리를 해주어야 정상적인 종료를 할 수 있을 것이다.
===== 예외 되던지기 =====
* 하나의 예외에 대해서 예외가 발생한 메서드와 호출한 메서드, 양 쪽에서 처리하도록 할 수 있다.
* 이것은 예외를 처리한 수에 인위적으로 다시 발생시키는 방법을 통해서 가능한데, 이것을 **예외 되던지기** (exception re-throwing)이라고 한다.
* [예제 8-23] /ch8/ExceptionEx23.java
class ExceptionEx23 {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("main메서드에서 예외가 처리되었습니다.");
}
}
static void method1() throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("method1메서드에서 예외가 처리되었습니다.");
throw e; // 다시 예외를 발생시킨다.
}
}
}
===== 사용자정의 예외 만들기 =====
* 기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception클래스로부터 상속받는 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 선택할 수 있다.
class MyException extends Exception {
MyException(String msg) { // 문자열을 매개변수로 받는 생성자
super(msg); // 조상인 Exception클래스의 생성자를 호출한다.
}
}
* 필요하다면 멤버번수를 추가할 수 있다.
class MyException extends Exception {
// 에러 코드 값을 저장하기 위한 필드를 추가했다.
private final int errorCode = 100;
MyException(String msg) { // 생성자
super(msg);
}
public int getCode() { // 에러 코드를 얻을 수 있는 메서드도 추가했다.
return errorCode; // 이 메서드는 주로 getMessage()와 함께 사용될 것이다.
}
}