8. 예외처리 (Exception Handling)

1.1 프로그램 오류

프로그램 실행 중 어떤 원인에 의해 오작동하거나 비정상적으로 종료되는 경우가 있는데 이런 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다. 발생시점에 따라 다음과 같이 두종류로 구분한다.

  • 컴파일 에러 (compile error) : 컴파일 시에 발생하는 에러
  • 런타임 에러 (runtime error) : 실행 시에 발생하는 에러
  • 논리적 에러 (logical error) : 실행은 되지만 의도와 다르게 동작하는 것

자바에서는 실행 시에(run time) 발생하는 프로그램 오류를 다음과 같이 구분한다

  • 에러(error): i.e OutOfMemoryError, StackOverflowError)와 같이 수습될 수 없는 심각한 오류
  • 예외(exception): 코드에 의해 수습될 수 있는 다소 미약한 오류

1.2 예외 클래스의 계층구조

                 Object
                    |
                Throwable
                  /  \
      Exception          Error
        / \               /  \
IOException RuntimeEx..  ..  OutOfMemoryError

자바에서는 실행할 수 있는 오류를 각각 Exception, Error 클래스로 정의하였고 Object 클래스의 자손들(children)이다.

또 모든 예외의 최고 조상은 Exception class이고 다음과 같이 표현할 수 있다.

Exception
    |
    |--- IOException
    |--- ClassNotFoundException
    |--- ...
    |--- RuntimeException
            |--- ArithmeticException
            |--- ClassCastException
            |--- NullPointerException
            |--- ...
            |--- IndexOutOfBoundsException

여기서 RuntimeException과 그 자손클래스들은 RuntimeException 클래스들이라하고 나머지들은 Exception 클래스라고 한다.

  • RuntimeException classes: 프로그래밍 요소들과 관계가 깊다. 0으로 나누려고 하는 경우에 발생하는 ArithmeticException, 값이 null인 참조변수의 멤버를 호출하려다가 생기는 NullPointerException 등등..
  • Exception classes: 외부적인 영향으로 발생할 수 있는 예외. 존재하지 않는 파일이름을 입력했거나(FileNotFoundException), 클래스의 이름을 잘못 적었거나 (ClassNotFoundException), 데이터 형식이 잘못된 경우 (DataFormatException)

1.3 예외처리하기 - try-catch문

예외처리 (Exception Handling)란

 정의 - 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
 목적 - 프로그램의 비정상 종료를 막고 정상적인 실행상태를 유지하는 것

특징

  • if else와 달리 try-catch는 블럭내에 포함된 문장이 하나여도 {} 를 생략할 수 없다.
  • try 블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 여러개의 catch블럭을 사용할 수 있다.
  • catch 내에 또 다른 try-catch 블럭을 사용할 수 있는데 이때는 다음과 같이 변수 e를 중복해서 사용할 수 없다.
    class ExceptionEx1{
     public static void main(String args[]) {
         try {
             try{ } catch(Exception e){ }
         }catch (Exception e){
             try{ } catch(Exception e){ }  // --> error 'e' 가 중복 선언
         }
     }
    }
    

예제 1

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); // line 7
            System.out.println(result);
        }
    }
}

결과 1

20
100
java.lang.ArithmeticException: /by zero
        at ExceptionEx2.main(ExceptionEx2.java: 7)

예제 2 예외처리

class ExceptionEx2 {
    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); // line 7
                System.out.println(result);
            }catch (ArithmeticException e){
                System.out.println("0")
            }
        }
    }
}

결과 2

16
20
11
0  <-- ArithmeticException이 발생했지만 프로그램은 종료하지 않고 대신 '0'을 출력
33
100
..
..

1.4 try-catch문에서의 흐름

try-catch문에서 예외가 발생한 경우와 발생하지 않았을 때의 흐름을 다음과 같이 정리 할 수 있다.

  • try 블럭 내에서 예외가 발생한 경우
    1. 발생한 예외와 일치하는 catch 블락이 있는지 확인한다.
    2. 일치하는 catch 블럭을 찾게 되면 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다 만일 일치하는 catch블럭을 찾지 못하면 예외는 처리되지 못한다.
  • try 블럭내에서 예외가 발생하지 않은 경우
    1. catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.

