콘텐츠로 이동

예외처리

예외처리

왜 쓰는지

프로그램 실행 중 예상 밖의 상황이 발생할 수 있습니다. 예외처리 없이는 프로그램이 강제로 종료되어 시스템 장애로 이어질 수 있습니다.

핵심: 예외처리는 프로그램의 안정성과 신뢰성을 보장하는 필수 메커니즘입니다.

예외 계층 구조

Object
 └─ Throwable
     ├─ Error
     └─ Exception
          └─ RuntimeException
계층 설명 특징 예시
Throwable 모든 예외와 에러의 최상위 클래스 JVM이 던질 수 있는 모든 문제의 부모 -
Error 시스템 레벨 오류 애플리케이션에서 복구 불가능 OutOfMemoryError, StackOverflowError
Exception 애플리케이션에서 처리 가능한 예외 대부분의 예외가 여기 포함 -
RuntimeException Exception의 자식 대표적인 언체크 예외 NullPointerException, IllegalArgumentException

핵심 정리 - Error → 복구 불가능, 잡지 않는 것이 원칙 - Exception → 애플리케이션 레벨 예외 - RuntimeException → 개발자 실수성 예외

체크 예외 vs 언체크 예외

어떤 차이가 있는가?

구분 체크 예외 (Checked) 언체크 예외 (Unchecked)
기준 Exception 상속 (단 RuntimeException 제외) RuntimeException 상속
컴파일 검사 ✅ 컴파일러가 검사 ❌ 검사하지 않음
처리 의무 ✅ 반드시 catch 또는 throws ❌ 선택
안정성 ↑ 높음 ↓ 낮음
편의성 ↓ 낮음 ↑ 높음
대표 예외 SQLException, IOException, FileNotFoundException NullPointerException, IllegalArgumentException, IndexOutOfBoundsException

언제 각각을 사용하는가?

체크 예외를 쓰는 경우: - 외부 시스템과의 상호작용: DB, 파일 I/O, 네트워크 통신 - 복구 가능성이 있는 상황: 파일이 없으면 생성, 연결 실패 시 재시도

언체크 예외를 쓰는 경우: - 개발자의 논리적 실수: null 체크 미흡, 범위 검증 실패 - 복구가 불가능한 상황: 조건이 만족되면 절대 발생하지 않아야 함

장점과 단점

체크 예외

장점 - 컴파일러가 강제하므로 예외 처리 빠뜨림 방지 - 개발자에게 예외 발생 가능성을 명확히 알림

단점 - 모든 예외를 명시적으로 선언해야 함 (boilerplate 증가) - 깊은 콜 스택에서 매번 throws 전파해야 함 - 과도한 catch-rethrow 패턴 발생

언체크 예외

장점 - 메서드 시그니처가 깔끔함 - 선택적 예외 처리 가능 - 불필요한 throws 체인 제거

단점 - 컴파일러가 검사하지 않아 예외 빠뜨리기 쉬움 - 어떤 예외가 발생할 수 있는지 예측하기 어려움 - 문서화가 중요함 (하지만 누락되기 쉬움)

요약

Exception
 ├─ Checked Exception    → 외부 시스템과의 상호작용 (DB, 파일, 네트워크)
 └─ RuntimeException (Unchecked Exception) → 개발자 논리 오류

예외 처리 방법

어떻게 처리하는가?

방법 설명 사용 시점
catch 예외를 잡아서 직접 처리 현재 메서드에서 복구 가능
throws 호출한 곳으로 예외 전달 상위 계층에서 처리하도록 위임

코드 예시

// catch: 현재 메서드에서 처리
try {
    readFile("data.txt");
} catch (IOException e) {
    System.out.println("파일을 찾을 수 없습니다");
}

// throws: 상위 계층으로 전달
public void readFile(String filename) throws IOException {
    new FileInputStream(filename);
}

부모 예외의 다형성

자식 예외는 부모 예외 타입으로도 catch할 수 있습니다.

try {
    // NullPointerException, IllegalArgumentException 등 다양한 예외 발생 가능
} catch (Exception e) {  // 모든 Exception 하위 예외를 catch
    System.out.println("예외 발생: " + e.getMessage());
}

주의: catch (Exception e)는 편하지만, 예외가 무엇인지 구분하지 않으므로 특정 예외만 catch하는 것이 좋습니다.

try {
    ...
} catch (IOException e) {
    // IO 관련 예외만 처리
} catch (NumberFormatException e) {
    // 숫자 변환 오류만 처리
}

try-catch-finally 구조

어떻게 구조화되어 있는가?

블록 역할 실행 조건
try 정상 로직 수행 항상 실행
catch 예외 발생 시 처리 예외 발생 시만 실행
finally 리소스 정리 및 마무리 코드 항상 실행 (return 직전)

실행 흐름

