[java] 예외처리(Exception Handling)

이것만은 알고 가 - 프로그램 오류

- 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우를 프로그램 에러 또는 오류라고 한다.

- 발생시점에 따라서

└ 컴파일 에러(compile-time error) : 컴파일 할 때 발생하는 에러

[소스코드(*.java)] -(컴파일:오타, 잘못된 구문, 자료형 체크등의 기본적인 검사 수행)-> [클래스파일(*.class)]

└ 런타임 에러(runtime error) : 프로그램 실행 도중에 발생하는 에러

실행도중에 발생할 수 있는 잠재적인 오류 혹은 비정상적인 종료 => 이를 방지하기 위한 모든 경우의 수를 고려하여 대비를 하는 것이 필요하다!

-종류

└ 에러(error) : 메모리 부족이나 스택오버플로우와 같이 일단 발생하면 복수할 수 없는 심각한 오류. 프로그램의 비정상적인 종료를 막을 수 없다.

└ 예외(exception) : 발생하더라도 수습될 수 있는 비교적 덜 심각한 것. 예외코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.

 

예외처리(Exception Handling)

- 정의 : 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것

- 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

 

예외처리구문(try - catch)

- 구조

try{

// 예외가 발생할 가능성이 있는 문장들을 넣는다.

} catch(Exception e){

// Exception이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.

}

- 하나의 try블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch블럭이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch블럭만 수행된다. 발생한 예외의 종류와 일치하는 catch블럭이 없으면 예외는 처리되지 않는다.

[참고] if문과 달리 try블럭이나 catch블럭 내에 포함된 문장이 하나라고 해서 괄호{}를 생략할 수는 없다.

- try블럭 또는 catch블럭에 또 다른 try-catch문이 포함될 수 있다.

- 자바에서는 정수값을 0으로 나누면, ArithmeticException(산술연산에 오류가 있을 때 발생하는 예외)이 발생하도록 되어 있다. 하지만, 실수값을 0으로 나눌 때는 ArithmeticException이 발생하지 않는다. 마찬가지로 정수값을 0.0(실수값)으로 나누는 것 역시 ArithmeticException이 발생하지 않는다.

 

try-catch문에서의 흐름

- try블럭 내에서 예외가 발생한 경우

1. 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.

2. 일치하는 catch블럭을 찾게 되면, 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면, 예외는 처리되지 못한다.

- try블럭 내에서 예외가 발생하지 않은 경우

1. catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.

 

예외 발생시키기

- 키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.

1. 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든다.

Exception e = new Exception();

2. 키워드 throw를 이용해서 예외를 발생시킨다.

throw  e;

3. 위 과정을 줄이면

throw new Exception();

[참고] Exception인스턴스를 생성할 때, 생성자에 String을 넣어 주면, 이 String이 Exception인스턴스에 메시지로 저장된다. 이 메시지는 getMessage()를 이용해서 얻을 수 있다.

 

예외 클래스의 계층구조

=====================================================

Object

└Throwable

Exception (모든 예외의 최고 조상)

IOException 

----------------------------------------

RuntimeException

...

Error

OutOfMemoryError

...

=====================================================

- RuntimeException클래스와 그 자손클래스들(RuntimeException 클래스들)과 Exception클래스와 그 자손클래스들(Exception클래스들)로 나눠질 수 있다.

+ RuntimeException클래스들

- 프로그래머의 실수에 의해서 발생될 수 있는 예외들로서 자바의 프로그래밍 요소들과 관계가 깊다.(ex) 배열의 범위를 벗어난다(IndexOutOfBoundsException), 값이 null인 참조변수의 멤버 호출(NullPointerException), 클래스간의 형변환을 잘못 수행(ClassCastException), 0으로 나누기(ArithmeticException) 등) 따라서 try-catch문을 사용하기 보다는 프로그래머들이 주의 깊게 작성하여 예외가 발생하지 않도록 해야 할 것이다. 

[참고] RuntimeException클래스들에 속하는 예외가 발생할 가능성이 있는 코드에도 예외처리를 해야 한다면, 모든 참조변수와 배열이 사용되는 곳에 예외처리를 해주어야 할 것이다.

+ Exception클래스들

- 외부의 영향으로 발생할 수 있는 것들로서, 프로그램의 사용자들의 동작에 의해서 발생하는 경우가 많다.(ex) 존재하지 않는 파일의 이름 입력(FileNotFoundException), 클래스명을 잘못 적은 경우(ClassNotFoundException), 입력한 데이터 형식이 잘못된 경우(DataFormatException) 등) 이런 종류의 예외들은 반드시 처리를 해주어야 한다.

