우아한 테크코스 프리코스를 진행하던 중 예외처리와 관련된 오류가 발생해 문제를 해결하려다 보니, 처음부터 정리해보려고 한다. 이전 프리코스에서는 구현에만 초점을 맞춰 프로그램을 작성했었다면, 이번에는 OOP와 각 코드의 작성 이유에 대해서도 중점적으로 살펴보고자 한다. 따라서 이번 포스트에서는 자바의 예외처리 구조와 이를 활용한 코드 작성 방법에 대해 알아보자.
Error & Exception
오류(Error) 는 시스템에 비정상적인 상황이 발생했을 때 생기는 문제다. 이는 시스템 레벨에서 발생하는 심각한 문제로, Error
클래스 및 그 하위 클래스들은 주로 가상머신(Virtual Machine)에서 발생하는 문제(예: 메모리 부족 등)를 나타낸다. 일반적으로 프로그램에서는 이러한 오류를 처리하려 하지 않으며, 오류 발생 시 프로그램이 종료된다. 개발자가 사전에 예측하여 처리할 수 없으므로, 애플리케이션에서는 오류에 대한 처리를 신경 쓸 필요가 없다.
반면, 예외(Exception) 는 개발자가 작성한 로직에서 발생한다. Exception
클래스와 그 하위 클래스들은 개발자가 예외 상황을 처리하고 복구하는 데 사용된다. 즉, 예외는 개발자가 직접 처리할 수 있으므로, 이를 명확히 구분하고 적절한 처리 방안을 마련하는 것이 중요하다.
- 오류(Error): 프로그램 코드로 수습할 수 없는 심각한 문제
- 예외(Exception): 프로그램 코드로 수습 가능한 비교적 경미한 문제
예외 클래스의 구조
- Error: 시스템 레벨의 심각한 문제를 나타내며, 보통 시스템 자체에서 처리해야 하는 경우가 많다.
- Exception: 개발자가 직접 로직을 추가해 처리할 수 있는 문제를 나타낸다.
모든 예외의 최고 조상은 Exception클래스이며, 상속계층도를 Exception클래스부터 도식화하면 다음과 같다.
특히, Exception
은 크게 두 가지 범주로 구분된다.
Checked Exception && Unchecked Exception
Unchecked Exception | Checked Exception | |
---|---|---|
처리여부 | 반드시 예외를 처리해야 함 (try/catch 또는 throws로 처리) | 명시적인 예외 처리를 강제하지 않음 |
확인 시점 | 컴파일 단계에서 확인 가능 | 실행 단계에서 주로 발견됨 |
대표 예외 | RuntimeException 을 제외한 Exception 의 모든 하위 클래스 | RuntimeException 과 그 하위 예외 |
public void checkedExceptionExample() throws IOException {
FileReader file = new FileReader("없는파일.txt");
}
public void handleCheckedException() {
try {
checkedExceptionExample();
} catch (IOException e) {
System.out.println("Checked Exception 발생: " + e.getMessage());
}
}
Checked Exception은 메소드 내에서 발생할 가능성이 있는 예외를 반드시 try/catch 블록으로 감싸거나 throws 키워드를 사용하여 호출 측으로 전달해야 한다. 왜 그럴까? 이는 Checked Exception 자체가 하나의 API 역할을 하기 때문인데, 클라이언트가 요청을 보냈을 때 예외 상황을 명시적으로 인지할 수 있도록 설계되어 있기 때문이다.
import java.io.FileReader;
import java.io.IOException;
public class ExceptionExample {
public void uncheckedExceptionExample() {
int[] arr = new int[3];
arr[5] = 10;
}
public void divideByZero() {
int result = 10 / 0;
}
public static void main(String[] args) {
ExceptionExample example = new ExceptionExample();
try {
example.uncheckedExceptionExample();
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Unchecked Exception 발생: " + e.getMessage());
}
try {
example.divideByZero();
} catch (ArithmeticException e) {
System.out.println("Unchecked Exception 발생: " + e.getMessage());
}
}
}
반면, Unchecked Exception은 개발자의 부주의 등으로 인해 발생하는 경우가 많아 명시적으로 예외 처리를 강제하지 않는다. 그래서 만약 문제가 생겼을 때, 클라이언트에서 할 수 있는게 없기 때문에 따로 예외처리를 강제하지 않는다.
언제 어떤 예외를 사용하는 것이 좋을까?
예외처리를 할 때, 클라이언트에서 복구가 가능한 예외라면 Checked Exception을 사용하는 것이 좋다. 그렇다고 모든 예외처리에 Checked Exception을 사용할 필요는 없다. 코드가 너무 번잡해진다.
NOTE오라클 공식 문서
“If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.”
클라이언트가 예외로부터 합리적으로 복구할 수 있을 것으로 예상된다면, 이를 checked exception으로 처리하고, 복구할 수 없는 경우에는 unchecked exception으로 처리하세요.
예를 들어, 파일을 열기 전에 먼저 입력 파일 이름을 검증할 수 있다. 만약 사용자 입력 파일 이름이 올바르지 않다면, 커스텀 Checked Exception을 던질 수 있다.
if (!isCorrectFileName(fileName)) {
throw new IncorrectFileNameException("Incorrect filename : " + fileName );
}
이렇게 하면 다른 사용자 입력 파일 이름을 받아 시스템을 복구할 수 있다.
하지만, 입력 파일 이름이 null 포인터이거나 빈 문자열이라면, 이는 코드에 문제가 있음을 의미한다. 이 경우에는 Unchecked Exception을 던져야 한다.
if (fileName == null || fileName.isEmpty()) {
throw new NullOrEmptyException("The filename is null or empty.");
}
예외처리 방식
자바에서는 예외 상황을 다루기 위한 여러 가지 방식이 있다. 아래는 그 주요 방식들이다.
1. 예외 복구
예외 복구는 예외가 발생하더라도 애플리케이션의 정상적인 흐름을 유지하는 방법이다. 예를 들어, 네트워크 상태가 불안정해 서버 접속에 실패하는 상황에서 아래와 같이 재시도 로직을 구현할 수 있다.
int maxRetry = MAX_RETRY;
while (maxRetry-- > 0) {
try {
// 예외 발생 가능성이 있는 코드 실행
return; // 작업 성공 시 결과 반환
} catch (SomeException e) {
// 로그 출력 및 일정 시간 대기
} finally {
// 리소스 해제 및 정리 작업
}
}
throw new RetryFailedException(); // 최대 재시도 횟수를 초과하면 예외 발생
위와 같이 구현하면, 예외 발생 시에도 여러 번 재시도를 통해 작업을 복구할 수 있으며, 모든 시도가 실패할 경우 명시적으로 예외를 발생시켜 문제를 상위로 전달할 수 있다.
2. 예외처리 회피
예외처리 회피는 메소드 내에서 발생할 수 있는 예외를 직접 처리하지 않고 호출한 쪽으로 넘기는 방식이다.
public void add() throws SQLException {
// 구현 로직
}
이 방식은 코드가 간결해 보이지만, 예외 상황을 호출 측에서 반드시 처리하도록 강제하는 것이므로 신중하게 사용해야 한다. 호출한 쪽에서 해당 예외를 적절히 처리할 수 있는 확신이 있을 때 사용해야 한다.
3. 예외 전환
예외 전환은 하나의 예외를 잡아서, 호출 측에서 더 명확하게 인지할 수 있도록 다른 예외로 변환하여 던지는 방식이다.
catch (SQLException e) {
throw new DuplicateUserIdException();
}
이 방법은, 예를 들어 복구가 불가능한 Checked Exception을 Unchecked Exception으로 전환하여 상위 계층에서 일일이 예외를 선언할 필요 없이 처리할 수 있도록 도와준다. 호출 측에서는 보다 구체적인 예외를 받고, 그에 따른 적절한 처리를 할 수 있게 된다.