[C++ to Java] 5. 예외 처리
Java의 예외 처리에 대해 알아보자. 2022-01-11

Exceptions

우리는 사용자의 프로그램 오작동이든, 시스템 내부의 예기치 못한 오류든 많은 경우의 예외 사항을 어플리케이션을 운영 하면서 맞게 됩니다. 그러므로 우리는 예외 처리에 대해서 알 필요가 있습니다.

예를 들어 보겠습니다. 만약 계산기 프로그램을 만든다고 가정 하였을 때, 사용자가 어떤 수를 0으로 나누려고 한다면 에러가 발생 할 것 입니다. 다음과 같이 말이죠.

public class Main {
    static public void main(String[] args) {
        int c = 5 / 0;
    }
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.main(Main.java:3)

위 에러를 잘 읽어보면, java.lang.ArithmeticException 이 발생 했다는 것을 알 수 있습니다. 우리는 C++에서 예외 처리를 위해서 try, catch 를 사용 한 것을 알고 있습니다. Java도 마찬 가지로, try, catch 문을 이용하여 예외 처리를 할 수 있습니다. 다음과 같이 말이죠.

public class Main {
    static public void main(String[] args) {
        try {
            int c = 5 / 0;
        } catch (ArithmeticException e) {
            System.out.println("0으로 나눌 수 없습니다.");
        }
    }
}
0으로 나눌 수 없습니다.

우리는 위와 같이 try 블럭에 예외처리를 시행 할 코드를 삽입 하고, catch 문에 예외가 발생할 것으로 예상 되는 Exception 클래스를 catch 문에 넣으면 됩니다. 그런데 우리가 예상치 못한 Exception 클래스가 throw (발생) 하면, 우리는 어떻게 해야 할 까요? 우리가 C++에서 배웠듯, Exception 클래스들은 Exception 이라는 클래스를 상속 받은 클래스 입니다. 그러므로, 우리는 전에 배웠던, 클래스의 다형성을 이용하여, 다양한 타입의, 예상치 못했던 Exception들을 일괄 처리 할 수 있습니다.

public class Main {
    static public void main(String[] args) {
        try {
            int[] a = {1, 2, 3};
            System.out.println(a[3]);
        } catch (ArithmeticException e) {
            System.out.println("0으로 나눌 수 없습니다.");
        } catch (Exception e) {
            System.out.println(e.getClass().getName());
            System.out.println("알 수 없는 에러가 발생 했습니다.");
        }
    }
}
java.lang.ArrayIndexOutOfBoundsException
알 수 없는 에러가 발생 했습니다.

Finally

예외가 처리 되든, 처리가 되지 않든 꼭 실행을 원하는 코드 블럭이 있을 수 있습니다. 그럴 때는 finally 블럭을 이용하여, 해당 블럭을 무조건 실행 하게 할 수 있습니다. 이 문구는, try 문에서 return이 되더라도, 무조건 실행 됩니다.

public class Main {
    static public void main(String[] args) {
        try {
            int[] a = {1, 2, 3};
            return;
        } catch (ArithmeticException e) {
            System.out.println("0으로 나눌 수 없습니다.");
        } catch (Exception e) {
            System.out.println(e.getClass().getName());
            System.out.println("알 수 없는 에러가 발생 했습니다.");
        } finally {
            System.out.println("Finally 구문 실행.");
        }
    }
}
Finally 구문 실행.

printStackTrace(), getMessage()

우리는 Exception 클래스에 있는 printStackTrace()getMessage()를 통해 예외 처리가 일어 나게 된 과정과, 메시지를 확인 할 수 있습니다.

  • printStackTrace(): 예외 발생 시점에 호출 스택에 있었던 메서드들의 정보 및 예외 메시지를 화면에 출력합니다.
  • getMessage(): 발생한 Exception 클래스의 인스턴스에 저장된 메시지를 얻을 수 있습니다.
public class Main {
    static public void main(String[] args) {
        try {
            throw new Exception("에러가 났지롱");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }
}
java.lang.Exception: 에러가 났지롱
	at Main.main(Main.java:4)
에러가 났지롱

Exception & RuntimeException

우리는 C++에서 했던 것 처럼, Exception을 상속 받은, 다른 예외 클래스를 만들 수 있습니다. 하지만, 우리는 용도에 따라서, 다른 Exception을 상속 해야 합니다.