- 두 클래스들간의 차이점은 컴파일시의 예외처리 체크여부이다. 따라서 Exception클래스들 그룹에 속하는 예외가 발생할 가능성이 있는 예외는 반드시 처리해 주어야 한다. RuntimeException클래스들 그룹에 속하는 예외들은 예외처리를 하지 않더라도 컴파일 된다는 점도 주의할 것!

 

예외의 발생과 catch블럭

catch(괄호) {블럭}

- 괄호에는 처리하고자 하는 예외와 같은 타입의 참조변수 하나를 선언해야 한다.

1. 예외가 발생하면 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다.

2. catch블럭의 괄호내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof연산자를 이용해서 검사한다.

3. 검사 결과가 true인 catch블럭을 찾게 되면 블럭에 있는 문장들을 모두 수행한 후에 try-catch문을 빠져나가고 예외가 처리된다. 그러나 적절한 catch블럭이 없다면 예외는 처리되지 않는다.

- 모든 예외 클래스는 Exception클래스의 자손이므로, catch블럭의 괄호에 Exception클래스 타입의 참조변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch블럭에 의해서 처리된다.

[참고] try-catch문의 마지막에 Exception클래스 타입의 참조변수를 선언한 catch블럭을 사용하면, 어떤 종류의 예외가 발생하더라도 처리되도록 할 수 있다.

- 예외가 발생했을 때 생성되는 예외클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며. getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.(catch블럭의 괄호에 선언된 참조변수를 통해 이 인스턴스에 접근할 수 있다.)

+ getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

+ printStackTrace() : 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.

더보기

- printStackTrace(PrintStream) 또는 printStackTrace(PrintWriter)를 사용하면 발생한 예외에 대한 정보를 파일에 저장할 수도 있다.

 

---------------------------------------------------------------

package test;

 

import java.io.PrintStream;

 

public class Test {

public static void main(String[] args) {

PrintStream ps = null;

 

try {

ps = new PrintStream("error.log");  // 경로 없이 파일의 이름만 지정하였으므로 현재 디렉토리에 생성된다.

 

System.out.println(1);

System.out.println(2);

System.out.println(0/0);

System.out.println(3);

} catch (Exception ae) {

ae.printStackTrace(ps);

ps.println("예외 메시지 : "+ae.getMessage());

}

System.out.println(4);

}

}

 

실행결과

console==================

1

2

4

========================

 

error.log file==========================================

java.lang.ArithmeticException: / by zero

at test.Test.main(Test.java:14)

예외 메시지 : / by zero

====================================================

---------------------------------------------------------------

- 위의 경우는 참조변수인 ps가 main메서드 내에 선언되어 있기 때문에 다른 메서드에서 발생한 예외에 대한 내용을 기록할 수 없다. 이를 해결하기 위해서는 'System.err'을 이용하면 된다.

 

---------------------------------------------------------------

package test;

 

import java.io.*;

import java.util.Date;

 

public class Test {

public static void main(String[] args) {

PrintStream ps = null;

FileOutputStream fos = null;

try {

fos = new FileOutputStream("error.log");

ps = new PrintStream(fos);

System.setErr(ps);

 

System.out.println(1);

System.out.println(2);

System.out.println(0/0);

System.out.println(3);

} catch (Exception ae) {

System.err.println("-------------------------------");

System.err.println("예외발생시간 : "+new Date());

ae.printStackTrace(System.err);

System.err.println("예외메시지 : "+ae.getMessage());

System.err.println("-------------------------------");

}

System.out.println(4);

}

}

 

실행결과

console==================

1

2

4

========================

 

error.log file==========================================

-------------------------------

예외발생시간 : Wed Sep 24 05:42:37 KST 2014

java.lang.ArithmeticException: / by zero

at test.Test.main(Test.java:17)

예외메시지 : / by zero

-------------------------------

====================================================

---------------------------------------------------------------

- 'System.out'이나 'System.err'은 System클래스의 static멤버로서 프로그램의 어디에서나 사용할 수 있다. 'System.err'은 'System.out'과 마찬가지로 기본적으로 출력방향이 화면으로 되어 있어서, setErr메서드를 이용해서 출력방향을 바꾸지 않는 한 err에 출력하는 내용은 모두 화면에 나타나게 된다.

- 위 예제에서는 setErr()를 이용해서 출력방향을 error.log라는 이름의 파일로 변경하였다. 출력방향이 변경되었기 때문에 'System.err'의 println메서드나 print메서드를 이용해서 출력하는 내용은 error.log파일에 저장된다.

 

 

finally블럭

- 문법

try{

// ...

} catch(){

// ...

} finally{

// 맨 마지막에 위치해야한다.

}

