[윤성우의 열혈 Java 프로그래밍] Chapter 18 - 예외처리(Exception Handling)

Update:     Updated:

카테고리:

태그:

18-1. 자바 예외처리의 기본

- 자바에서 말하는 예외

프로그램 실행 중에 발생하는 ‘예외적인 상황’을 줄여서 ‘예외’라 한다.
즉, 단순한 문법 오류가 아닌 실행 중간에 발생하는 ‘정상적이지 않은 상황’을 뜻한다.

가상머신은 예외가 발생하면 그 내용을 간단히 출력하고 프로그램을 종료해버린다.
가상머신은 예외의 원인을 알지만 개발자가 원하는 예외의 처리 방식까지는 알지 못하기 때문에
이 예외에 대해 처리하는 부분을 만들어주면 된다.

- 예외의 처리를 위한 try ~ catch

자바는 예외 상황별로 그 상황을 알리기 위한 클래스를 정의하고 있다.
그 클래스를 ‘예외 클래스’라 한다.

예외를 처리할 때는 try ~ catch문을 사용하는데, 구조는 다음과 같다.

// try 영역에서 발생한 예외 상황을 catch 영역에서 처리한다.

try {
    ... 관찰 영역 ...
} catch(Exception e) {
    ... 처리 영역 ...
}
  1. try 영역의 실행 중간에 예외 상황이 만들어지고
  2. 이로 인해 가상머신이 ArithmeticException 인스턴스를 생성하면
  3. 이 인스턴스는 메소드를 호출하듯이 catch 구문의 매개변수 e에 전달이 된다.

- 둘 이상의 예외를 처리하기 위한 구성

// 각 예외마다 처리 과정이 다른 경우
try {
    ... 관찰 영역 ...
} catch(ArithmeticException e) {
    System.out.println("Exception 1 Occur!");
} catch(InputMIsmatchException e) {
    System.out.pritntln("Exception 2 Occur!");
}
// 각 예외에 대해 처리 과정이 같은 경우
try {
    ... 관찰 영역 ...
} catch(ArithmeticException | InputMIsmatchException e) {
    System.out.println("Exception Occur!");
}

- Throwable 클래스와 예외처리의 책임 전가

image

public class ExceptionMessage {
    public static void md1(int n) {
        md2(n, 0);
    }

    public static void md2(int n1, int n2) {
        int r = n1 / n2;
    }

    public static void main(String[] args) {
        md1(3);
        System.out.println("Good bye~~!");
    }
}

위 예제의 메소드 호출 흐름은 다음과 같다.
main -> md1 -> md2

예외가 발생하고 해당 예외를 처리하지 않으면
발생한 메소드를 호출한 메소드에 그 책임이 넘어간다.

image

즉, 책임이 넘어가는 과정은
md2 -> md1 -> main
이렇게 된다.

18-2. 예외처리에 대한 나머지 설명들

- 예외 클래스의 구분

image

예외라는 객체는 최상위 인터페이스로 Throwable이라는 인터페이스를 가진다.
이 Throwable 인터페이스를 error와 exception 인터페이스가 구현을 하고, 다시 runtimeException이 exception 인터페이스를 구현한다.

  • Error
    Error 클래스를 상속하는 클래스들을 보면 VirtualMachineError나 IOError 등등, JVM에서 발생한 오류이거나 입출력 관련하여 코드 수준 복구가 불가능한 오류 같이 사용자 애플리케이션이 제어할 수 있는 예외가 아니다. 말 그대로 ‘에러’인 것이다.

    즉 Error 클래스를 상속하는 예외는 처리의 대상이 아니다.

    따라서 이런 유형의 예외가 발생하면 그냥 프로그램이 종료되도록 놔두고 이후에 원인을 파악하는 과정이 이어져야 한다.

  • Exception
    예상할 수 있는 예외 또는 프로그래머가 만드는 예외 이다.

    Checked exception은 개발자가 사용하는 라이브러리, 코드 등에서 new 생성자를 이용해 만들어져, 해당 모듈을 사용하는 객체에게 여기서 예외가 발생할 수 있으니 throws 키워드를 이용하여 상위 caller에게 예외를 전달하든, 확인해서 처리하든지 하라는 뜻이다.

    Unchecked exception은 개발자가 사용하는 라이브러리, 코드 등에서 만들어졌지만, 예외처리를 해야 할 상황은 아닌 경우를 말한다. NegativeArraySizeException이나 ArrayStoreException 처럼 코드를 수정해야 할 상황이지 예외처리할 상황은 아닌 것이다.

    Unchecked exception 또한 처리의 대상이 아니다.

