예외(Exception)

자바에서 매우 중요한 예외

자바에서 예외는 우리가 예상한, 혹은 예상치도 못한 일이 발생하는 것을 미리 예견하고 안전장치를 하는 것을 말한다.

가장 일반적인 예가 null인 객체에 메소드를 호출한다든지, 5개의 공간을 가지는 배열을 만들었는데 6번째 값을 읽으라고 하든지, 어떤 파일을 읽으라고 했는데 읽을 파일이 존재하지 않는다든지, 네트워크 연결이 되어 있는 어떤 서버가 갑자기 작동을 멈춰서 연결이 끊겨버린다든지 하는 경우가 여기에 속한다.

try-catch는 짝이다

가장 일반적인 예로 설명했던 배열 범위 밖의 값을 읽으려고 할 때를 살펴보자.

package c.exception;

public class ExceptionSample {

    public static void main(String[] args) {
        ExceptionSample sample = new ExceptionSample();
        sample.arrayOutOfBounds();
    }

    public void arrayOutOfBounds() {
        int[] intArray = new int[5];
        System.out.println(intArray[5]);
    }
}

정상적으로 컴파일이 되지만 프로그램을 실행하면 ArrayIndexOutOfBoundsException이라는 것이 발생한다.

이와같은 예외 메시지가 발생하지 않도록 하기 위해서는 메소드 괄호 안에 try를 쓰고 중괄호로 감싸고, catch라고 쓰고 메소드를 호출하는 것처러 소괄호 안에 Exception이라는 클래스 이름과 매개 변수 이름 같은 e라는 것을 써준다. 그 다음엔 아무것도 하지는 않았지만 중괄호로 묶어준다.

package c.exception;

public class ExceptionSample {

    public static void main(String[] args) {
        ExceptionSample sample = new ExceptionSample();
        sample.arrayOutOfBounds();
    }

    public void arrayOutOfBounds() {
        int[] intArray = new int[5];
        try {
          // int[] intArray = new int[5];
            System.out.println(intArray[5]);
        } catch (Exception e) {

        }
    }
}

다음과 같은 메소드를 만들어 놓고 실행하면 아무런 메시지를 출력하지 않고 끝난다. 자바의 try-catch의 try 블록 안에서 예외가 발생되면 그 이하의 문장은 실행되지 않고 바로 catch 블록으로 넘어간다. 다음의 코드를 확인하자.

package c.exception;

public class ExceptionSample {

    public static void main(String[] args) {
        ExceptionSample sample = new ExceptionSample();
        sample.arrayOutOfBounds();
    }

    public void arrayOutOfBounds() {
        int[] intArray = new int[5];
        try {
            System.out.println(intArray[5]);
          System.out.println("This code should run.");
        } catch (Exception e) {
            System.out.println("Exception occured.");

        }
    }
}

다음과 같이 수정하면 try-catch문이 실행되고 다음 문장이 실행되는 것을 알 수 있다.

package c.exception;

public class ExceptionSample {

    public static void main(String[] args) {
        ExceptionSample sample = new ExceptionSample();
        sample.arrayOutOfBounds();
    }

    public void arrayOutOfBounds() {
        int[] intArray = new int[5];
        try {
            System.out.println(intArray[5]);
        } catch (Exception e) {
            System.out.println("Exception occured.");

        }
        System.out.println("This code should run.");
    }
}

try-catch를 사용할 때 try 블록 냉에서 예외가 발생하면, 예외가 발생한 줄 이후에 있는 try 블록 내의 코들들은 수행되지 않는다. 그 다음에 catch 블록에 있는 문장이 실행이 된다. try 블록 냉에서 예외가 발생하지 않으면 catch 내에 있는 코드는 실행되지 않는다. 그리고 try-catch 구문 밖에 있는 문장이 실행된다.

try-catch를 사용하면서 처음에 적응하기 힘든 변수 선언

try-catch를 사용할 때 가장 하기 쉬운 실수가 있다. 바로 변수의 지정이다. try 블록은 중괄호로 쌓여 있는 블록이다. 따라서 try 블록 내에서 선언한 변수를 catch에서 사용할 수 없다.

package c.exception;

public class ExceptionSample {
  // 중간 생략
    public void arrayOutOfBounds() {
        int[] intArray = new int[5];
        try {
            System.out.println(intArray[5]);
        } catch (Exception e) {
            System.out.println(intArray.length);
        }
        System.out.println("This code should run.");
    }
}

위의 코드는 문제 없이 실행이 된다. 하지만 다음과 같이 코드를 변경하고 실행하면 컴파일 에러가 발생한다.

package c.exception;

public class ExceptionSample {
  // 중간 생략
    public void arrayOutOfBounds() {
        try {
          int[] intArray = new int[5];
            System.out.println(intArray[5]);
        } catch (Exception e) {
            System.out.println(intArray.length);
        }
        System.out.println("This code should run.");
    }
}