- try-catch문과 함께 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally의 순서로 구성된다.

- 예외가 발생한 경우에는 'try - catch - finally'의 순으로 실행되고, 예외가 발생하지 않은 경우에는'try - finally'의 순으로 실행된다.

-----------------------------------------------------------------

return 예제

public class Test {

static void method(){

try {

System.out.println("method start!");

return;

} catch (Exception e) {

e.printStackTrace();

} finally {

System.out.println("method() finally block");

}

System.out.println("try{} exit");

}

public static void main(String[] args) {

System.out.println("main start!");

Test.method();

System.out.println("back main!");

}

}

 

실행결과

main start!

method start!

method() finally block

back main!

-----------------------------------------------------------------

- try블럭에서 return문이 실행되는 경우에도 finally블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다. 이와 마찬가지로 catch블럭의 문장 수행 중에 return문을 만나도 finally블럭의 문장들은 수행된다.
 
메서드에 예외 선언하기
- 메서드의 선언부에 키워드 throws를 사용해서 메서드내에서 발생할 수 있는 예외를 적어주기만 하면 된다. 예외가 여러 개일 경우에는 쉼표로 구분한다.
[참고] 예외를 발생시키는 키워드 throw와 예외를 메서드에 선언할 때 쓰이는 throws 혼동하지 말 것.
- 메서드의 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 때, 이 메서드를 사용하기 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있다. 또한 선언부에 명시되어있기 때문에 사용하는 쪽에서 이에 대한 처리를 하도록 강요하기 때문에, 프로그래머들의 짐을 덜어 주는 것은 물론이고 보다 견고한 프로그램 코드를 작성할 수 있도록 해준다.
- 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다. 예외를 전달받은 메서드가 또 다시 자신을 호출한 메서드에게 전달할 수 있으며, 계속 호출스택에 있는 메서드를 따라 전달되다가 제일 마지막에 있는 main메서드에서도 예외가 처리되지 않으면, main메서드 마저 종료되어 프로그램이 전체가 종료된다.

-----------------------------------------------------------------

예외 처리 예제
public class Test {
static void method1() throws Exception{
method2();
}
static void method2() throws Exception{
throw new Exception();
}
public static void main(String[] args) throws Exception {
method1();
}
}
 
실행결과
Exception in thread "main" java.lang.Exception
at test.Test.method2(Test.java:8)
at test.Test.method1(Test.java:5)
at test.Test.main(Test.java:11)

-----------------------------------------------------------------

- 위의 결과로부터 다음과 같은 사실을 알 수 있다.
- 예외가 발생했을 때, 모두 3개의 메서드(main, method1, method2)가 호출스택에 있었다.
- 예외가 발생한 곳은 제일 윗줄에 있는 method2()이다.
- main메서드가 method1()을, method1()이 method2()를 호출했다는 것을 알 수 있다.
- main메서드에서 조차 예외처리를 해주지 않았으므로 main메서드가 종료되어 프로그램이 예외로 인해 비정상적으로 종료되는 것이다.
- 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 수는 있지만, 이것으로 예외가 처리된 것은 아니고 예외를 단순히 전달만 하는 것이다. 결국 어느 한 곳에서는 반드시 try-catch문으로 예외처리를 해주어야 한다.

- 예외가 발생한 메서드내에서 처리되어지면, 호출한 메서드에서는 예외가 발생했다는 사실조차 모르게 된다. 예외가 발생한 메서드에서 예외를 처리하지 않고 호출한 메서드로 넘겨주면, 호출한 메서드에서는 메서드를 호출한 라인에서 예외가 발생한 것으로 간주되어 이에 대한 처리를 하게 된다.

 

예외 되던지기(exception re-throwing)

- 한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 나눠서 처리되도록 할 수 있다. 또한 단 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드, 양 쪽에서 처리하도록 할 수 있다.

- 예외를 처리한 후에 인위적으로 다시 발생시키는 방법을 통해서 가능하다.

1. 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용해서 예외를 처리한다.

2. catch문에서 필요한 작업을 행한 후에 throw문을 사용해서 예외를 다시 발생시킨다.

3. 다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드가 처리한다.

 

사용자정의 예외 만들기

- 기존에 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception클래스로부터 상속받는 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 선택할 수 있다.

-----------------------------------------------------------------

사용자 정의 예외 클래스 예제

class MyException extends Exception{

private final int ERR_CODE;

 

public MyException(String msg, int errCode) {

super(msg); // 에러메시지 저장

ERR_CODE = errCode;

}

 

public MyException(String msg) {

this(msg,100);

}

 

public int getErrCode(){

return ERR_CODE;

}

}

-----------------------------------------------------------------