2015 단어
10 분
[ JAVA ] Exception

우아한 테크코스 프리코스를 진행하던 중 예외처리와 관련된 오류가 발생해 문제를 해결하려다 보니, 처음부터 정리해보려고 한다. 이전 프리코스에서는 구현에만 초점을 맞춰 프로그램을 작성했었다면, 이번에는 OOP와 각 코드의 작성 이유에 대해서도 중점적으로 살펴보고자 한다. 따라서 이번 포스트에서는 자바의 예외처리 구조와 이를 활용한 코드 작성 방법에 대해 알아보자.

Error & Exception#

오류(Error) 는 시스템에 비정상적인 상황이 발생했을 때 생기는 문제다. 이는 시스템 레벨에서 발생하는 심각한 문제로, Error 클래스 및 그 하위 클래스들은 주로 가상머신(Virtual Machine)에서 발생하는 문제(예: 메모리 부족 등)를 나타낸다. 일반적으로 프로그램에서는 이러한 오류를 처리하려 하지 않으며, 오류 발생 시 프로그램이 종료된다. 개발자가 사전에 예측하여 처리할 수 없으므로, 애플리케이션에서는 오류에 대한 처리를 신경 쓸 필요가 없다.

반면, 예외(Exception) 는 개발자가 작성한 로직에서 발생한다. Exception 클래스와 그 하위 클래스들은 개발자가 예외 상황을 처리하고 복구하는 데 사용된다. 즉, 예외는 개발자가 직접 처리할 수 있으므로, 이를 명확히 구분하고 적절한 처리 방안을 마련하는 것이 중요하다.

  • 오류(Error): 프로그램 코드로 수습할 수 없는 심각한 문제
  • 예외(Exception): 프로그램 코드로 수습 가능한 비교적 경미한 문제

예외 클래스의 구조#

대체 텍스트

모든 예외 클래스는 최상위 클래스인 `Object`의 자식인 `Throwable` 클래스를 상속받는다. `Throwable`을 상속하는 클래스는 크게 `Error`와 `Exception` 두 가지로 나뉜다.
  • Error: 시스템 레벨의 심각한 문제를 나타내며, 보통 시스템 자체에서 처리해야 하는 경우가 많다.
  • Exception: 개발자가 직접 로직을 추가해 처리할 수 있는 문제를 나타낸다.

모든 예외의 최고 조상은 Exception클래스이며, 상속계층도를 Exception클래스부터 도식화하면 다음과 같다.

대체 텍스트

특히, Exception은 크게 두 가지 범주로 구분된다.

Checked Exception && Unchecked Exception#

Unchecked ExceptionChecked 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으로 전환하여 상위 계층에서 일일이 예외를 선언할 필요 없이 처리할 수 있도록 도와준다. 호출 측에서는 보다 구체적인 예외를 받고, 그에 따른 적절한 처리를 할 수 있게 된다.

출처#

[ JAVA ] Exception
https://blog-full-of-desire-v3.vercel.app/posts/java/exception/
저자
SpeculatingWook
게시일
2023-11-05
라이선스
CC BY-NC-SA 4.0