  • Exception: 사용자의 실수와 같은 외적인 요인으로 발생 하는 예외, 컴파일 시점에서 예측 가능한 예외들이 이에 속합니다. (예: IOException, ClassNotFoundException)
  • RuntimeException: 프로그래머의 실수로 발생하는 예외 상황, 논리적 오류에 속하는 예외들이 이에 속합니다. (예: ArithmeticException, ClassCastException)

구조는 RuntimeExceptionException을 상속 받은 구조 입니다.

출처: https://commons.wikimedia.org/wiki/File:Java_exception_classes.svg

우리는 Exception을 상속 받아서 또 다른 Exception 클래스를 만들고, 사용자에게 원치 않는 예외 입력를 받았을 때, Exceptionthrow 할 수 있습니다. 생성자에서 super()를 이용하여, Exception 클래스의 생성자를 호출, 메시지를 생성 할 수 있습니다.

import java.util.Scanner;

class NoneInputException extends Exception {
    public NoneInputException(String s) {
        super(s);
    }
}

public class Main {
    static public void main(String[] args) {
        try {
            Scanner scanner = new Scanner(System.in);
            System.out.print("아무거나 입력 후, Enter를 눌러 주세요: ");

            String s = scanner.nextLine();

            if (s.equals("")) {
                throw new NoneInputException("아무 것도 없지롱");
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }
}
아무거나 입력 후, Enter를 눌러 주세요: 
아무 것도 없지롱
NoneInputException: 아무 것도 없지롱
	at Main.main(Main.java:18)

try-with-resource

JDK1.7 이상부터 지원하는 기능입니다. try 문 옆에 괄호로 close() 가 필요한 스트림 클래스 같은 경우에, try-catch 문이 끝나면, 스트림을 자동으로 닫아주는 기능을 수행 합니다.

test.txt

1
2
3
4
5

Main.java

import java.io.*;

public class Main {
    static public void main(String[] args) {
        try (FileReader fr = new FileReader("test.txt")) {
            while (true) {
                int temp = fr.read();
                if (temp == -1)
                    throw new EOFException();
                System.out.print((char)temp);
            }
        } catch (FileNotFoundException e) {
            System.out.println("파일이 없습니다.");
        } catch (EOFException e) {
            System.out.println("\n읽기 끝!");
        } catch (IOException ie) {
            ie.printStackTrace();
        }
    }
}
1
2
3
4
5
읽기 끝!

throws

함수에 throws 문을 입력하여, 특정 예외 클래스가 나올 것이라고 타입을 지정 해 줄 수 있습니다. 이렇게 예외 타입을 지정을 하게 되면 다음과 같은 이점을 얻을 수 있습니다.

  1. throw 되지 않는 예외 타입을 catch 하려는 경우에 컴파일 에러를 발생 시킵니다.
  2. throws로 예외 타입이 지정된 해당 함수가 try, catch 문 안에 없는 경우 에러를 발생 시킵니다. 즉, 무조건 예외 처리 하게 끔 만들 수 있습니다.
  3. 함수 내에서 발생한 예외들을 함수를 사용하는 곳으로 예외에 대한 책임을 전가 할 수 있습니다. (그래서 2번 항목이 충족이 되어야 합니다.)
  4. 협업 시 명시적으로 throw 해줘야 하는 에러를 알려주어, 의사소통을 원활하게 할 수 있습니다.
public class Main {
    static public void main(String[] args) {
        try {
            someThingForThrow();
        } catch (NullPointerException ne) {
            ne.printStackTrace();
        } catch (ArithmeticException ae) {
            ae.printStackTrace();
        }
    }

    public static void someThingForThrow() throws NullPointerException, ArithmeticException {
        double temp = Math.random();
        if (temp >= 0.5) {
            throw new NullPointerException();
        } else {
            throw new ArithmeticException();
        }
    }
}
java.lang.ArithmeticException
	at Main.someThingForThrow(Main.java:17)
	at Main.main(Main.java:4)

마치며

다음 시간에는 기본 import 되는 클래스인 java.lang 패키지에 대해서 공부 해 보는 시간을 가져 보도록 하겠습니다.