[Java] Checked Exception과 Unchecked Exception

 
 

 
 

Exception


Exception은 프로그램 실행 중에 발생하는 오류상황이다. 가령 없는 파일을 읽거나 잘못된 인덱스를 참조하는 등 많은 예외가 발생한다. 그리고 이들은 Checked Exception과 Unchecked Exception으로 분류를 할 수 있다.
 
 

Checked Exception


Checked Exception은 '컴파일 시점'에 처리 여부를 확인해야 하는 예외다. 즉, 코드 작성 때 반드시 처리해야한다. 처리하지 않으면 컴파일러가 오류를 발생시킬 것이므로, try-catch 블록으로 처리하거나 throws 키워드를 선언해주는 것이 예방책이다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
            System.out.println(reader.readLine());
            reader.close();
        } catch (IOException e) {
            System.out.println("파일 처리 중 오류 발생: " + e.getMessage());
        }
    }
}

 
 
대표적인 예시중 하나. BufferedReader는 파일을 읽는 데에 있어서 try-catch를 명시해주지 않으면 컴파일러한테 혼난다.
IOException 외에도 SQLException, classNotFoundException.. 등등이 존재한다
 
 

Unchecked Exception


앞선 예외가 '컴파일'시점이었으니 이번엔 '런타임'에만 발생하는 예외다. 컴파일러가 처리 여부를 강제하지 않는다.
try-catch로 처리하지 않아도 컴파일은 정상 완료되며, 프로그래머는 선택적인 처리를 하게된다.

public class UncheckedExceptionExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        
        // 배열 인덱스 초과 (Unchecked Exception)
        System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException 발생
    }
}

 
대표적으로 NFE를 비롯하여 위처럼 배열 인덱스 초과라든지, ArithmeticException(연산 실수)라든지가 존재한다.
 
 
 
 

왜 Checked Exception이 있을까


Java만 하는 사람한테는 이 컴파일 시점의 오류가 익숙할텐데, 사실 C++과 Python 등은 이렇게 강제적인 예외처리를 요구하지 않는다. try-catch가 있을지 언정, 예외 처리 여부는 전적으로 개발자 마음에 달린 것이다. 
 

Java의 Checked/Unchecked Exception의 도입 이유


Java는 초기 설계 단계에서 안전성을 강조했다. 개발자가 외부 환경에 의존하는 작업을 특히나 염두했기에 이와 관련된 예외를 강제 처리하고팠으며 가령 파일이 없을 때라든지 네트워크 연결 실패가 발생한다면 프로그램이 안전하게 실패와 복구를 하길 바란다는 철학이 있었다. 따라서 Java의 Checekd Exception을 통해 "개발자가 예외를 놓치지 않길" 바랬다.
 
하지만, 모든 예외를 강제 처리하면 코드가 복잡하고 장황해진다. 더군다나 프로그램의 논리 자체는 개발자가 어느정도 추측하고 처리가 가능하기에 굳이 강제하지 않아도 된다는 공존 했다. 강제한다면 오히려 생산성이 떨어지니까. 따라서 안전성과 유연성이 대립하여 이 두가지의 예외처리로 나뉘어진 것이다.
 

Checked Exception은 비판을 받기도 한다


강제적인 처리로 인하여 개발자는 실제로는 의미가 없는 코드를 작성하게 되는 경우도 있다. 가령 아래처럼 예외를 삼켜버리는 경우다

try {
    // 어떤 작업
} catch (Exception e) {
    e.printStackTrace();
    // 사실상 별도의 처리/복구 작업은 없음
}

Checked Exception을 처리하지 않으면 컴파일이 안되니까 억지로 catch 블록을 넣어 의도가 변질되어 버렸다.
이와 관련해 그 유명한 로버트 C.마틴의 클린 코드 철학에 따르면 "예외를 잡아 삼키지 말라, 예외를 예상하지 말고 복구 가능한 방식으로 처리하라" 라고 한다.  
 
예를 들어서, 아래와 같이 DAO 계층에서 무조건적인 예외 처리를 하는 것은 나쁜 예다

public class UserDao {

    public User findUserById(int id) {
        try {
            // 데이터베이스 작업 (SQLException 발생)
            throw new SQLException("DB 연결 실패!");
        } catch (SQLException e) {
            System.out.println("예외 처리 중: " + e.getMessage()); // 단순히 예외 로그
            return null; // 강제로 null 반환 (위험!)
        }
    }
}

 
DAO에서 의미없는 예외 처리를 해버려, 서비스 계층에서 이 예외를 복구하거나 적절히 반응할 기회를 주지 못했다. 결과적으로 그저 잘못된 데이터를 반환하게 만들었다.
 
올바른 방식은 아래와 같다.

public class UserDao {

    // Checked Exception을 명시적으로 throws
    public User findUserById(int id) throws SQLException {
        // 데이터베이스 작업
        throw new SQLException("DB 연결 실패");
    }
}

public class UserService {

    public User getUser(int id) {
        try {
            return new UserDao().findUserById(id);
        } catch (SQLException e) {
            
            // 예외를 로깅...
            // 적절한 대체 로직 수행...
            
            System.out.println("서비스 계층에서 예외 처리: " + e.getMessage());
        }
    }
}

DAO 계층에서는 SQLException을 throws로 선언해 서비스 계층에게 처리를 위임했고, 거기에 대해 Service 단에서 복구로직을 수행했다고 치면 이는 보다 알맞은 방식이 될 수 있다. 
여기서 한가지를 더 짚자면 throws 이용시 'Exception' 으로 뭉뜨그린다면 이는 어떤 오류인지 명확히 파악할 수 없으니 나쁜 예로 다시 조명받을 것이다. 어떤 상황인지 가능한 명확히 알리는 게 좋다.
 
더불어 아래와 같은 경우도 딱히 좋은 상황은 아니다.

public class UserValidator {

    public void validateUser(User user) throws Exception {
        if (user == null) {
            throw new Exception("유효하지 않은 사용자");
        }
    }
}

Null Pointer Exception에 대한 처리를 예로 드는 것인데, NFE는 앞서 말한 바와 같이 Unchecked Exception 유형 중 하나다. Unchecked Exception이 가지는 특징 중 하나가 일반적으로 개발자의 논리적 오류/실수 라는 것인데, 이는 사전에 방지할 수 있음에도 불구하고 예외 처리를 강제했다는 뜻이다. 즉 불필요한 코드를 작성했다. 따라서 코드 설계 과정에서 오류를 방지하는 논리를 추가하는 게 더 효율적일 경우다.
 
 
 
예외처리에 대한 핵심은 코드를 간결하고 명확하게 유지하면서 복구 가능성을 고려하는 것이다. 그러니 이를 바탕으로 Unchecked Exception을 사용할 때 올바른 적용인지 따져볼 수 있다.