정상 실행 경로:  try → finally
예외 발생 경로:  try → catch → finally

주의: return, break 등이 있어도 finally는 반드시 실행됨

코드 예시

try {
    // 정상 흐름
    connection.createStatement().execute("SELECT ...");
    System.out.println("쿼리 실행 완료");
} catch (SQLException e) {
    // 예외 처리
    System.out.println("쿼리 실행 오류: " + e.getMessage());
} finally {
    // 항상 실행 - 리소스 정리
    connection.close();
    System.out.println("연결 종료");
}

언제 쓰는가?

  • try: 예외 가능성이 있는 코드
  • catch: 해당 예외를 처리할 수 있는 로직
  • finally: 반드시 정리해야 하는 리소스 (연결, 스트림 등)

try-with-resources

왜 필요한가?

finally로 리소스를 정리하는 것은 번거롭고 실수하기 쉽습니다. Java 7부터 도입된 자동 리소스 관리 방식입니다.

어떻게 쓰는가?

// finally를 사용한 기존 방식
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("file.txt"));
    String line = reader.readLine();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// try-with-resources를 사용한 개선 방식 (권장)
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    String line = reader.readLine();
} catch (IOException e) {
    e.printStackTrace();
}
// reader.close()가 자동으로 호출됨

장점 vs finally

기존 방식 (try-catch-finally)

  • 리소스 정리 코드가 길어짐
  • null 체크 필수
  • 중첩된 try-catch로 복잡해짐
  • close() 호출 빠뜨리기 쉬움

try-with-resources (권장)

  • 자동으로 close() 호출
  • 코드가 간결함
  • 여러 리소스 한 번에 관리 가능
  • null 안전 (리소스 누수 방지)

실행 순서 비교

기존 방식:      try → (exception) → catch → finally → close()
try-with-resources:  try → (exception) → catch → (자동 close)

사용 조건

AutoCloseable 인터페이스를 구현해야 합니다.

class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("리소스 정리");
        // 실제 리소스 해제 로직
    }
}

// 사용
try (MyResource resource = new MyResource()) {
    // resource 사용
}  // 자동으로 resource.close() 호출

여러 리소스 관리

try (
    InputStream input = new FileInputStream("input.txt");
    OutputStream output = new FileOutputStream("output.txt")
) {
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = input.read(buffer)) != -1) {
        output.write(buffer, 0, bytesRead);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// input과 output이 자동으로 닫힘 (역순: output → input)

주의할 점

AutoCloseable이 아닌 리소스는 사용 불가

// ❌ 잘못된 예시
try (String str = "hello") {  // String은 AutoCloseable을 구현하지 않음
}

// ✅ 올바른 예시
try (BufferedReader reader = new BufferedReader(...)) {
}

예외가 발생해도 close()는 실행됨 - try 블록 실행 중 예외 발생 → close() 실행 → catch 블록 실행 - 따라서 리소스 누수 걱정 없음

실전 가이드

언제 어떤 것을 사용할까?

상황 선택 이유
DB 연결, 파일 I/O, 네트워크 try-with-resources 리소스를 안전하게 정리하기 위함
외부 시스템 호출 catch로 체크 예외 처리 복구 가능한 상황에 대비
입력값 검증 실패 throw new IllegalArgumentException() 개발자 실수를 명확히 함
NullPointerException 예방 null 체크 코드 추가 예외보다 사전 검증
깊은 콜 스택 상위 계층으로 throws 위임 boilerplate 감소

안티패턴 (피해야 할 패턴)

❌ 1. 예외를 무시하는 코드

try {
    riskyOperation();
} catch (Exception e) {
    // 아무것도 하지 않음 - 위험!
}

✅ 올바른 방식:

try {
    riskyOperation();
} catch (IOException e) {
    logger.error("작업 실패: {}", e.getMessage());
    // 또는 더 적절한 예외로 변환
    throw new ApplicationException("작업을 수행할 수 없습니다", e);
}

❌ 2. 일반적인 Exception catch

try {
    // 여러 가지 예외 가능
} catch (Exception e) {
    // 모든 예외를 같게 처리 - 예측 불가능
}

✅ 올바른 방식:

try {
    readFile();
} catch (FileNotFoundException e) {
    // 파일 없음 처리
} catch (IOException e) {
    // 읽기 오류 처리
}

❌ 3. finally에서 return (예외 숨김)

try {
    throw new RuntimeException("중요한 에러");
} finally {
    return;  // 예외가 숨겨짐!
}

정리

구분 원칙
Error 절대 잡지 않는다
Checked Exception 외부 시스템 (DB, 파일, 네트워크)
RuntimeException 개발자 로직 오류
리소스 관리 try-with-resources 필수 (IO/DB)
예외 처리 무시하지 말고, 구체적으로 처리하거나 의미 있게 전파