Java Exception

Seoyeon Kim·2023년 3월 14일
0

Error vs Exception

Error

개발자가 처리할 수 없는 오류

예시

  1. StackOverFlowError
  2. OutOfMemoryError

Exception

개발자가 처리할 수 있는 오류

예시

  1. NullPointerException
  2. IllegalArgumentException

Checked Exception

compile 전에 확인할 수 있는 에러를 말한다.
반드시 예외 처리를 해야 한다.

Unchecked Exception = Runtime Exception

runtime 시 발생하는 에러를 말한다.
예외 발생 시 rollback 처리를 해야 한다.

Exception Class

Throwable 클래스를 상속받은 Exception 클래스는 다시 일반 예외 클래스와 실행 예외 클래스로 나뉜다.

Exception 클래스에게서 직접 상속받은 예외 클래스들이 처리하는 일반 예외는 컴파일 전에 예외 발생 문법을 검사하며, 예외 처리를 하지 않으면 문법 오류가 발생한다.

여기서 '검사'는 예외의 실제 발생 여부를 검사하는 것이 아니라 예외가 발생할 수 있는 문법을 사용했는지를 검사하는 것을 의미한다.

반면 RuntimeException 클래스를 상속받은 예외 클래스들이 처리하는 실행 예외는 컴파일 전이 아니라 실행할 때 발생하는 예외로, 예외 처리를 따로 하지 않더라도 문법 오류가 발생하지 않는다. 다만 프로그램 실행 시 프로그램이 강제 종료되는 이유는 대부분 실행 예외 때문이므로 주의가 필요하다.

일반 예외 클래스

일반 예외는 예외 처리를 하지 않으면 문법 오류를 발생시켜 compile 자체가 불가능하다. 일반 예외 자체는 문법으로 예외 처리를 요구한다.

대표적인 일반 예외는 아래와 같다.

InterruptedException

Thread.sleep() 은 일정 시간 동안 해당 thread를 일시정지 상태로 만드는 Thread 클래스의 정적 메서드다. 이 메서드는 일반 예외가 발생할 수 있기 때문에 반드시 예외 처리를 해야 한다. 이를 생략하면 문법 오류가 발생해 compile 자체가 불가능하다.

ClassNotFoundException

Class.forName() 은 클래스를 동적으로 메모리에 로딩하는 메서드로 해당 클래스의 정보를 담고 있는 Class 타입의 객체를 반환한다. 만일 클래스를 메모리에 동적으로 로딩하는 과정에서 해당 클래스가 존재하지 않을 때는 ClassNotFoundException이 발생하므로 이에 대한 예외 처리를 반드시 포함해야 한다.

실제 클래스가 존재한다 하더라도 예외 처리를 하지 않으면 문법 오류가 발생한다. 즉, 실제 클래스의 존재 유무와 상관 없이 예외가 발생할 수 있는 코드인지가 중요하다.

IOException

입출력 부분에서 발생하는 일반 예외로, 콘솔이나 파일에 데이터를 쓰거나 읽을 때 발생하며 반드시 IOException에 대한 예외 처리를 해야 한다.

FileNotFoundException

파일을 읽을 때 해당 경로에 파일이 없으면 FileNotFoundException이 발생한다. 이 또한 실제 파일의 존재 유무와는 상관없이 파일이 존재하지 않을 가능성이 있는 코드이기 때문에 반드시 예외 처리를 해야 문법 오류가 발생하지 않는다.

CloneNotSupportedException

자바의 모든 클래스는 Object 클래스를 상속한다. 이는 Object의 모든 메서드를 사용할 수 있다는 뜻이다. Object 클래스의 메서드 중 clone() 은 자신의 객체를 복사한 클론 객체를 생성해 리턴하는 메서드다. 다만 이를 위해 복사의 대상이 되는 클래스는 반드시 Cloneable 인터페이스를 상속해야 한다. 쉽게 말해, 해당 클래스가 복사 기능을 제공해야 한다는 것이다. 만약 Cloneable 인터페이스를 상속하지 않은 클래스의 객체를 복사하기 위해 clone()을 호출하면 CloneNotSupportedException이 발생한다.

실행 예외 클래스

일반 예외와 다르게 실행 예외는 문법 오류가 발생하지 않는다. 그렇기 때문에 예외 처리 없이 compile과 실행이 가능하지만, 실행 중 실행 예외가 발생하면 프로그램은 강제 종료된다. 실행 예외를 처리하는 클래스는 Exception의 자식 클래스인 Runtime Exception 클래스의 자식 클래스들이다.

대표적인 실행 예외는 아래와 같다.

ArithmeticException

