먼저 에러와 예외의 개념에 대해서 정리해야 할 것 같다. 위의 다이어그램 처럼 사실 우리는 프로그램에서 문제가 발생하면 에러가 났다고 얘기를 한다. 하지만 문제가 어떻게 발생했는지에 따라 에러와 예외로 나누어서 얘기할 수 있다. 우선 에러는 시스템에 비정상적인 상황이 발생한 것에 기인한다. 즉, 애플리케이션 레벨이 아니라 시스템 레벨이기 때문에 개발자가 미연에 방지할 수 없는 문제이다. 하지만 예외는 개발가 구현하는 로직에서 발생한다. 즉, 애플리케이션 레벨에서 미연에 방지할 수 있는 문제이다. 그래서 예외 처리를 잘하는 것이 좋은 개발자라고도 할 수 있다.
예외의 특징을 정리하면 아래와 같다.
- 개발자의 잘못된 로직
- 애플리케이션 레벨에서 발생
- 핸들링 가능
에러의 특징을 요약하면 아래와 같다.
- 컴퓨터 하드웨어 또는 JVM의 오작동이나 고장
- 시스템 레벨에서 발생
- 핸들링 불가
try {
// 예외가 발생할 가능성이 있는 코드
} catch (Exception1 e1 ) {
// Exception1 이 발생할 때 처리하는 코드
} catch (Exception2 e2 ) {
// Exception2 가 발생할 때 처리하는 코드
} finally {
// 예외가 발생하지 않아도 항상 실행
}
이 구문의 실행 절차는 아래와 같다.
- throw 는 예외를 발생시키는 키워드
- throws 는 메서드에서 발생할 1개 이상의 예외를 정의
이와 관련한 아래의 예시를 확인해보자.
String str = "A";
int num = Integer.parseInt(str);
위의 예시처럼 Integer.parseInt()에 인자로 문자열이 숫자가 아닌 경우에 예외를 발생시키며 종료된다.
Exception in thread "main" java.lang.NumberFormatException: For input string: "A"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
그래서 Integer.parseInt()의 코드를 살펴보면 아래와 같이 작성되어 있다.
public static int parseInt(String s, int radix) throws NumberFormatException
{
if (s == null) {
throw new NumberFormatException("null");
}
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException("radix " + radix +
" less than Character.MIN_RADIX");
}
....
}
여기서 메서드를 선언할 때 throws NumberFormatException 이라는 예외를 발생시킬 수 있도록 정의가 되어있다. 그리고 구현부에 throw new NumberFormatException() 이라는 throw 키워드와 NumberFormatException 예외 객체를 통해 예외를 발생시키는 것을 확인 할 수 있다.
위의 계층 구조에서 가장 먼저 확인할 수 있는 점은 예외나 에러 둘다 Throwable이라는 클래스를 상속받고 있는데 이 Throwable 클래스는 Object를 상속받고 있는 것을 확인할 수 있다. 즉, 이전의 예시에서도 확인할 수 있었지만 에러와 예외도 객체로서 활용된다는 것을 알 수 있다. 하지만 대표적인 에러에는 OutofMemoryError, ThreadDeath, StackOverflowError 등이 있지만 핸들링이 불가하기 때문에 예외 계층 구조에 대해서 더 알아보도록 하자.
첫번째 다이어그램 상의 Throwable 자식클래스들은 아래의 그림처럼 Checked Exception 과 Unchecked Exception 으로 나누어서 볼 수도 있다.
그리고 예외(Exception)의 경우 RuntimeException을 상속받지 않으면 Checked Exception이고 상속받으면 Unchecked Exception으로 분류할 수 있다. 여기서 Checked와 Unchecked는 예외가 확인되는 시점이 컴파일 단계인지 런타임인지로 구분되는 것을 명확하게 알 수 있다.
그럼 Unchecked Exception인 RuntimeException과 Checked Exception인 나머지 예외들의 차이는 아래와 같다.
Checked Exception의 경우 try-catch 및 throw를 통해 Unchecked Exception은 확인되는 시점이 런타임인 이유는 프로그램 구조 상의 문제가 아닌 실행 중 개별 코드의 문제이기 때문에 컴파일 단계에서 확인하지 못하는 문제이다.
이런 RuntimeException을 상속받는 예외들은 아래와 같고 대표적인 것은 NullPointerException과 ArithmeticException이다.
지금까지 메서드에서 예외를 발생시키는 법과 예외의 구조를 알았으면 사용자 정의 예외를 만드는 법을 알 차례이다. 우선 아래 개념을 다시 확인하고시작하자.
- Throwable을 직접 상속 받는 클래스는 Exception과 Error가 있다
- Error는 개발자 영역 X
- Exception에는 CheckedException과 Unchecked Exception이 있다.
사용자 정의 예외를 만드는 것은 예외를 상속하는 것에서 시작한다. 일반적인 경우는 그냥 Exception을 상속받지만 어떻게 처리할지의 Checked 또는 Unchecked로 예외를 처리할 전략에 따라 상속하는 예외 클래스를 선택할 수 있다.
class CustomException extends Exception{
// 문자열을 매개로 받는 생성자
CustomException(String msg){
// 부모 클래스인 Exception 클래스 생성자로 인자 전달
super(msg);
}
}
위의 예시가 기본적인 형태의 예외이고 필드 변수가 메서드를 추가하여 활용할 수도 있다. 마지막으로 해당 사용자 정의 예외를 활용하는 건 아래와 같이 할 수 있다.
class Main {
public static void main(String args[]){
func(5);
}
// 사용자 정의 예외를 발생시키는 메서드
static void func(int num) throws CustomException {
if( num < 5 ) {
throw new CustomException("예외 발생");
}
}
}