Java 프로그래밍 언어는 오류와 그 밖의 예외적인 상황을 처리하기 위해 예외를 사용해요.
예외는 프로그램을 실행하는 도중에 발생해서 정상적인 명령 흐름을 방해하는 사건이에요. Java에서는 예외를 명시적으로 던져요. 이렇게 예외를 던지는 행위를 _예외 던지기_라고 하고, 예외를 처리하는 행위를 _예외 잡기_라고 해요.
Java에서 모든 예외는 Exception 클래스의 하위 클래스이고, Exception은 다시 Throwable의 하위 클래스예요.
Java는 예외를 두 가지로 구분해요.
_검사 예외_는 애플리케이션이 미리 예상하고 복구해야 하는 예외적인 상황이에요.
검사 예외의 예로는, 존재하지 않는 파일을 읽으려고 할 때 발생하는 FileNotFoundException이 있어요.
이런 예외는 컴파일 시점에 검사돼요. 검사 예외를 던지는 메서드는 메서드 시그니처에 이를 명시해야 하고, 검사 예외를 던질 수 있는 메서드를 호출하는 코드는 그 예외를 처리해야 하며, 그렇지 않으면 컴파일되지 않아요.
모든 검사 예외는 RuntimeException을 상속하지 않는 Exception의 하위 클래스예요.
_비검사 예외_는 애플리케이션이 보통 예상하거나 복구할 수 없는 예외적인 상황이에요.
비검사 예외의 예로는, null이 아닌 값을 기대하는 메서드가 null을 받을 때 발생하는 NullPointerException이 있어요.
이런 예외는 컴파일 시점에 검사되지 않아요. 비검사 예외를 던지는 메서드는 메서드 시그니처에 이를 명시할 필요가 없고, 비검사 예외를 던질 수 있는 메서드를 호출하는 코드도 그 예외를 처리할 필요가 없어요.
모든 비검사 예외는 RuntimeException을 상속받고, RuntimeException은 Exception을 확장한 것이에요.
Java에서 메서드는 throw문을 사용해서 예외를 던질 수 있어요.
메서드에서 검사 예외를 던질 때는 아래 예시처럼 throws 키워드를 사용해서 메서드 시그니처에 이를 명시해야 해요.
그러면 이 메서드를 호출하는 코드는 예외가 던져질 수 있다는 것을 미리 예상하고 그에 맞게 처리해야 해요.
public class InsufficientBalanceException extends Exception {
}
public class BankAccount {
public void withdraw(double amount) throws InsufficientBalanceException {
if (balance < amount) {
throw new InsufficientBalanceException();
}
// rest of the method implementation
}
}
메서드에서 비검사 예외를 던질 때는 메서드 시그니처에 이를 명시할 필요가 없어요. 물론 명시할 수도 있어요.
public class BankAccount {
public void withdraw(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Cannot withdraw a negative amount");
}
// rest of the method implementation
}
}
Java에서 예외 처리는 try, catch, finally 키워드로 해요.
try 블록으로 감싸야 해요.try 블록 뒤에는 try 블록에서 던진 예외를 잡는 하나 이상의 catch 블록이 와요.catch 블록 뒤에는 선택적으로 finally 블록이 올 수 있는데, 이 블록은 예외가 던져졌는지와 상관없이 try 블록 다음에 항상 실행돼요.다음 예시는 이 키워드들이 어떻게 동작하는지 보여줘요.
public class ATM {
public void withdraw(BankAccount bankAccount, double amount) {
try {
System.out.println("Withdrawing " + amount);
bankAccount.withdraw(amount);
System.out.println("Withdrawal succeeded");
} catch (InsufficientBalanceException) {
System.out.println("Withdrawal failed: insufficient balance");
} catch (RuntimeException e) {
System.out.println("Withdrawal failed: " + e.getMessage());
} finally {
System.out.println("Current balance: " + bankAccount.getBalance());
}
}
}
이 예시에서 예외가 던져지지 않으면 다음이 출력돼요.
Withdrawing 10.0
Withdrawal succeeded
Current balance: 5.0
반대로 bankAccount.withdraw(amount) 문이 InsufficientBalanceException을 던지면 다음이 출력돼요.
Withdrawing 10.0
Withdrawal failed: insufficient balance
Current balance: 5.0
또는 bankAccount.withdraw(amount)에서 비검사 예외가 던져지면 다음이 출력돼요.
Withdrawing -10.0
Withdrawal failed: Cannot withdraw a negative amount
Current balance: 5.0
Java에는 _오류_라는 별도의 범주도 있는데, 이는 애플리케이션 외부에서 생기는 심각한 문제예요.
오류의 예로는, 애플리케이션이 시스템에서 사용할 수 있는 것보다 더 많은 메모리를 사용하려고 할 때 발생하는 OutOfMemoryError가 있어요.
비검사 예외와 마찬가지로 오류도 컴파일 시점에 검사되지 않아요. 차이라면 오류는 시스템 수준의 문제를 나타내며, 보통 애플리케이션이 아니라 Java 가상 머신이나 환경에서 던져져요. 애플리케이션은 보통 이런 오류를 잡거나 처리하려고 해서는 안 돼요.
Java의 모든 오류는 Error 클래스를 상속받아요.
앞서 말했듯이 예외는 정상적인 명령 흐름을 방해하는 사건이고, _예외적인 상황_을 처리하는 데 사용해요. 따라서 애플리케이션에서 흐름 제어를 위해 예외를 사용하는 것은 권장되지 않아요.