Arithmetic의 사전적인 뜻은 '산술' 또는 '연산'이다. 즉 ArithmeticException은 연산 자체가 불가능할 때 발생하는 실행 예외이다. 수학식에서 절대 존재할 수 없는 대표적인 연산은 분모가 0일 때다. 이 연산을 수행하도록 하면 예외가 발생하는 것이다. 이때 예외 처리를 하지 않으면 예외가 발생했을 때 실행 중인 프로그램은 예외 발생 상황을 출력하고 강제 종료된다.

ClassCastException

상속 관계에 있는 클래스 간의 업캐스팅은 항상 가능하지만, 다운캐스팅은 가능할 수도 불가능할 수도 있다. ClassCastException은 다운캐스팅이 불가능한 상황에서 다운캐스팅을 시도할 때 발생한다.

ArrayIndexOutOfBoundException

배열의 인덱스를 잘못 사용했을 때 발생한다. 배열의 인덱스는 항상 0 ~ (배열의 길이 - 1) 까지의 값만 사용할 수 있다. 만일 이 범위 밖에 있는 인덱스를 사용하면 이 예외가 발생한다.

NumberFormatException

문자열을 정수로 변환하고자 할 때는 Integer.parseInt("문자열") 실수로 변환하고자 할 때는 Double.parseDouble("문자열") 을 사용한다. 이렇게 문자열을 숫자 또는 실수로 변환할 때 문자열이 변환하고자 하는 숫자의 형식과 다르면 변환이 실패하는데 이때 발생하는 실행 예외가 NumberFormatException이다.

NullPointerException

참조 변수가 실제 객체를 가리키고 있지 않은 상황에서 필드나 메서드를 호출할 때 발생한다. 여기서 null 은 위치를 저장하는 참조 변수의 초깃값으로 사용할 수 있으며, 현재 pointing 하고 있는 객체가 없다는 것을 의미한다. 객체를 가리키고 있지 않은데 해당 위체에 가서 객체 안에 있는 멤버를 실행하라고 명령하니 수행할 수 없는 것이다.

예외 처리

예외 처리 문법 구조

try {
	// 일반 예외 또는 실행 예외가 발생 가능한 코드
} catch (예외 클래스명 참조 변수명) {
	// 예외가 발생했을 때 처리
} finally { // 생략 가능
	// 예외 발생 여부에 상관없이 무조건 실행
}

예외 처리 구문이 있으면 JVM은 적절히 예외가 처리됐다고 판단하기 때문에 프로그램을 강제 종료하지 않는다. 심지어 예외처리 구문 내에 아무런 코드를 작성하지 않아도 예외가 처리된 것으로 간주한다.

다중 예외 처리

try {

} catch (예외 타입 e1) {

} catch (예외 타입 e2) {

}
...

finally { // 생략 가능

}

다중 예외 처리 구문을 작성할 때 반드시 주의할 점은 try block에서 예외가 발생하고 여러 개의 catch block이 있을 때 실행할 catch block의 선택 과정은 항상 위에서부터 확인한다는 것이다. 이는 마치 if-else if-else 구문에서 조건식을 위에서부터 검사하는 것과 같다.

리소스 자동 해제 예외 처리

finally 는 '항상 실행해야 하는 기능이 있을 때 사용하는' block이다. 가장 대표적인 기능은 리소스를 해제하는 것이다. 리소스 해제는 더이상 사용하지 않는 자원을 반납하는 것을 의미한다. 예를 들어 수정하기 위해 열어 둔 파일이 있다면 이 파일을 닫아야 다른 프로그램이 이 파일을 사용할 수 있다. 메모리에 엄청난 크기의 객체를 만들어 놓고 사용했을 때도 사용이 완료되면 메모리 공간 확보를 위해 리소스를 해제해야 할 것이다.

try (리소스 자동 해제가 필요한 객체 생성) {
	// 예외가 발생할 수 있는 코드
} catch (예외 클래스명 참조 변수명) {
	// 해당 예외가 발생했을 때 처리하는 코드
} finally {
	// 예외 발생 여부에 상관없이 무조건 실행하는 코드
}

기존 예외 처리 구문과의 유일한 차이점은 try 구문에도 소괄호가 포함된다는 것이다. 이 소괄호 안에서 자동으로 리소스를 반납해야 할 객체를 생성하면 예외 처리 구문의 실행이 끝났을 때 리소스가 자동으로 해제된다.

모든 객체를 try 구문의 소괄호에 넣을 수는 없다. AutoCloseable 인터페이스를 구현해 내부에 close() 메서드를 포함하고 있는 클래스의 객체에 한해서만 리소스 자동 해제 기능을 제공한다.

예외 전가

예외가 발생했을 때 바로 처리할 수도 있지만, 자신을 호출한 지점으로 예외를 전가할 수 있다. 예외를 전가하면 예외 처리의 의무를 호출한 메서드가 갖게 된다.

