본문 바로가기

Java/Java SE, EE

[Java/Java SE, EE] 예외(Exception)와 런타임 예외(Runtime exception)

예외(Exception)

컴퓨터 하드웨어의 오동작 또는 고장이 발생하면 우리가 실행중인 프로그램은 어떻게 될까요? 매우 높은 확률로 애플리케이션이 중지되거나 오작동하게 됩니다. 이를 자바에서는 에러(Error)라고 부릅니다. 에러는 JVM 실행에 문제가 발생했다는 것이므로, JVM 위에서 동작하는 프로그램을 아무리 견고하게 설계하였다 하더라도 실행에 문제가 생깁니다.

반면 예외(Exception)는 사용자의 예기치 않은 조작 또는 개발자의 잘못된 프로그래밍으로 인해 발생하는 프로그램의 오류입니다. 일단 예외가 발생하면 프로그램 또는 높은 확률로 중지되거나 오작동된다는 점은 예외가 동일합니다. 하지만 프로그램에서 발생하는 예외는 예외 처리(Exception handling)으로 프로그램이 중지되지 않고 실행 상태를 유지되도록 보완 할 수 있습니다.

예외에는 크게 두 가지 종류가 있습니다. 두 가지 예외는 컴파일러에서 예외 처리 검사 여부만 차이가 있고, 예외 처리가 필요함에는 동일합니다.

  • 일반 예외(Exception)
    컴파일러 체크 예외라고도 부릅니다. 컴파일러가 자바 소스를 컴파일하는 과정에서 예외 처리 코드의 필요성을 검사하고, 예외 처리가 충분치 않으면 컴파일 오류를 발생시킵니다. java.lang.Exception 클래스를 상속 받습니다.
  • 실행-런타임 예외(Runtime exception)
    런타임 예외는 예외 처리 코드로 검사되지 않고 발생하는 예외를 의미합니다. java.lang.RuntimeException 클래스를 상속 받습니다.

자바에서는 예외를 클래스로 관리합니다. JVM은 프로그램이 실행되는 도중에 어떤 예외가 발생하면, 해당 예외와 일치하는 클래스를 인스턴싱합니다. 그리고 나서 예외 처리 코드가 해당 인스턴스를 다룰 수 있도록 합니다.

일반 예외와 런타임 예외

자바의 모든 예외 클래스는 java.lang.Exception 클래스를 상속 받습니다.

public class Exception extends Throwable { }

런타임 예외의 경우 java.lang.RuntimeException 클래스를 상속 받습니다.

public class RuntimeException extends Exception { }

따라서 일반 예외와 런타임 예외는 어떤 예외 클래스를 기반으로 동작하는지에 따라서 구분 할 수 있습니다. 기본적으로 런타임 예외가 상속 받는RuntimeException 클래스 역시 Exception 클래스 위에서 구현되지만, JVM은 먼저 RuntimeException 클래스의 상속 여부에 따라서 예외를 런타임 예외를 판단합니다.

일반 예외

java.lang.Exception 클래스를 상속 받으며, 컴파일러에서 일반 예외에 대한 처리가 필요한 경우 컴파일 오류를 일으킵니다.

public class Exception extends Throwable { }

대표적인 일반 예외는 java.lang.IOException이 있습니다. 예를 들어, 다음과 같이 임시 파일을 만드는 File.createTempFile 정적 메소드는 IOException을 throws 합니다.

File file = File.createTempFile("prefix", "suffix");

일반 예외가 발생하면, 컴파일 오류가 발생하기 때문에 코드를 수정하기 전까지 빌드 또는 실행이 불가능합니다. 따라서 우리가 작성하는 프로그램은 다음과 같이 예외 처리가 함께 처리될 것입니다.

try {
   File file = File.createTempFile("prefix", "suffix");
} catch (IOException e) {
   e.printStackTrace();
}

런타임 예외

java.lang.RuntimeException 클래스를 상속 받으며, 컴파일러가 예외 처리에 대한 필요성을 검증하지 않습니다. 따라서 프로그램이 실행되는 도중 언제라도 RuntimeException이 발생할 수 있음을 염두하고 코드를 작성해야 합니다.

public class RuntimeException extends Exception { }