- 개발자가 정의하는 예외

개발자가 직접 예외 클래스를 정의하고 이를 기반으로 특정 상황에서 예외가 발생하도록 할 수도 있다.
핵심은 Exception을 상속하는데 있다.

import java.util.Scanner;

class ReadAgeException extends Exception {
    public ReadAgeException() {
        super("유효하지 않은 나이가 입력되었습니다.");
    }
}

public class MyExceptionClass {
    public static void main(String[] args) {
        System.out.print("나이 입력: ");

        try {
            int age = readAge();
            System.out.printf("입력된 나이: %d \n", age);
        } catch (ReadAgeException e) {
            System.out.println(e.getMessage());
        }

    }

    public static int readAge() throws ReadAgeException {   // 발생시킨 예외를 자신을 호출한 메소드에 던짐
        Scanner kb = new Scanner(System.in);
        int age = kb.nextInt();

        if (age < 0) {
            throw new ReadAgeException();   // 예외를 발생시킴
        }

        return  age;
    }
}

image

- finally 구문

try ~ catch문은 하나의 문장이므로 try문 홀로 존재할 수 없다.
그런데 try문에 이어서 다음과 같이 finally 구문을 둘 수도 있다.

try {
    ...
} finally { // 코드의 실행이 try 안으로 진입하면, 무조견 실행된다.

}
try {
    ...
} catch(Exception e) {
    ...
} finally { // 코드의 실행이 try 안으로 진입하면, 무조견 실행된다.
    ...
}


그렇다면 finally 구문은 어떻게 유용하게 사용할 수 있을까?
다음 코드를 보자.

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FinallyCase {
    public static void main(String[] args) {
        Path file = Paths.get("/Users/june/Study/java-lang");
        BufferedWriter writer = null;

        try {
            writer = Files.newBufferedWriter(file); // IOException 발생 가능
            writer.write('A');
            writer.write('Z');
        } catch (IOException e) {
            e.printStackTrace();
        } finally { // 예외가 발생하든 안 하든 무조건 실행해야 하는 구문
            try {
                if (writer != null) {
                    writer.close(); // 파일을 열었으면 닫아주어야 함
                }
            } catch (IOException e) {   // close 하는 중에도 IOException 발생 가능하기 때문
                e.printStackTrace();
            }
        }
    }
}

파일 버퍼를 열었다면 예외가 발생하든 안 하든 무조건 close를 실행하여 닫아야 한다.
따라서, finally에 close메소드를 추가하여 무조건 실행이 되도록 만들어 주었다.
다만, close하는 동안에도 IOException이 발생할 수 있기 때문에 불가피하게 try ~ catch로 잡아주게 되었다.
하지만 코드 구성이 깔끔하지 않다는 아쉬움이 남는다.
이를 해결하기 위해서 try-with-resources라는 문법이 등장했다.

- try-with-resources 구문

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class TryWithResource {
    public static void main(String[] args) {
        Path file = Paths.get("/Users/june/Study/java-lang");

        try (BufferedWriter writer = Files.newBufferedWriter(file)) {
            writer.write('A');
            writer.write('Z');
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

다음과 같이 try문 옆에 괄호 안에 종료의 과정을 필요로 하는 리소스를 생성할 수 있다.
그리고 이 리소스는 try-with-resources문을 빠져나오면서 자동으로 종료된다.

‘자동으로 종료되어야 할 리소스’ 관련 클래스는 다음 인터페이스를 구현해야한다.

java.lang.AutoCloseable

그리고 이 인터페이스에는 다음 추상 메소드가 존재한다.

void close() throws Exception

즉, try-with-resources문에서 호출하는 메소드는 AutoCloseable 인터페이스의 close 메소드이다. 따라서 이외의 메소드를 기대하는 것은 어렵다는 것을 염두해두자.


(참고) 예외처리는 성능의 저하로 이어진다.
try 구문 안에 위치한 코드는 try 구문 밖에 위치한 코드에 비해 실행 속도가 느리다. 따라서 특별히 예외처리가 필요한 부분만 골라서 사용하도록 해야한다.

Reference

https://brunch.co.kr/@kd4/5 [자바의 예외처리]

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기