프로그램 실행 시에 프로그램이 normal flow를 따라가지 않는 경우 프로그램이 종료되는 상황이 있다. 이를 프로그램 오류가 발생했다고 한다.
오류는 컴파일 에러와 런타임 에러 2가지로 크게 나뉜다. 여기서 자바는 런타임 에러를 Error와 Exception, 두 가지로 나뉜다.
에러는 자바에서 객체로 표현이 되고 예측 불가능하며 보통 복구 불가능한 심각한 상황을 나타낸다. 메모리가 부족할 경우 OutOfMemoryError가 발생하거나 호출이 깊어져 생기는 StackOverFlowError가 대표적인 예시다.
예외는 예측 가능한 상황에서 오류를 제어할 수 있는 것을 말한다. 예외는 개발자의 로직 실수에 발생하고 예측 가능하므로 개발자는 예외 상황에 따른 핸들링을 해줘야 한다.
int a = 5;
int b = 0;
System.out.println(a/b);
예외를 설명할 때 가장 대표적으로 쓰이는 예시는 어떤 정수를 0으로 나누는 사례다. 해당 경우처럼 0으로 나누면 에러가 생긴다는 예측 가능한 상황에서 핸들링이 가능한 것이 예외다.
예외는 checked이거나 unchecked다. checked exception
은 컴파일 단계에서 발견되는 예외를 뜻한다. 컴파일 시에 프로그램의 어떤 메서드의 코드가 checked exception을 throw하는 경우, 코드 상에 예외를 처리하거나 throws 키워드를 사용해 예외를 지정해주어야 한다. unchecked exception
은 프로그램 실행 중에 발생하는 예외다. 둘의 관계는 위에서 말한 컴파일 에러와 런타임 에러의 관계와 유사하다.
자바에서 예외 핸들링의 첫 번째 키워드는 try-catch다.
try{
} catch(FileNotFoundException e){
} catch(IOException e){
} catch(Exception e){
}
try-catch 구문은 위 형식으로 예외를 핸들링한다.
try 블록에는 예외가 발생될 만한 코드가 들어간다.
catch 블록은 예외가 발생했을 때 처리할 동작을 정의한다. 위 코드처럼 catch 블록은 여러 개가 가능하다. 맨 처음 catch에 잡히지 않았다면 다음 catch를 검사하는 방식으로 진행된다. 이 때, 예외 간의 상속 관계가 존재한다면 자식 예외 클래스는 부모 예외 클래스의 위에 존재해야 한다.
Exception class는 IOException class의 부모이기 때문에 Exception catch 블록 이후에 IOException catch 블록이 놓일 수 없다. IOException에 도달할 수 없기 때문이다.
try {
int a = 10;
int b = 0;
System.out.println(a/b);
} catch (Exception e){
System.out.println("Unusual input");
System.out.println(e);
}
System.out.println("exception test");
Output
Unusual input
java.lang.ArithmeticException: / by zero
exception test
0으로 나눌 경우를 try-catch 구문으로 예외 핸들링을 했다. try 블록에서 Exception을 잡아내 catch 블록의 동작을 수행한다. 마지막 문장이 출력된 것으로 보아 try에서 예외가 잡히더라도 프로그램이 종료되지는 않는다.
throw 키워드는 exception을 명시적으로 던지는 데에 쓰인다. 주로 exception을 custom해 던지기 위함이다.
try{
int age = 16;
if(age<18) throw new Exception("!!!under 18");
System.out.println("Valid age");
} catch(Exception e){
System.out.println(e);
System.out.println("Not valid age");
}
Output
java.lang.Exception: !!!under 18
Not valid age
throw는 보통 예외가 던져지는지 확인하기 위해 if문 내에 사용한다.
어떤 메서드가 예외를 발생시킬 수 있다면, throws를 이용해 코드를 간결하게 적을 수 있다. 만약 예외 상황이 많은 경우, 그것에 대해 하나하나 try-catch를 작성해야 하는데 코드가 지루하고 매우 길어진다.
예외를 던진다는 것은 예외를 메서드에서 직접 처리하지 않고 메서드를 호출하는 녀석에게 예외 처리를 전가함을 의미한다.
public void divide(int a, int b) throws ArithmeticException {
if(b==0) throw new ArithmeticException("/ zero");
System.out.println(a/b);
}
public static void main(String[] args) {
int a = 10;
int b = 0;
divide(a, b);
}
divide()는 b가 0인 상황에서 예외가 발생할 수 있음을 예측하게 되고, try-catch로 이 예외를 처리해줘야 한다. 그러나 throws는 이 예외를 main()에서 처리하도록 한다. 이렇게 예외 처리를 호출 메서드에 전가하는 이유는 divide()가 예외가 발생한 이후의 처리를 divide()가 정하지 않기 때문이다. divide()를 호출한 main()은 예외가 발생한 후, 메서드를 재호출하거나 프로그램을 종료시키거나 b의 값을 다시 받아야하는 등의 이후 처리가 필요한데 divide()는 거기까지 관여할 수 없다.
예외를 전가 받은 main()은 다음과 같이 수정되어야 한다.
public static void main(String[] args) {
int a = 10;
int b = 0;
try {
divide(a, b);
} catch(ArithmeticException e){
e.getMessage();
}
}
위 코드는 전달받은 예외를 catch한 방식이다.
물론 전가받은 예외를 다시 누군가에게 전가할 수도 있다. 이를 exception propagaion이라 하는데 메서드의 호출이 계층적으로 일어날 경우 사용한다.
class Final {
void display() throws Exception {
int a=10;
int b=0;
int result=a/b;
}
void output() throws Exception {
display();
}
void finalDisplay() {
try{
output();
}
catch(Exception e){
System.out.println("Input the correct number");
}
}
}