intArray가 try 블록 안에서 지역변수로 선언되었기 때문에 catch 블록에서는 intArray가 누군지 알 수 없는 것이다. 그래서 이런 문제를 해결하기 위해서 일반적으로 catch 문장에서 사용할 변수는 다음과 같이 try 앞에 미리 선언해 놓는다.

package c.exception;

public class ExceptionSample {
  // 중간 생략
    public void arrayOutOfBounds() {
      int[] intArray=null;
        try {
          intArray = new int[5];
            System.out.println(intArray[5]);
        } catch (Exception e) {
            System.out.println(intArray.length);
        }
        System.out.println("This code should run.");
    }
}

finally 야~ 넌 무슨 일이 생겨도 반드시 실행해야 돼

try-catch 구문에 추가로 붙을 수 있는 블록이 finally 블록이다. finally 블록은 예외가 발생하건 안하건 상관없이 실행하는 블록이다. 아래 코드를 실행하면 catch 블록이 실행되고 finally 블록이 실행되는 것을 알 수 있다.

package c.exception;

public class ExceptionSample {
  // 중간 생략
    public void finallySample() {
        int[] intArray = new int[5];
        try {
            System.out.println(intArray[5]);
            //System.out.println(intArray[4]);
        } catch (Exception e) {
            System.out.println(intArray.length);
        } finally {
            System.out.println("Here is finally");
        }
        System.out.println("This code should run.");
    }
}

위 try 문에서 예외를 발생시키지 않게 수정해도 finally 블록이 실행되는 것을 알 수 있다.

두 개 이상의 catch

catch 블록에 catch 블록을 하나 더 추가할 수 있다.

package c.exception;

public class ExceptionSample {
  // 중간 생략
    public void multiCatch() {
        int[] intArray = new int[5];
        try {
            System.out.println(intArray[5]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBoundsException occured");
        } catch (Exception e) {
            System.out.println("Exception occured");
        }
    }
}

실행을 해보면

ArrayIndexOutOfBoundsException

이라는 예외를 볼 수 있다. 여기서 catch 블록의 순서가 중요하다. 아래와 같이 순서를 바꾸면 에러가 발생하는 것을 알 수 있다. 이미 catch를 했는데 다시 catch 블록을 사용했다고 메시지가 발생한다.

package c.exception;

public class ExceptionSample {
  // 중간 생략
    public void multiCatch() {
        int[] intArray = new int[5];
        try {
            System.out.println(intArray[5]);
        } catch (Exception e) {
            System.out.println("Exception occured");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBoundsException occured");
        }
    }
}

예외는 부모 예외 클래스가 이미 catch하고, 자식 클래스가 그 아래에서 catch를 하도록 되어 있을 경우에는 자식 클래스가 예외를 처리할 기회를 놓치게 되므로 에러를 발생시킨다.

다음과 같이 메소드를 수정하자.

package c.exception;

public class ExceptionSample {
  // 중간 생략
    public void multiCatch() {
        int[] intArray = new int[5];
        try {
            System.out.println(intArray[5]);
        } catch (NullPointerException e) {
            System.out.println("NullPointerException occured");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBoundsException occured");
        } catch (Exception e) {
            System.out.println("Exception occured");
        }
    }
}

NullPointerException을 추가했다. 현재 예제상으로는 null인 객체를 갖고 있지 않기 때문에 NullPointerException은 건너 뛰고 예외가 처리가 된다. 만일 try 블록을 다음과 같이 변경하면

// 앞 부분 생략
try {
  intArray = null;
  System.out.println(intArray[5]);
}
//이하 생략

NullPointerException을 발생시키는 것을 알 수 있다. 위에서 예외가 발생하면 밑의 예외는 모두 무시되어 catch 블록을 처리하지 않는다. 가장 마지막 catch 블록은 Exception 클래스를 선언하여 예외들이 빠져 나가지 않게 처리하는 것이 좋다.

이번 절에서 배운 것을 정리하면 다음과 같다.

  • try 다음에 오는 catch블록은 1개 이상 올 수 있다.

  • 먼저 선언한 catch 블록의 예외 클래스가 다음에 선언한 catch 블록의 부모에 속하면, 자식에 속하는 catch 블록은 절대 실행될 일이 없으므로 컴파일이 되지 않는다.

  • 하나의 try 블록에서 예외가 발생하면 그 예외와 관련이 있는 catch 블록을 찾아서 실행한다.

  • catch 블록 중 발생한 예외와 관련있는 블록이 없으면, 예외가 발생되면서 해당 쓰레드는 끝난다. 따라서 마지막 catch 블록에는 Exception 클래스로 묶어주는 습관을 들어야 한다.

예외의 종류는 세 가지다

  • checked exception

  • error