만약 상위 메서드들이 예외를 직접 처리하지 않고 계속 전가하다가 올라간 main 마저 예외를 전가하면 이 main을 실행한 JVM이 직접 예외를 처리하게 된다.

JVM의 예외 처리 방식은 매우 간단하다. 발생한 예외의 정보를 화면에 출력하고 프로그램을 강제 종료하는 것이다. 우리가 예외를 처리하지 않으면 그동안 JVM이 혼자서 이런 식으로 예외를 처리해 왔던 것이다.

사용자 정의 예외 클래스

Java가 모든 예외 클래스를 제공하는 것은 불가능하다.
대신 필요한 상황에 따라 우리가 직접 예외 클래스를 정의해 사용할 수 있다.

사용자 예외 클래스를 정의하는 방법은 Java에서 제공하는 예외 클래스와 마찬가지로 Exception을 바로 상속해 일반 예외 클래스로 만드는 방법과 RuntimeException을 상속해 실행 예외 클래스로 만드는 방법으로 나눌 수 있다.

1단계 : 사용자 정의 예외 클래스 작성

class MyException extends Exception { // 일반 예외 상속
	MyException() { }
    MyException(String s) { // 실행 메시지
    	super(s); // 부모 생성자 호출
    }
}
class MyRunException extends RuntimeException { // 실행 예외 상속
	MyRunException() { }
    MyRunException(String s) { // 실행 메시지
    	super(s); // 부모 생성자 호출
    }
}

2단계 : 사용자 정의 예외 클래스 객체 생성

MyException me1 = new MyException(); // 일반 예외 객체
MyException me2 = new MyException("예외 메시지");
MyRunException mre1 = new MyRunException(); // 실행 예외 객체
MyRunException mre2 = new MyRunException("예외 메시지");

3단계 : 예외 상황에서 예외 객체 던지기

throw me1;
throw me2;
throw new MyException();
throw new MyException("예외 메시지");
throw mre1;
throw mre2;
throw new MyRunException();
throw new MyRunException("예외 메시지");

예외를 '던진다'는 것은 '실제 자바 가상 머신에게 예외 객체를 만들어 전달한다.'는 의미다. 예외 객체를 던지면 곧바로 예외가 발생한다. 그러면 JVM은 그 예외를 처리할 수 있는 cath block에게 받았던 예외를 전달할 것이다.

예외를 던지는 throw 와 예외를 전가하는 throws 랑 혼동할 수 있으므로 주의한다.

예외 클래스의 메서드

getMessage()

예외가 발생했을 때 생성자로 넘긴 메시지를 문자열 형태로 반환하는 메서드로 원형은 다음과 같다.

public String getMessage()

객체를 생성했을 때 전달된 메시지를 반환하는 메서드이므로 객체를 기본 생성자로 생성하면 null 을 반환한다.

printStackTrace()

예외 발생이 전달되는 경로, 즉 예외가 전가된 과정을 한눈에 확인할 수 있는 메서드로 원형은 다음과 같다.

public void printStackTrace()

아래 클래스 A에는 3개의 메서드가 있다. a() → b() → c() 순으로 순차적으로 호출하고 있으며 모든 예외는 상위 메서드로 전가하고 있다.

class A {
	void a() throws NumberFormatException {
    	b();
    }
    
    void b() throws NumberFormatException {
    	c();
    }
    
    void c() throws NumberFormatException {
    	int num = Integer.parseInt("10A");
    }

public static void main(String[] args) {
	A a = new A();
    try {
    	a.a();
    } catch (NumberFormatException e) {
    	e.printStackTrace();
    }
}

main()에서 클래스 A의 객체를 생성하고 a() 메서드를 호출하면 아래와 같은 경로가 출력될 것이다.

java.lang.NumberFormatException: For input String: 10A
	at java.lang.Integer.parseInt(Unknown Source)
    at pack01.EX01_ExceptionMethod.A.c(...)
    at pack01.EX01_ExceptionMethod.A.b(...)
    at pack01.EX01_ExceptionMethod.A.a(...)
  at pack01.EX01_ExceptionMethod.ExceptionMethodTest.main(...)

최초로 예외가 발생한 위치에서 해당 예외가 전달된 경로를 출력한다. 즉 이때는 Integer.parseInt() → c() → b() → a() → main() 순으로 출력된다.

엄밀히 말하면 두 메서드는 Exception 클래스의 부모 클래스인 Throwable 클래스의 메서드다. 즉, 모든 Exception 클래스와 Error 클래스 내에서 사용할 수 있는 메서드라는 뜻이다.

참고 자바 완전 정복 457 page ~

4개의 댓글

comment-user-thumbnail
2023년 3월 15일

오 자바 완전정복하셨나요?

1개의 답글
comment-user-thumbnail
2023년 3월 17일

와 미쳤다.. 오브젝트 때부터 정리 엄청 열심히 하시네요..!! 😲

1개의 답글