Java
에는 에러(Error
)와 예외(Exception
)이라는 개념이 존재한다.
Java
에는 Throwable
이라는 클래스가 존재한다.
해당 그림을 보면 크게 Error
와 Exception
을 상속하는 클래스들로 구성되어 있다.
여기서 Error
를 상속하는 클래스들은 코드의 문제가 아닌 하드웨어 장비나 가상머신, 메모리와 같은 외적인 문제이고 Exception
을 상속하고 있는 클래스들이 애플리케이션 코드에서 발생하는 에러이다.
Error
가 발생하면 코드레벨에서 뭘 할 수 있을까?위의 그림을 보면RuntimeException
을 상속하는 클래스가 있고, 상속하지 않는 클래스가 있다.
RuntimeExcepiton
을 상속하는 예외는 UnchekdException
이라고 부르고, 상속하지 않는 예외는 CheckedException
이라고 부른다.
그렇다면 두 Exception
의 차이는 뭘까?
RuntimeException
이라는 이름의 클래스를 기준으로 구분을 하기 때문에 예외가 발생하는 시점에 대한 차이로 착각하는 경우가 많다.
실제로 블로그를 찾다보면 발생 시점을 컴파일 시점과 런타임 시점이라고 나누어 설명하는 경우가 많은데, 정확히는 try-catch
t를 통한 예외처리
의 강제성에 차이가 있다.
두 예외 모두 런타임 시점에 발생하지만, CheckedException
의 경우 컴파일러가 예외처리를 확인 하는 과정에서 코드가 실행되지 않기 때문에 컴파일 환경에서 발생하는 것으로 보이는 것이다.
매우 중요하니 반드시 명심해야한다.
RuntimeException
이란 말 그대로 런타임 환경에서 발생하는 Exception
이다.
컴파일 과정에서는 해당 Exception
을 인지할 수 없고, 코드가 런타임 환경에서 실행되는 과정에서 Exception
이 발생한다.
자바 동작 구조상 .java
파일이 자바 컴파일러를 통해 .class
파일로 변환되고, 클래스 로더가 바이트 코드인 .class
파일을 JVM
에 올려야 Java Interpreter
나 JIT Compiler
에 의해 자바 런타임이 동작한다.
Exception
에 대한 체크를 하지 않아도 애플리케이션 코드를 실행시킬 수 있는 Exception
이기 때문에 UncheckedException
이라고 한다.
public class UncheckedExceptionSample {
public static void main(String[] args) {
int[] arr = new int[10];
arr[10] = 10;
}
}
예를 들어 위와 같은 코드가 있다고 하자 프로그래밍을 처음 시작하면 자주 접하게 되는 예외중에 IndexOutOfBoundException
이라는 예외가 있다.
해당 코드는 컴파일 후 실행까지의 동작이 정상적으로 수행된다.
하지만 배열의 크기가 10으로 0~9까지의 인덱스를 가지게 되는데, 10이라는 범위 밖의 인덱스를 조회하고 있으므로 예외가 발생한다.
RuntimeException
이 런타임 환경에서 발생하는 예외라면 Exception
은 런타임 시점에 발생하지 않을까?
위에서 말한 것 처럼 Exception
역시 런타임 시점에 발생하는 예외이며, CheckedException
과의 차이는 try-catch
문을 통한 예외처리가 강제된다는 것이다.
반드시 해당 코드에 대한 예외처리 즉 체크를 해주어야 하기 때문에 CheckedExcption
이라고 부르는 것이다.
CheckedException
은 주로 자바 설계에 의한 문제인 경우가 많다.
CheckedException
의 예를 들어보자.
대표적인 예로 FileNotFoundException
이 있다.
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class CheckedExceptionSample {
public static void main(String[] args) {
try {
//파일 객체 생성
File file = new File("D:/Sample.txt");
//입력 스트림 생성
FileReader file_reader = new FileReader(file);
int cur = 0;
while ((cur = file_reader.read()) != -1) {
System.out.print((char) cur);
}
file_reader.close();
} catch (FileNotFoundException e) {
e.getStackTrace();
} catch (IOException e) {
e.getStackTrace();
}
}
}
위의 코드를 보면 try-catch
문으로 감싸져 있는 코드들이 있다.
CheckedException
은 이렇게 try-catch
를 통해 예외가 발생할 수 있는 부분을 강제로 예외처리 해 주어야 한다.
위에서 설명한 것 처럼 CheckedException
에 대해서는 반드시 예외처리가 필요하다.
그렇다면 예외처리 방법에는 어떤 것이 있을까?
위의 FileNotFoundException
에 대한 코드를 보면서 설명하겠다.
먼저 try-catch
문을 통한 예외처리가 가능하다.
try {
// 비즈니스 로직
} catch (Exception명) {
// 해당 Exception이 발생했을 때 동작 구현
} finally {
// 반드시 수행할 동작 구현
}
try
내부에 비즈니스 로직을 구현하고, catch
에 발생할 수 있는 예외를 정의하고 예외 발생시 어떤 동작을 할지 구현한다.
그리고, 만약 예외가 발생하던 하지 않던 반드시 수행해야 하는 동작들 예를들어 DB 커넥션을 해제하는 등의 동작은 finally
에 정의한다.
그런데 위의 try-catch
문으로 감싼 코드를 보면 의문이 생긴다.
try-catch
문으로 감쌌다는 것은 해당 코드들이 CheckedException
을 발생시킨다는 의미인데 어떻게 예외가 발생할것을 알 수 있을까?
그건 try-catch
문을 지워보면 알 수 있다.
위의 코드에서 try-catch
문을 지웠더니 코드에 문제가 있다는 빨간색 밑줄이 생겼다.
그럼 14번째 줄의 FileReader
라는 구현체에 어떤 문제가 있는지 확인해 보겠다.
위의 그림은 FileReader
의 생성자중 하나이다.
그런데 생성자 선언문의 파라미터
와 {
사이에 throws
라는 예약어가 있고 그 옆에 FileNotFoundException
이라는 단어가 적혀 있다.
Java
에서는 이것을 예외를 던진다
라고 한다.
이런 CheckedException
들은 반드시 어디에선가 해결을 해 주어야 한다.
throws
는 이 예외가 발생한 객체가 아닌 자신을 호출한 대상이 예외를 처리하도록 떠넘기는 것과 같다.
위의 코드에서 main
에서 FileReader
의 생성자를 호출했다. FileReader
에 throws
예약어가 붙어 있으니 예외가 발생하면 FileReader
의 생성자는 main
으로 예외를 던지게된다.
이제 예외 처리에 대한 책임이 main
으로 넘어갔기 때문에 main
에서 try-catch
를 통한 예외처리를 해주어야 한다.
그렇다면 FileNotFoundException
의 예외를 처리해보자.
try-catch
문을 이용해 FileNotFoundException
에 대한 예외를 처리해 주었다.
그런데 FileReader
구현체의 read()
와 close()
메소드를 사용하는 부분에도 문제가 있다고 나온다...
이번에는 이 두 메소드의 형태를 살펴보자.
이 글만 보지 말고 반드시 직접 해당 구현체와 메소드를 확인해 보는 것이 좋다.
read()
와 close()
메소드는 FileNotFoundException
이 아닌 IOException
예외를 던지고 있기 때문에 문제가 발생한 것이었다.
그럼 IOException
에 대한 예외도 처리해 주겠다.
문제가 해결되었다!
그런데 만약 여기서 FileNotFoundException
의 catch
문을 제거하면 어떻게 될까?
문제가 발생하지 않는다.
어째서 문제가 발생하지 않는걸까?
맨 위의 다이어그램을 유심히 봤다면 눈치챘을수도 있지만, FileNotFoundException
이 IOException
을 상속한 클래스이기 때문이다.
상속에 대한 개념을 잘 알고 있는 사람이라면, IOException
뿐만 아니라 Exception
이나 Throwable
로 처리해도 된다는 사실을 깨닫게 될 것이다.
그렇다면 모든 클래스의 최상위 클래스인 Object
클래스를 통해서도 될까?
catch
의 파라미터 값이 Throwable
을 상속한 클래스로 제한되므로 안된다.
이에 대해서는 상속과 업캐스팅
다운캐스팅
에 대해서 알아보면 좋을 것 같다.
이제 CheckedExcpetion
은 해당 예외가 발생할 수 있는 객체들이 throws
를 통해 예외를 던져주기 때문에 반드시 처리해야 된다는 사실을 알게 되었고, try-catch
를 통해 예외를 전달받은 main
에서 예외처리도 해주었다.
그런데 main
마저 예외처리를 하기 싫다면 어떻게 하면 될까?
간단하게 main
메소드 옆에서 throws
를 통해 예외를 던져주면 된다.
이렇게하면 프로그램에서 가장 최상단에 위치한 main
마저 예외를 처리하지 않는다.
하지만, 이렇게 하면 어떤 에러가 어디서 발생했는지 식별하는데 어려움이 있고 예외가 발생하면 서로 떠밀다가 프로그램이 죽어버리기 때문에 권장하지 않는다...