1.5 예외의 발생과 catch블럭

  1. catch블럭은 괄호()와 블럭 {} 두 부분으로 나눠져 있는데 괄호 ()내에는 처리하고자 하는 예외와 같은 타입의 참조변수 하나를 선언해야 한다.
  2. 예외가 발생하면 해당 클래스의 인스턴스가 만들어진다.
  3. 예외가 발생한 문장이 try-catch문의 try블럭에 있다면, 이 예외를 처리할 수 있는 catch블럭을 찾게된다.
  4. 첫번째 catch 블럭부터 차례로 내려가면서 catch블럭의 괄호()내에 선언된 참조변수의 종류와 생성된 예외클래스의 instance에 instanceof를 히용해 검사하고 결과가 true이면 예외처리한다. 모든 예외 클래스는 Exception클래스의 자손이므로 Exception클래스 타입의 참조변수를 선언해 놓으면 어떤 종류의 예외가 발생해도 이 catch블럭에 의해 처리된다.

예제 1

 class ExceptionEx6 {
     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 (Exception e){
             System.out.println(5);
         }
         System.out.println(6);
     }
 }

실행결과

1
2
3
5
6

예제 2

 class ExceptionEx7 {
     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);
     }
 }

실행결과

1
2
3
true
ArithmeticException
6

예외가 발생했을 때 생성되는 예외클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며 다음 method들을 통해 정보를 얻을 수 있다.

  • printStackTrace(): 예외발생 당시의 호출스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력
  • getMessage(): 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

예제 3

 class ExceptionEx8 {
     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());
         }
         System.out.println(6);
     }
 }

결과

1
2
3
java.lang.arithmeticException: /by zero
    at ExceptionEx8.main ( ExceptionEx8.java: 7)
예외메세지 : /by zero
6

멀티 catch블럭 : JDK 1.7부터 여러 catch블럭을 | 기호를 이훃애서 하나의 catch블럭으로 합칠 수 있게 되었으며 이를 멀티 catch블럭dlfk gksek.

try {
    ...
}catch (ExceptionA e){
    e.printStackTrace();
}catch (ExceptionB e2){
    e2.printStackTrace();
}
// from JDK 1.7
try {
    ...
}catch (ExceptionA | ExceptionB e){
    e.printStackTrace();
}

멀티 블럭 사용할 때에 | 로 연결된 예외 클래스가 조상과 자손의 관계이면 에러가 생기므로 (불필요한 코드를 제거하라는 의미에서 발생) 조상클래스만으로 사용.

try{
...
} catch (ParentException | ChildException e){  //--> error
}
try{
..
} catch (ParentException e){
}

1.6 예외 발생시키기

키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있으며, 방법은 아래와 같다.

  1. 먼저 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만들고
  2. 키워드 throw를 이용해서 예외 발생

    Exception e = new Exception("고의로 발생시킴");
    throw e;
    

    예제

    class ExceptionEx9 {
      public static void main(String args[]) {
          try {
              Exception e = new Exception("고의로 발생시킴");
              throw e; // 예외를 발생시킴
              // throw new Exception("고의로 발생시킴"); --> 위에 두줄을 한줄로 표현
    
          }catch (Exception e){
              System.out.println("예외메세지 : " + ae.getMessage());
              e.printStackTrace();
          }
          System.out.println("프로그램이 정상 종료되었음." );
      }
    }
    

실행결과

예외메세지 : 고의로 발생시킴
java.lang.Exception: 고의로 발생시킴
    at ExceptionEx9.main (ExceptionEx9.java:4)
프로그램이 정상 종료되었음
  • Checked Exception vs Unchecked Exception

    class ExceptionEx10 {
       public static void main(String args[])
       {
           throw new Exception();  // 고의로 Exception을 발생시킨다.
       }
    }
    

    실행결과

     ExcpetionEx10.java:4: unsupported exception java.lang.Exception; must be caught or decalred to be thrown
         throw new Exception();
    ^
    1 error
    

    ExceptionEx10과 같은 경우 에러가 발생하고 컴파일이 완료되지 않는다 예외처리가 되어야 할 부분에 처리가 되어 있지 않다는 에러이다.

  class ExceptionEx11 {
       public static void main(String args[])
       {
           throw new RuntimeException();  // 고의로 RuntimeException을 발생시킨다.
       }
   }

