자바 예외 처리 마스터하기: 예상치 못한 오류 우아하게 다루기
예외(또는 예외적 이벤트)는 프로그램 실행 중에 발생하는 문제입니다. 예외(Exception)가 발생하면 프로그램의 정상적인 흐름이 중단되고 프로그램이 비정상적으로 종료되는데, 이는 권장되지 않으므로 이러한 예외들을 처리해야 합니다.
예외는 다양한 이유로 발생할 수 있습니다. 예외가 발생하는 몇 가지 시나리오:
- 사용자가 잘못된 데이터를 입력함
- 열어야 할 파일을 찾을 수 없음
- 통신 중 네트워크 연결이 끊기거나 JVM 메모리가 부족함
이 중 일부는 사용자 실수, 일부는 프로그래머 실수, 일부는 물리적 자원 고장으로 인한 것입니다.
이를 바탕으로 예외는 세 가지 범주로 나뉩니다. 자바에서 예외 처리가 어떻게 작동하는지 이해하려면 이들을 알아야 합니다.
예외의 세 가지 범주
1. 체크 예외(Checked Exceptions)
체크 예외는 컴파일러가 컴파일 시점에 체크(알림)하는 예외로, 컴파일 타임 예외라고도 합니다. 이 예외들은 단순히 무시할 수 없으며, 프로그래머가 반드시 처리해야 합니다.
예를 들어, 파일에서 데이터를 읽기 위해 FileReader 클래스를 사용하는데 생성자에 지정된 파일이 존재하지 않으면 FileNotFoundException이 발생하고, 컴파일러는 프로그래머에게 예외 처리를 요구합니다.
예제
import java.io.File;
import java.io.FileReader;
public class FilenotFound_Demo {
public static void main(String args[]) {
File file = new File("E://file.txt");
FileReader fr = new FileReader(file);
}
}
위 프로그램을 컴파일하면 다음 예외가 발생합니다.
출력
C:\>javac FilenotFound_Demo.java
FilenotFound_Demo.java:8: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader fr = new FileReader(file);
^
1 error
참고 - FileReader 클래스의 read()와 close() 메서드가 IOException을 던지므로, 컴파일러는 FileNotFoundException과 함께 IOException 처리도 요구합니다.
2. 언체크 예외(Unchecked Exceptions)
언체크 예외는 실행 시점에 발생하는 예외로, 런타임 예외(Runtime Exceptions)라고도 합니다. 로직 오류나 API 부적절한 사용 같은 프로그래밍 버그가 포함됩니다. 런타임 예외는 컴파일 시 무시됩니다.
예를 들어, 크기 5의 배열을 선언하고 6번째 요소를 접근하려 하면 ArrayIndexOutOfBoundsException이 발생합니다.
예제
public class Unchecked_Demo {
public static void main(String args[]) {
int num[] = {1, 2, 3, 4};
System.out.println(num[5]);
}
}
컴파일하고 실행하면 다음 예외가 발생합니다.
출력
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at Exceptions.Unchecked_Demo.main(Unchecked_Demo.java:8)
3. 에러(Errors)
에러는 예외가 아니라, 사용자나 프로그래머의 통제를 벗어난 문제들입니다. 에러는 자바 프로그램에서 거의 대처할 수 없으므로 보통 코드에서 무시합니다. 예를 들어 스택 오버플로가 발생하면 에러가 일어납니다. 컴파일 시에도 무시됩니다.
예외 계층 구조
모든 예외 클래스는 java.lang.Exception 클래스의 하위 타입입니다. 예외 클래스는 Throwable 클래스의 하위 클래스입니다. 예외 클래스 외에 Throwable 클래스에서 파생된 또 다른 하위 클래스인 Error가 있습니다.
에러는 심각한 실패 시 발생하는 비정상적 상태로, 자바 프로그램에서 처리하지 않습니다. 에러는 런타임 환경에서 생성된 에러를 나타냅니다. 예: JVM 메모리 부족. 일반적으로 프로그램은 에러에서 복구할 수 없습니다.
Exception 클래스에는 두 개의 주요 하위 클래스가 있습니다: IOException 클래스와 RuntimeException 클래스.
예외 메서드
Throwable 클래스에 있는 중요한 메서드들입니다.
| 번호 |
메서드 및 설명 |
| 1 | public String getMessage() - 발생한 예외에 대한 상세 메시지 반환. 이 메시지는 Throwable 생성자에서 초기화됨 |
| 2 | public Throwable getCause() - Throwable 객체로 표현된 예외의 원인 반환 |
| 3 | public String toString() - 클래스 이름과 getMessage() 결과를 연결하여 반환 |
| 4 | public void printStackTrace() - toString() 결과와 스택 트레이스를 System.err(에러 출력 스트림)에 출력 |
| 5 | public StackTraceElement[] getStackTrace() - 스택 트레이스의 각 요소를 담은 배열 반환. 인덱스 0은 호출 스택의 최상위, 마지막 요소는 호출 스택의 최하위 메서드 |
| 6 | public Throwable fillInStackTrace() - 현재 스택 트레이스로 이 Throwable 객체의 스택 트레이스를 채움. 이전 스택 트레이스 정보에 추가 |
예외 잡기(Catching Exceptions)
메서드는 try와 catch 키워드 조합으로 예외를 잡습니다. try/catch 블록은 예외가 발생할 수 있는 코드 주위에 배치합니다. try/catch 블록 내의 코드를 보호된 코드라고 하며, try/catch 사용 구문은 다음과 같습니다.
구문
try {
// 보호된 코드
} catch (ExceptionName e1) {
// Catch 블록
}
예외가 발생하기 쉬운 코드는 try 블록에 배치합니다. 예외가 발생하면 관련된 catch 블록이 처리합니다. 모든 try 블록은 즉시 catch 블록 또는 finally 블록이 따라와야 합니다.
catch 문은 잡으려는 예외 타입을 선언합니다. 보호된 코드에서 예외가 발생하면 try 다음의 catch 블록(들)이 검사됩니다. 발생한 예외 타입이 catch 블록에 나열된 타입과 일치하면, 메서드 매개변수로 인수가 전달되듯 예외가 catch 블록으로 전달됩니다.
예제
요소가 2개인 배열을 선언하고 3번째 요소에 접근하려 하여 예외를 던지는 예제입니다.
// File Name: ExcepTest.java
import java.io.*;
public class ExcepTest {
public static void main(String args[]) {
try {
int a[] = new int[2];
System.out.println("Access element three: " + a[3]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Exception thrown: " + e);
}
System.out.println("Out of the block");
}
}
실행 결과
Exception thrown: java.lang.ArrayIndexOutOfBoundsException: 3
Out of the block
다중 Catch 블록
try 블록 뒤에는 여러 catch 블록이 올 수 있습니다.
구문
try {
// 보호된 코드
} catch (ExceptionType1 e1) {
// Catch 블록
} catch (ExceptionType2 e2) {
// Catch 블록
} catch (ExceptionType3 e3) {
// Catch 블록
}
위 예제는 세 개의 catch 블록을 보여주지만, 단일 try 뒤에 원하는 만큼 많은 catch 블록을 둘 수 있습니다. 보호된 코드에서 예외가 발생하면 리스트의 첫 번째 catch 블록으로 예외가 던져집니다. 던져진 예외의 데이터 타입이 ExceptionType1과 일치하면 거기서 잡힙니다. 그렇지 않으면 두 번째 catch 문으로 넘어갑니다. 예외가 잡히거나 모든 catch를 통과할 때까지 계속되며, 모두 통과하면 현재 메서드 실행이 중단되고 예외가 호출 스택의 이전 메서드로 던져집니다.
예제
try {
file = new FileInputStream(fileName);
x = (byte) file.read();
} catch (IOException i) {
i.printStackTrace();
return -1;
} catch (FileNotFoundException f) // 유효하지 않음!
{
f.printStackTrace();
return -1;
}
여러 타입의 예외 한 번에 잡기 (Java 7+)
자바 7부터 단일 catch 블록으로 둘 이상의 예외를 처리할 수 있어 코드가 간단해집니다.
catch (IOException | FileNotFoundException ex) {
logger.log(ex);
throw ex;
}
throws/throw 키워드
메서드가 체크 예외를 처리하지 않으면 throws 키워드로 선언해야 합니다. throws 키워드는 메서드 시그니처 끝에 위치합니다.
새로 인스턴스화한 예외나 방금 잡은 예외를 throw 키워드로 던질 수 있습니다.
throws와 throw의 차이를 이해하세요: throws는 체크 예외 처리를 미루는 데 사용되고, throw는 예외를 명시적으로 발생시키는 데 사용됩니다.
예제: RemoteException 던지기 선언
import java.io.*;
public class ClassName {
public void deposit(double amount) throws RemoteException {
// 메서드 구현
throw new RemoteException();
}
// 클래스 정의 나머지
}
메서드는 둘 이상의 예외를 던질 수 있다고 선언할 수 있으며, 이때 예외들은 콤마로 구분됩니다.
예제: 여러 예외 선언
import java.io.*;
public class ClassName {
public void withdraw(double amount) throws RemoteException, InsufficientFundsException {
// 메서드 구현
}
// 클래스 정의 나머지
}
finally 블록
finally 블록은 try 블록 또는 catch 블록 뒤에 옵니다. finally 블록의 코드는 예외 발생 여부와 관계없이 항상 실행됩니다.
finally 블록을 사용하면 보호된 코드에서 무슨 일이 일어나든 실행하고 싶은 정리 타입의 문장들을 실행할 수 있습니다.
finally 블록은 catch 블록들의 끝에 위치하며 구문은 다음과 같습니다.
구문
try {
// 보호된 코드
} catch (ExceptionType1 e1) {
// Catch 블록
} catch (ExceptionType2 e2) {
// Catch 블록
} catch (ExceptionType3 e3) {
// Catch 블록
} finally {
// finally 블록은 항상 실행됨
}
예제
public class ExcepTest {
public static void main(String args[]) {
int a[] = new int[2];
try {
System.out.println("Access element three: " + a[3]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Exception thrown: " + e);
} finally {
a[0] = 6;
System.out.println("First element value: " + a[0]);
System.out.println("The finally statement is executed");
}
}
}
실행 결과
Exception thrown: java.lang.ArrayIndexOutOfBoundsException: 3
First element value: 6
The finally statement is executed
다음 사항에 유의하세요.
- catch 절은 try 문 없이는 존재할 수 없음
- try/catch 블록이 있을 때마다 finally 절이 필수는 아님
- try 블록은 catch 절이나 finally 절 없이는 존재할 수 없음
- try, catch, finally 블록 사이에 어떤 코드도 있을 수 없음
try-with-resources (자바 7+)
일반적으로 스트림, 연결 같은 리소스를 사용하면 finally 블록에서 명시적으로 닫아줘야 합니다. 다음 프로그램에서는 FileReader로 파일에서 데이터를 읽고 finally 블록으로 닫습니다.
예제: 기존 방식
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class ReadData_Demo {
public static void main(String args[]) {
FileReader fr = null;
try {
File file = new File("file.txt");
fr = new FileReader(file);
char[] a = new char[50];
fr.read(a); // 내용을 배열로 읽음
for (char c : a)
System.out.print(c); // 문자 하나씩 출력
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fr.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
try-with-resources는 자동 리소스 관리라고도 하며, 자바 7에 도입된 새로운 예외 처리 메커니즘으로, try-catch 블록에서 사용된 리소스를 자동으로 닫아줍니다.
이 문을 사용하려면 괄호 안에 필요한 리소스를 선언하기만 하면 되며, 생성된 리소스는 블록 끝에서 자동으로 닫힙니다. try-with-resources 문의 구문은 다음과 같습니다.
구문
try (FileReader fr = new FileReader("file path")) {
// 리소스 사용
} catch () {
// catch 블록 본문
}
예제: try-with-resources 사용
import java.io.FileReader;
import java.io.IOException;
public class Try_withDemo {
public static void main(String args[]) {
try (FileReader fr = new FileReader("E://file.txt")) {
char[] a = new char[50];
fr.read(a); // 내용을 배열로 읽음
for (char c : a)
System.out.print(c); // 문자 하나씩 출력
} catch (IOException e) {
e.printStackTrace();
}
}
}
try-with-resources 문 사용 시 주의사항:
- try-with-resources 문에서 클래스를 사용하려면 AutoCloseable 인터페이스를 구현해야 하며, 그 close() 메서드가 런타임에 자동으로 호출됨
- try-with-resources 문에 둘 이상의 클래스 선언 가능
- try-with-resources 문의 try 블록에 여러 클래스를 선언하면 역순으로 닫힘
- 괄호 안의 리소스 선언을 제외하면 일반 try/catch 블록과 동일
- try에 선언된 리소스는 try 블록 시작 직전에 인스턴스화됨
- try 블록에 선언된 리소스는 암묵적으로 final로 선언됨
사용자 정의 예외(User-defined Exceptions)
자바에서 직접 예외를 만들 수 있습니다. 직접 예외 클래스를 작성할 때 다음 사항을 기억하세요.
- 모든 예외는
Throwable의 자식이어야 함
- 컴파일러가 자동으로 강제하는 체크 예외를 원하면
Exception 클래스를 상속
- 런타임 예외를 원하면
RuntimeException 클래스를 상속
다음과 같이 직접 Exception 클래스를 정의할 수 있습니다.
class MyException extends Exception {
}
미리 정의된 Exception 클래스를 상속하기만 하면 자신만의 Exception을 만들 수 있습니다. 이들은 체크 예외로 간주됩니다. 다음 InsufficientFundsException 클래스는 Exception 클래스를 상속하는 사용자 정의 예외로, 체크 예외가 됩니다. 예외 클래스도 다른 클래스처럼 유용한 필드와 메서드를 가질 수 있습니다.
예제: InsufficientFundsException
// File Name: InsufficientFundsException.java
import java.io.*;
public class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(double amount) {
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
사용자 정의 예외 사용을 시연하기 위해, withdraw() 메서드에서 InsufficientFundsException을 던지는 CheckingAccount 클래스를 만듭니다.
// File Name: CheckingAccount.java
import java.io.*;
public class CheckingAccount {
private double balance;
private int number;
public CheckingAccount(int number) {
this.number = number;
}
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= balance) {
balance -= amount;
} else {
double needs = amount - balance;
throw new InsufficientFundsException(needs);
}
}
public double getBalance() {
return balance;
}
public int getNumber() {
return number;
}
}
다음 BankDemo 프로그램은 CheckingAccount의 deposit()과 withdraw() 메서드 호출을 시연합니다.
// File Name: BankDemo.java
public class BankDemo {
public static void main(String[] args) {
CheckingAccount c = new CheckingAccount(101);
System.out.println("Depositing $500...");
c.deposit(500.00);
try {
System.out.println("\nWithdrawing $100...");
c.withdraw(100.00);
System.out.println("\nWithdrawing $600...");
c.withdraw(600.00);
} catch (InsufficientFundsException e) {
System.out.println("Sorry, but you are short $" + e.getAmount());
e.printStackTrace();
}
}
}
위 세 파일을 모두 컴파일하고 BankDemo를 실행하면:
실행 결과
Depositing $500...
Withdrawing $100...
Withdrawing $600...
Sorry, but you are short $200.0
InsufficientFundsException
at CheckingAccount.withdraw(CheckingAccount.java:25)
at BankDemo.main(BankDemo.java:13)
자주 발생하는 예외
자바에서는 예외와 에러를 두 범주로 정의할 수 있습니다.
- JVM 예외(JVM Exceptions) - JVM이 독자적으로 또는 논리적으로 던지는 예외/에러. 예: NullPointerException, ArrayIndexOutOfBoundsException, ClassCastException
- 프로그래밍 예외(Programmatic Exceptions) - 애플리케이션이나 API 프로그래머가 명시적으로 던지는 예외. 예: IllegalArgumentException, IllegalStateException