본문 바로가기

Java 길찾기/이것이 자바다

[Java] 예외

예외와 예외 클래스

컴퓨터 하드웨어의 오동작 또는 고장으로 인해 응용프로그램 실행 오류가 발생하는 것을 자바에서는 에러라고 한다. 에러는 JVM 실행에 문제가 생겼다는 것이므로 JVM 위에서 실행되는 프로그램을 아무리 견고하게 만들어도 결국 실행 불능이 된다. 개발자는 이런 에러에 대처할 방법이 없다. 자바에서는 에러 이외에 예외(exception)라고 부르는 오류가 있다. 예외란 사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 프로그램 오류를 말한다. 예외가 발생되면 프로그램은 곧바로 종료된다는 점에서 에러와 동일하다. 그러나 예외는 예외 처리를 통해 프로그램을 종료하지 않고 정상 실행 상태가 유지되도록 할 수 있다.

 

예외는 두 가지 종류가 있다. 하나는 일반 예외(Exception)이고, 다른 하나는 실행 예외(Runtime Exception)이다. 일반 예외는 컴파일러 체크 예외라고도 하는데, 자바 소스를 컴파일하는 과정에서 예외 처리 코드가 필요한지 검사하기 때문이다. 만약 예외 처리 코드가 없다면 컴파일 오류가 발생한다. 실행 예외는 컴파일하는 과정에서 예외 처리 코드를 검사하지 않는 예외를 말한다. 컴파일 시 예외 처리를 확인하는 차이일 뿐, 두 가지 예외는 모두 예외 처리가 필요하다. 자바에서는 예외를 클래스로 관리한다. JVM은 프로그램을 실행하는 도중에 예외가 발생하면 해당 예외 클래스로 객체를 생성한다. 그리고 나서 예외 처리 코드에서 예외 객체를 이용할 수 있도록 해준다. 모든 예외 클래스들은 java.lng.Exception 클래스를 상속받는다.

 

일반 예외와 실행 예외 클래스를 구별하는 방법은 일반 예외는 Exception을 상속받지만 Runtime Exception을 상속받지 않는 클래스들이고, 실행 예외는 RuntimeException을 상속받은 클래스들이다. RuntimeException 역시 Exception을 상속받지만, JVM은 RuntimeException을 상속했는지 여부를 보고 실행 예외를 판단한다.

 

 

실행 예외

실행 예외는 자바 컴파일러가 체크를 하지 않기 때문에 오로지 개발자의 경험에 의해서 예외 처리 코드를 삽입해야 한다. 만약 개발자가 실행 예외에 대해 처리 코드를 넣지 않았을 경우, 해당 예외가 발생하면 프로그램은 곧바로 종료된다. 자바 프로그램 개발 경력이 풍부하다면 언제, 어떤 실행 예외가 발생하는지 쉽게 알 수 있지만, 이제 시작하는 개발자라면 지금부터 설명하는 몇 가지 실행 예외를 잘 익혀두길 바란다. 자바 프로그램에서 자주 발생하는 실행 예외이므로 언제 발생되고, 어떤 오류 메시지가 출력되는지 잘 알아둘 필요가 있다.

 

NullPointerException

자바 프로그램에서 가장 빈번하게 발생하는 실행 예외는 java.lang.NullPointerException일 것이다. 이것은 객체 참조가 없는 상태, 즉 null 값을 갖는 참조 변수로 객체 접근 연산자인 도트( . )를 사용했을 때 발생한다. 객체가 없는 상태에서 객체를 사용하려 했으니 예외가 발생하는 것이다.

// NullPointerException이 발생하는 경우
public class NullPointerExceptionExample {
    public static void main(String[] args) {
        String data = null;
        System.out.println(data.toString());
    }
}

3라인에서 data 변수는 null 값을 가지고 있기 때문에 String 객체를 참조하고 있지 않다. 하지만 4라인에서 String 객체의 toString() 메소드를 호출하고 있다. 여기서 NullPointerException이 발생한다.

 

프로그램에서 예외가 발생하면 예외 메세지가 Console 뷰에 출력되면서 프로그램이 종료된다. Console 뷰에 출력되는 내용에는 어떤 예외가 어떤 소스의 몇 번째 코드에서 발생했는지에 대한 정보가 들어있다. 위 예제의 경우 NullPointerExceptionExample.java 소스의 4번째 코드에서 발생했음을 알 수 있다. Console 뷰에서 밑줄 처리된 NullPointerExceptionExample.java:4를 마우스로 클릭하면 소스 편집기는 정확히 NullPointerExceptionExample.java의 4번째 줄을 하이라이팅해준다.

 

ArrayIndexOutOfBoundExeption

배열에서 인덱스 범위를 초과하여 사용할 경우 실행 예외인 java.lang.ArrayIndexOutOfBoundExeption이 발생한다. 예를 들어 길이가 3인 int[] arr = new int[3] 배열을 선언했다면, 배열 항목을 지정하기 위해 arr[0] ~ arr[2]를 사용할 수 있다. 하지만 arr[3]을 사용하면 인덱스 범위를 초과했기 때문에 ArrayIndexOutOfBoundExeption이 발생한다.

// ArrayIndexOutOfBoundExeption
public class ArrayIndexOutOfBoundExeptionExample {
    public static void main(String[] args) {
        String data1 = args[0];
        String data2 = args[1];
        
        System.out.println("args[0]: " + data1);
        System.out.println("args[1]: " + data2);
    }
}