실행결과

     Exception in thread "main" java.lang.RuntimeException
        at ExceptionEx11.main(ExceptionEx11.java:4)

ExceptionEx11과 같은 경우는 예외처리를 하지 않았음에도 불구하고 컴파일 에러는 생기지 않았다 실행하면 RuntimeException이 발생하여 비정상적으로 종료될 것이다. 컴파일러가 예외처리를 확인하지 않는 RuntimeException 클래스들은 'unchecked'라고 부르고 예외처리를 확인하는 Exception 클래스들은 'checked'라고 부른다.

1.7 메서드의 예외 선언하기

지금까지는 예외를 try-catch문을 사용해서 처리했었는데 예외를 method에 선언하는 방법도 있다.

    void method() throws Exception1, Exception2, ...ExceptionN {
     // ...
    }

아래와 같이 최고조상인 Exception클래스를 메서드에 선언하면 이 메서드는 모든 종류의 예외가 발생할 가능성이 있다는 뜻이다.

    void method() throws Exception {
     // ...
    }

메서드의 선언부에 예외를 선언함으로서 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 때 이 메서드를 사용하지 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알수 있다. 기존의 많은 다른 언어에서는 볼 수 없는 형태인데, 자바에서는 메서드를 사용하는 쪽에서 예외처리를 하도록 강요하기 때문에 프로그래머들의 짐을 덜어주는 것은 물론이고 보다 견고한 프로그램 코드를 작성할 수 있도록 도와준다.

Java API에서 찾아본 Object class의 wait() method

 public final void wait() throws InterruptedException


    Waits to be notified by another thread of a change in this object.
    The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until
    another thread notifies threads waiting on this object's monitor to wake up either through a call to the notify method
    or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.

    This method should only be called by a thread that is the owner of this object's monitor. See the notify method
    for a description of the ways in which a thread can become the owner of a monitor.


    Throws: IllegalMonitorStateException
        if the current thread is not the owner of the object's monitor.
    Throws: InterruptedException
        if another thread has interrupted this thread.

여기서 의미하는 것은 wait() 메서드를 호출하게 되면 InterruptedException이 발생할 수 있으므로 wait()을 호출하는 메서드에서 InterruptedException을 처리해주어야 한다는 것이다.

Class java.lang.InterruptedException

java.lang.Object
   |
   +----java.lang.Throwable
           |
           +----java.lang.Exception
                   |
                   +----java.lang.InterruptedException

위의 구조에서 볼 수 있듯이 InterruptedException은 Exception의 자손이기 때문에 이는 'checked' Exception 이므로 반드시 처리해주어야 하는 예외임을 알수 있다.

Class java.lang.IllegalMonitorStateException

java.lang.Object
   |
   +----java.lang.Throwable
           |
           +----java.lang.Exception
                   |
                   +----java.lang.RuntimeException
                           |
                           +----java.lang.IllegalMonitorStateException

wait() 메서드는 또한 IllegalMonitorStateException 을 발생시킬 수 있는데 IllegalMonitorStateException은 RuntimeException의 자손이기 때문에 예외처리를 하지 않아도 된다.

지금까지 본것처럼 메서드의 예외를 선언할때 보통은 RuntimeException클래스들은 적지 않는다. 적는다고 해서 문제가 되지는 않지만 보통은 반드시 처리해야 할 예외만 적어둔다.

사실 예외를 메서드의 throws에 명시하는것은 호출하는 메서드에 예외처리를 떠넘기는 것이다. 예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있으며 이런식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main 메서드에서도 예외처리가 되지 않으면 main마저 종료되어 프로그램이 종료된다.

  class ExceptionEx12 {
       public static void main(String args[]) throws Exception {
           method1()  // 같은클래스내의 static 멤버이므로 객체생성 없이 직접 호출 가능
       }

       static void method1() throws Exception {
           method2()
       }

       static void method2() throws Exception
       {
           throw new Exception();
       }
   }