  • runtime exception or unchecked exception

error(에러)

에러는 자바 프로그램 밖에서 발생한 예외를 말한다. 에러와 예외로 끝나는 가장 큰 차이는 프로그램 안에서 발생했는지 밖에서 발생했는지 여부이다. 하지만, 더 큰 차이는 프로그램이 멈추어 버리느냐 계속 실행할 수 있느냐의 차이다. 더 정확하게 말하면 Error는 프로세스에 영향을 주고, Exception은 쓰레드에만 영향을 준다.

runtime exception(런타임 예외)

런타임 예외는 예외가 발생할 것을 미리 감지하지 못했을 때 발생한다. 런타임 예외는 RuntimeException의 확장한 예외들이다. 컴파일시에 체크를 하지 않기 때문에 unchecked exception이라고도 부른다.

모든 예외의 할아버지는 java.lang.Throwable 클래스다

Exception과 Error클래스의 부모 클래스는 Throwable 클래스이다. Throwable 클래스에서 선언되어 있는 가장 많이 사용되는 메소드들 중에 - getMessage() - toString() - printStackTrace() 등이 있다.

public class ExceptionSample {

    //중간생략

    public void throwable() {
        int[] intArray = new int[5];
        try {
            intArray = null;
            System.out.println(intArray[5]);
        } catch (Throwable t) {
            System.out.println(t.getMessage());
            System.out.println(t);
            t.printStackTrace();
        }
    }
}

난 예외를 던질 거니까 throws라고 써 놓을께

예외를 발생시키는 방법에 대해서 알아보자.

public class ExceptionSample {
        public void throwException(int number) throws Exception {
        try {
            if (number>12) {
                throw new Exception("Number is over than 12.");
            }
        } catch (NullPointerException e) {
            e.printStackTrace();
        }
//        catch (Exception e) {
//            e.printStackTrace();
//        }
    }

}

throw 다음에 Exception 객체를 놓으면 된다. 그러면 그 메소드 안에서 예외를 catch하는 문장을 마련해 놓든지 아니면 메소드 선언 뒤에 throws Exception을 선언해야 한다. main 문장에서 역시 throws를 해도 된다.

public class ExceptionSample {

    public static void main(String[] args) throws Exception {
        ExceptionSample sample = new ExceptionSample();
//        sample.throwable();
//        sample.throwException(13);
        try {
            sample.throwException(13);
        } catch (MalformedURLException e) {
            System.out.println("malformed:" + e);
        }
    }

    public void throwException(int number) throws Exception {
        try {
            if (number>12) {
//                throw new Exception("Number is over than 12.");
                throw new MalformedURLException("Number is over than 12.");
            }
        } catch (NullPointerException e) {
            System.out.println("NullPointerException:" + e);
//            e.printStackTrace();
        }
//        catch (Exception e) {
//            e.printStackTrace();
//        }
    }
}

여러 개의 throw를 할 수 있다.

public class ExceptionSample {

    public void multiThrow() throws NullPointerException, ArrayIndexOutOfBoundsException {
        int[] intArray = new int[5];
        intArray = null;
        System.out.println(intArray[5]);
    }
}

내가 예외를 만들 수도 있다구?

Throwable이나 Exception 클래스를 상속받아 사용자가 직접 예외 클래스를 만들 수 있다. 새로운 MyException이란 클래스를 만들자.

package c.exception;

/**
 * Created by dyoon on 2016-10-26 026.
 */
public class MyException extends Exception {
    public MyException() {
        super();
    }

    public MyException(String message) {
        super(message);
    }
}

ExceptionSample 클래스에 새로운 메소드 throwException2()를 만들자.

public class ExceptionSample {
    public void throwException2(int number) throws MyException {
        try {
            throw new MyException("Number is over than 12.");
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}

자바 예외 처리 전략

try {
    //예외 발생 코드
} catch {
    //아무 코드 없음.
}

와 같이 하는 것을 피해야 한다.

직접해 봅시다

다음과 같이 간단하게 나누기 계산을 수행하는 클래스가 있다.

package c.exception.practice;

public class Calculator {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        calc.printDivide(1, 2);
        calc.printDivide(1, 0);
    }

    public void printDivide(double d1, double d2) {
        double result = d1/d2;
        System.out.println(result);
    }
}
  1. 이 클래스를 작성하고 컴파일해 보자. 컴파일이 정상적으로 되었다면 실행을 해보자. 결과는 다음과 같다.

0.5
Infinity
  1. 왜 두 번째 결과가 Infinity로 나왔는지 이야기해 보자.

  2. 만약 두 번째 값이 0이면 “Second value can’t be Zero”라는 메시지를 갖는 예외를 발생시키자. 그리고 발생시킨 예외를 throw할 수 있도록 코드를 수정하자.

  3. main() 메소드에서 printDivide() 메소드를 호출하는 부분을 try-catch로 묶자.

  4. main() 메소드의 catch 문장에서 다음과 같은 메시지가 출력되도록 만들자.

    0.5
    Second value can't be Zero