위 예제를 실행하면 3라인에서 ArrayIndexOutOfBoundExeption이 발생한다. 그 이유는 두 개의 실행 매개값을 주지 않았기 때문에 args[0], args[1]과 같이 인덱스를 사용할 수 없다.

 

명령 프롬프트에서 "java ArrayIndexOutOfBoundExeptionExample 값1 값2" 와 같이 실행하거나 이클립스 메뉴에서 Run Configurations를 선택한 후, Arguments 탭의 Program argument 입력란에 '배열 인덱스' 와 같이 두 개의 매개값을 입력하고 실행하면 예외가 발생하지 않는다.

 

예제를 다음과 같이 수정하면 ArrayIndexOutOfBoundExeption이 발생하지 않는 종은 프로그램이 된다. 배열값을 읽기 전에 배열의 길이를 먼저 조사하는 것이다. 실행 매개값이 없거나 부족할 경우 조건문을 이용해서 사용자에게 실행 방법을 알려준다.

// ArrayIndexOutOfBoundExeption
public class ArrayIndexOutOfBoundExeptionExample {
    public static void main(String[] args) {
        if(args.length == 2) {
            String data1 = args[0];
            String data2 = args[1];
            System.out.println("args[0]: " + data1);
            System.out.println("args[1]: " + data2);
        }
        else {
            System.out.println("[실행 방법]");
            System.out.println("java ArrayIndexOutOfBoundExeptionExample ");
            System.out.println("값1  값2");
        }
    }
}

 

NumberFormatException

프로그램을 개발하다 보면 문자열로 되어 있는 데이터를 숫자로 변경하는 경우가 자주 발생한다. 문자열을 숫자로 변환하는 방법은 여러 가지가 있지만 가장 많이 사용되는 코드는 다음과 같다.

 

반환 타입 메소드명(매개 변수) 설명
int Interger.parseInt(String s) 주어진 문자열을 정수로 변환해서 리턴
double Double.parseDouble(String s) 주어진 문자열을 실수로 변환해서 리턴

 

Interger 와 Double은 포장(Wrapper) 클래스라고 하는데, 나중에 설명할 것이다. 이 클래스의 정적 메소드인 paresXXX() 메소드를 이용하면 문자열을 숫자로 변환할 수 있다. 이 메소드들은 매개값인 문자열이 숫자로 변환될 수 있다면 숫자를 리턴하지만, 숫자로 변환될 수 없는 문자가 포함되어 있다면 java.lang.NumberFormatException을 발생시킨다.

// NumberFormatException
public class NumberFormatExceptionExample {
    public static void main(String[] args) {
        String data1 = "100";
        String data2 = "a100";
        
        int value1 = Integer.parseInt(data1);
        int value2 = Integer.parseInt(data2); // 예외 발생
        
        int result = value1 + value2;
        System.out.println(data1 + "+" + data2 + "=" + result);
    }
}

data1 변수의 "100" 문자열은 숫자로 변환이 가능하기 때문에 6라인이 정상적으로 실행되지만, data2 변수의 "a100" 문자열은 숫자로 변환할 수 없기 때문에 NumberFormatExceptiondl 발생한다.

 

ClassCastException

타입 변환(Casting)은 상위 클래스와 하위 클래스 간에 발생하고 구현 클래스와 인터페이스 간에도 발생한다. 이러한 고나계가 아니라면 클래스는 다른 클래스로 타입 변환할 수 없다. 억지로 타입 변환을 시도할 경우 ClassCastException이 발생한다. 

다음은 올바른 타입 변환을 보여준다. Animal 타입 변수에 대입된 객체가 Dog 이므로 다시 Dog 타입으로 변환하는 것은 아무런 문제가 없다. 마찬가지로 RemoteControl 타입 변수에 대입된 객체가 Television 이므로 다시 Television 타입으로 변환하는 것은 아무런 문제가 없다.

Animal animal = new Dog();
Dog dog = (Dog) animal();

RemoteControl rc = new Television();
Television tv = (Television) rc;

 

그러나 다음과 같이 타입 변환을 하면 ClassCastException이 발생한다. 대입된 객체가 아닌 다른 클래스 타입으로 타입 변환했기 때문이다.

Animal animal = new Dog();
Cat cat = (Cat) animal;

RemoteControl rc = new Television();
Audio audio = (Audio) rc;

 

ClassCastException을 발생시키지 않으려면 타입 변환 전에 타입 변환이 가능한지 instanceof 연산자로 확인하는 것이 좋다. instanceof 연산의 결과가 true 이면 좌항 객체를 우항 타입으로 변환이 가능하다는 뜻이다.

Animal animal = new Dog();
if(animal instanceof Dog) {
    Dog dog = (Dog) animal;
}
else if(animal instanceof Cat) {
    Cat cat = (Cat) animal;
}

RemoteControl rc = new Audio();
if(rc instanceof Television) {
    Television rc = (Television) rc;
}
else if(rc instanceof Audio) {
    Audio audio = (Audio) rc;
}

'Java 길찾기 > 이것이 자바다' 카테고리의 다른 글

[Java] 자동 리소스 닫기  (0) 2022.02.15
[Java] 예외 처리 코드  (0) 2022.02.14
[Java] 익명 객체  (0) 2022.02.10
[Java] 중첩 인터페이스  (0) 2022.02.09
[Java] 중첩 클래스  (0) 2022.02.08