실행결과

   java.lang.Exception
        at ExceptionEx12.method2(ExceptionEx12.java:11)
        at ExceptionEx12.method1(ExceptionEx12.java:7)
        at ExceptionEx12.main(ExceptionEx12.java:3)
  • 예외가 발생했을 때 모두 3개의 메서드가 호출스택에 있었으며,
  • 예외가 발생한 곳은 제일 윗줄에 있는 method2()라는 것과
  • main메서드가 method1()을 그리고 method1()은 method2()를 호출했다는 것을 알 수 있다

Exception을 throw 한 곳이 있으면 반드시 어디선가에서는 try-catch문으로 예외처리를 해야 한다.

class ExceptionEx15 {
    public static void main(String[] args) {
        // command line에서 입력받은 값을 이름으로 갖는 파일을 생성한다.
        File f = createFile(args[0]);
        System.out.println( f.getName() + " 파일이 성공적으로 생성되었습니다.");
    }

    static File createFile(String fileName) {
        try {
            if (fileName==null || fileName.equals(""))
                throw new Exception("파일이름이 유효하지 않습니다.");
        } catch (Exception e) {
             // fileName이 부적절한 경우, 파일 이름을 '제목없음.txt'로 한다.
            fileName = "제목없음.txt";
        } finally {
            File f = new File(fileName); // File클래스의 객체를 만든다.
            createNewFile(f);           // 생성된 객체를 이용해서 파일을 생성한다.
            return f;
        }
    }   // createFile메서드의 끝

    static void createNewFile(File f) {
        try {
            f.createNewFile();      // 파일을 생성한다.
        } catch(Exception e){ }
    }
}

실행결과

java ExceptionEx15 "test.txt"
test.txt 파일이 성공적으로 생성되었습니다.
java ExceptionEx15 ""
제목없음.txt 파일이 성공적으로 생성되었습니다.

이 예제는 예외가 발생한 메서드에서 직접 예외를 처리하도록 되어있다 createFile메서드를 호출한 main에서는 예외가 발생한 사실을 알지 못한다. 적절하지 못한 파일 이름이 입력되면 예외를 발생시키고 finally 블럭에서는 예외의 발생여부에 관계없이 파일을 생성하는 일을 한다

1.8 finally블럭

try 블럭이 끝날 때 항상 수행되는 블럭

  • Exception 이 발생하더라도
  • return, continue, 또는 break 등이라도
PrintWriter out = null;
try {
    out = new PrintWriter(new FileWriter("OutFile.txt");
    ...
} catch (IOException e) {
    ...
} finally {
    if (out != null) 
        out.close();
}

1.9 자동 자원 반환 - try-with-resources문

Java 7 부터 추가된 try-catch문 변형

  • java.lang.AutoCloseable
  • Suppressed Exception : try-catch 절에서 예외가 발생하면 AutoCloseable.close() 에서 발생할 수도 있는 예외는 억제된 예외로 처리된다.
try (FileWriter f = new FileWriter("OutFile.txt");
     PrintWriter out = new PrintWriter(f)) {
    ...
} catch (IOException e) {
    ...
}

1.10 사용자정의 예외 만들기

필요에 따라 새로운 예외 클래스를 정의할 수 있다.

class MyException extends Exception {
    private final int ERR_CODE;

    MyException(String msg, int errCode) {
        super(msg);
        ERR_CODE = errCode;
    }

    MyException(String msg) {
        this(msg, 100);
    }

    public int getErrCode() {
        return ERR_CODE;
    }
}

Exception

  • checked
  • 컴파일러에 의해 반드시 예외처리해야 한다.
  • IOException, SQLException, DataAccessException, ClassNotFoundException, InvocationTargetException, MalformedURLException, ...

RuntimeException

  • unchecked
  • 컴파일러가 확인하지 않으며, 잘못된 개발에 의해 발생하는 예외상황
  • NullPointerException, ArrayIndexOutOfBound, IllegalArgumentException, IllegalStateException, ...

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

예외 전파(회피)를 위해 다시 예외를 발생시키는 방식

void method1() throws Exception {
    try {
        throw new Exception();
    catch (Exception e) {
        ...
        throw e;
    }
}

1.12 연결된 예외 (chained exception)

예외 전환을 위해 다른 예외로 발생시키는 방식

try {
    ...
} catch (SpaceException e) {
    InstallException ie = new InstallException();
    ie.initCause(e);
    throw ie;
}