컴파일러가 예외 처리 누락 여부에 대해서 따로 경고하지 않으므로, 순전히 개발자의 경험에 의해서 프로그램의 안정성이 좌지우지됩니다. 따라서 대표적인 런타임 예외와 각 예외가 어떤 오류 메시지가 출력되는지 미리 숙지하고 있는 것이 좋습니다.

이어지는 섹션에서는 대표적인 런타임 예외 몇 종류에 대해서 소개하도록 하겠습니다. 전체 예외 클래스에 대해서 다룰 수 없으므로, 개별 예외 클래스는 별도의 포스트에서 항목 별로 다루도록 하겠습니다.

NullPointerException

java.lang.NullPointerException은 자바 프로그램에서 가장 빈번하게 발생하는 런타임 예외입니다.

public class NullPointerException extends RuntimeException { }

NullPointerException은 객체 참조가 없는 상태, 즉 null 값을 갖고 있는 참조 변수에 대해서 객체 접근 연산자를 사용했을 때 발생합니다. 객체가 할당되지 않은 상태에서 객체의 내용에 접근하려고 하기 때문입니다.

try {
   String obj = null;
   obj.length();
} catch (NullPointerException e) {
   e.printStackTrace();
}

NullPointerException 참조(Reference) 타입 변수가 null 값을 갖고 있을 때에만 발생합니다. 자바의 기본(Primitive) 타입은 null 값을 저장 할 수 없기 때문입니다.

ArrayIndexOutOfBoundsException

java.lang.ArrayIndexOutOfBoundsException은 배열에서 인덱스 범위를 벗어나는 엘리먼트에 대한 접근 시 발생합니다. 

public class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException { }

예를 들어, 길이가 10인 배열은 인덱스 0~9에 해당하는 엘리먼트를 갖습니다. 만약 0보다 작거나, 9보다 큰 인덱스를 통해 배열의 엘리먼트에 접근하게 되면 ArrayIndexOutOfBoundsException가 발생합니다.

try {
   int[] array = new int[10];
   array[-1] = 10;
   array[10] = 20;
} catch (ArrayIndexOutOfBoundsException e) {
   e.printStackTrace();
}

NumberFormatException

java.lang.NumberFormatException은 문자열을 숫자로 변환하는 과정에서 발생 할 수 있습니다.

public class NumberFormatException extends IllegalArgumentException { }

변환하려는 문자열이 정수 또는 실수와 정확하게 호환되지 않는 경우 예외가 발생합니다.

try {
   String str_var = "1000-ABC";
   int int_var = Integer.valueOf(str_var);
   float float_var = Float.valueOf(str_var);
} catch (NumberFormatException e) {
   e.printStackTrace();
}

ClassCastException

java.lang.ClassCastException은 다형성에 의한 업 캐스팅(Upcasting) 또는 다운 캐스팅(Downcasting)이 아닌, 상속 관계가 없는 클래스 간의 캐스팅에서 발생합니다.

public class ClassCastException extends RuntimeException { }

기본적으로 상속 관계가 없는 클래스 간 캐스팅은 컴파일러가 오류(Inconvertible types; cannot cast 'A' to 'B')를 발생시킵니다. 하지만 형제 관계(동일한 부모 클래스를 상속 받는)에 있는 객체들은 서로의 실제 객체 타입을 알 수 없으므로 컴파일러가 사전에 방지하지 못하는 경우가 발생합니다.

class Parent { }
class Child extends Parent { }
class Sibling extends Parent { }

try {
   Parent child = new Child();
   Sibling sibling = (Sibling) child;
} catch (ClassCastException e) {
   e.printStackTrace();
}

Parent 객체는 자신을 상속받는 Sibling 클래스 타입 변수로 다운 캐스팅 될 수 있습니다. 자식-부모 관계이기 때문에 다형성이 성사되기 때문입니다. 하지만 실제 Parent 클래스 타입으로 선언된 변수는 Child 객체가 담겨있습니다. Parent-Child 클래스 간의 관계 역시 부모-자식 관계이기 때문에 업 캐스팅이 가능하기 때문입니다.

따라서 위 코드는 형제 클래스 Child-Sibling 간의 캐스팅이 발생합니다. 두 클래스는 상속 관계가 아니기 때문에 예외가 발생합니다.