본문 바로가기

JAVA 스터디

[JAVA] 예외처리(Exception)

예외(Exception)

예외란 "exceptional event"의 약어입니다.

예외는 프로그램 실행 중에 발생하는 프로그램 실행의 일반적인 흐름을 방해하는 이벤트입니다.

 

메소드 내부에서 에러가 발생하면 메서드는 객체를 만들고 런타임 시스템에 전달합니다.

해당 객체는 "예외 객체"라고 불리며 객체에는 오류가 발생했을 때 프로그램 상태를 포함하여

오류에 대한 정보와 유형에 대한 정보를 포함하고 있습니다.

예외 객체를 생성하여 런타임 시스템에 전달하는 것을 "예외 발생(throwing an exception)"이라고 합니다.

 

특정 메소드에서 예외가(를) 발생되면(던지면), 프로그램의 동작이 멈추게 되므로 반드시 해결해야 합니다.

이를 위해서 제일 먼저 해야 하는 일은 발생한 예외를 해결할 수 있는 특정 메소드를 찾는 일입니다.

이때 말하는 특정 메소드란 발생한 오류를 해결할 수 있는 "예외 핸들러(exception handler)"를 포함하는 메소드입니다.

해당 역할을 수행하는 것이 바로 "런타임 시스템(runtime system)"입니다.

 

런타임 시스템은 예외 처리기가 포함된 메소드를 찾기 위해서 리스트 형태로 정의된 메소드 집합을 활용합니다.

 

참고로 이 메소드들의 리스트는 "콜 스택(call stack)"으로 불리고 있으며, 아래의 그림과 같이 정의됩니다.

 

콜 스택(call stack)

런타임 시스템은 예외를 처리할 수 있는 메소드를 찾기 위해서 콜 스택을 탐색합니다.

탐색은 오류가 발생한 메소드로부터 시작하여 메소드가 호출된 역순으로 진행됩니다.

적절한 핸들러를 발견하면, 런타임 시스템은 이 핸들러에게 예외를 전달(throwing)합니다.

 

핸들러는 전달받은 예외 객체의 유형이 처리할 수 있는 것인지 확인하고,

처리할 수 있는 경우 런타임 시스템의 행동이 적절했다고 판단합니다.

 

핸들러가 선택되면 "예외를 캐치했다(catch the exception)"고 합니다.

 

아래 그림과 같이 런타임 시스템(runtime system)이 적절한 핸들러를 찾지 못하고 모든 콜 스택을 탐색하게 되면

런타임 시스템은 종료됩니다.(사실상 프로그램이 종료된단 의미입니다.)

 

핸들러에 대한 호출 스택 검색

자바에서 예외 처리 방법(try, catch, throw, throws, finally)

자바에서 예외를 처리하기 위해선 try, catch, finally 블럭을 사용합니다.

 

try {
    예외를 처리하길 원하는 실행 코드;
} catch (e1) {
    e1 예외가 발생할 경우에 실행될 코드;
} catch (e2) {
    e2 예외가 발생할 경우에 실행될 코드;
}
...
finally {
    예외 발생 여부와 상관없이 무조건 실행될 코드;
}

 

try 블럭

예외 핸들러를 생성할 때 제일 먼저 try 블럭을 작성해줍니다.

try 블럭 내부에는 예외가 발생할 수 있는 코드를 넣어줍니다.

코드는 한 줄이 될 수 있고, 여러 줄이 될 수도 있습니다.

 

예제를 살펴보면 다음과 같습니다.

 

public void writeList() {
    printWriter out = null;
    try {
        System.out.println("try문에 들어왔습니다.");
        new Printwriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at : " + 1 + " = " + list.get(i));
        }
    }
    catch and finally
}

 

만약 try 블럭 안의 코드가 실행되는 중에 예외가 발생한다면, 예외는 연관된 예외 처리 코드에 의해 처리됩니다.

try 블럭과 연관된 예외 처리 코드를 선언하는 방법은 catch 블럭에서 작성해줍니다.

 

catch 블럭

try 블럭에서 발생한 예외 코드나 예외 객체를 인수로 전달받아 그 처리를 담당하는 블럭입니다.

catch 블럭은 try 블럭 바로 다음에 위치해야 합니다.

 

try {
    ...
} catch(ExceptionType name) {
    ...
 } catch(ExceptionType name) {
    ...
 }

 

각 catch 블럭은 인자가 나타내는 예외 타입을 처리하는 핸들러입니다.

인수 타입인 ExceptionType은 핸들러가 처리할 수 있는 예외 유형을 선언하며,

Throwable Class에서 상속되는 클래스의 이름이어야 합니다.

name을 통해서 예외를 참조할 수 있습니다.

 

catch 블럭은 아래의 예제처럼 작성할 수 있습니다.

 

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("try문에 들어왔습니다.")
        new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + 1 + " = " + list.get(i));
        }
    } catch (IndexOutOfBoundsException e) {
        System.err.println("IndexOutOfBoundsException: " + e.getMessage());
    } catch (IOException e) {
        System.err.println("Caught IOException: " + e.getMessage());
    }
}

 

catch 블럭을 살펴보면 어떤 예외가 발생했는지 알려주는 출력문이 작성되었습니다.

해당 부분에 적절한 해결 코드를 작성해주는 것이 예외 처리의 목표입니다.

 

Java 7 이후 버전에서는 catch 블럭에 여러 개의 예외를 처리할 수 있는 핸들러를 등록할 수 있습니다.

이를 통해서 중복된 코드를 제거하고, 지나치게 많은 예외를

한 번에 찾으려고 하는 행동(Exception e와 같은 것)을 줄일 수 있습니다.

(예외의 계층 구조상 Exception이 모든 예외의 부모 클래스이기 때문에 대부분의 예외를 포착하게 됩니다.)

 

catch(IOException | SQLException ex) {
    logger.log(ex);
    throw ex;
}

 

finally 블럭

이 블럭은 try 블럭에서 예외가 발생하든 안 하든 맨 마지막에 무조건 실행됩니다.

 

아래의 코드는 해당 내용의 예제입니다.

 

public class Example {
    public static void main(String[] args) {
        int[] num = new int[1];
       
        try {
            System.out.println("num[0] : " + num[0]);
         // System.out.println("num[0] : " + num[1]);        예외 발생
        } catch (Exception e) {
            System.out.println("Exception occurred : " + e);
        } finally {
            System.out.println("Done");
        }
    }
}

// 예외가 발생하지 않은 경우
0
Done

// 예외가 발생한 경우
Exception occurred : java.lang.ArrayIndexOutOfBoundsException: 1
Done

 

예기지 못한 예외가 발생했을 때, 무조건 처리해야 하는 코드를 finally에 작성해두면 되겠습니다.

 

writeList 메서드의 try 블럭에서는 PrintWriter를 엽니다.

프로그램은 writeList 메서드를 종료하기 전에 해당 스트림을 닫아야 합니다.

이것은 writeList의 try 블럭이 세 가지 방법 중 하나로 종료될 수 있기 때문에 복잡한 문제가 될 수 있습니다.

 

1. FileWrite 문은 실패하면 IOException을 던집니다.

2. list.get(i) 문이 실패하면 IndexOutOfBoundsException을 던집니다.

3. 모두 성공하면 try 블럭은 종료됩니다.

 

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("try문에 들어왔습니다.");
        new PrintWriter(new FileWriter("OutFile.txt"));
        for(int i = 0; i < SIZE; i++) {
            System.out.println("Value at: " + 1 + " = " + list.get(i));
        }
    } catch(IndexOutOfBoundsException e) {
        System.err.println("IndexOutOfBoundsException: " + e.getMessage());
    } catch(IOException e) {
        System.err.println("Caught IOException: " + e.getMessage());
    } finally {
        if(out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } else {
            System.out.println("PrintWriter not open");
        }
    }
}

 

위의 예제에서 finally 블럭은 리소스 누수를 막기 위한 중요한 도구입니다.

 

throw & throws

throw는 예외를 강제로 발생시키는 것으로

코드를 작성하는 프로그래머가 강제로 예외를 발생시킵니다

 

public class Example {
    public static void main(String[] args) {
        try {
            throw new Exception();    // 강제로 Exception 객체를 생성
        } catch(Exception e) {
            System.out.println("예외를 강제로 발생했습니다.");
        }
    }
}

// 실행결과
예외를 강제로 발생했습니다.

 

위의 예제 try 블럭에서 Exception 객체를 생성하고 throw로 강제로 예외를 발생시켰습니다.

강제된 Exception으로 catch 블럭이 실행되는 모습을 볼 수 있습니다.

예외를 강제로 발생시키는 이유는 예외를 한 번 더 처리하기 위해서입니다.

 

한 번 더 예외를 처리한다는 말의 의미를 알기 위해선, throws의 쓰임을 알아야 합니다.

 

throws는 현재 메소드에서 상위 메소드로 예외를 던지는 것입니다.

이때 현재 메소드란 호출된 메소드(혹은 생성자)이고, 상위 메소드는 메소드(혹은 생성자)를 호출하는 메소드입니다.

 

아래의 예제를 보시면 이해하기 쉬울 것입니다.

 

class Test {
    final static in value[] = {1, 2, 3};
    
    public Test() {
        System.out.println("Test instance was created");
    }
    
    // 예외 발생시 현재 메소드인 indexOf()
    public int indexOf(int idx) throws ArrayIndexOutOfBoundsException {
        return value[idx];        // 범위를 벗어나는 접근일 경우 예외 발생 & 예외를 던짐(throw)
    }
}

public class Example {
    public static void main(String[] args) {    // 예외 발생 시 상위 메소드 main()
        Test test = new Test();
        
        try {
            System.out.println(test.indexOf(2));
            System.out.println(test.indexOf(3));        // 범위를 벗어나는 접근 & 던져진 예외를 받음
        } catch (Exception e) {
            // try 블럭이 넘겨받은 예외를 처리
            System.out.println("Exception occurred " + e);
        }
    }
}

// 실행결과
Test instance was created
3
Exception occurred java.lang.ArrayIndexOutOfBoundsException: 3

 

위의 예제에서 Test 클래스의 indexOf() 메소드가 현재 메소드이고, main() 메소드가 상위 메소드입니다.

 

이처럼 특정 예외를 상위 메소드로 전가하는 이유는 예외 핸들러(exception handler)를 분산시키기 위해서입니다.

 

예를 들어 A라는 메소드를 참조하는 상위 메소드가 1,000,000개 있다고 가정하겠습니다.

또한 1,000,000개의 상위 메소드가 A 메소드를 이용하면서 발생하는 예외도 모두 제각각이라고 가정하겠습니다.

이렇게 되면 A() 메소드 내에서 try catch finally 블럭을 생성해서 예외를 처리하면 되겠지만,

그 내용이 터무니없이 많아집니다.

 

제 3자가 A() 메소드를 사용하다 예외가 발생하면 어떤 예외가 발생했는지,

어떻게 처리되었는지 확인하기 어려운 문제도 있습니다.

 

많은 시간을 낭비하게 되므로 일부 예외는 상위 메소드로 전가해서 처리하도록 유도하는 것입니다.

 

아래 예제는 throw와 throws를 함께 사용한 예제입니다.

 

class Test {
    final static in value[] = {1, 2, 3};
    ...    
    public int indexOf(int idx) throws ArrayIndexOutOfBoundsException {
        int buf;
        try {
            buf = value[idx];
            System.out.println(idx + "번째 인덱스의 요소 " + value[idx]);
        } catch (ArrayIndexOutOfBoundsException e) {
            // 범위를 벗어난 접근으로 예외 발생 & 처리
            System.out.println("인덱스 범위를 벗어났습니다.");
            
            throw e;    // 발생된 예외를 강제로 발생 & throws로 인해서 상위 메소드로 던짐(throw)
        }
        return buf;
    }
}

public class Example {
    public static void main(String[] args) {
        Test test = new Test();
        
        try {
            System.out.println(test.indexOf(3));        // 범위를 벗어나는 접근 & 던져진 예외를 받음
        } catch (Exception e) {
            // try 블럭이 넘겨받은 예외를 처리
            System.out.println("유효하지 않은 접근입니다.");
        }
    }
}

// 실행결과
...
인덱스 범위를 벗어났습니다.
유효하지 않은 접근입니다.

 

자바가 제공하는 예외 계층 구조

자바의 예외 계층 구조는 다음과 같습니다.

 

예외 계층

Throwable은 Object를 상속하는 예외와 관련된 최상위 클래스입니다.

이 클래스의 후손 클래스들이 모두 예외 관련 클래스들이 됩니다.

보이는 바와 같이 Throwable은 2개의 직계 후손인 Error와 Exception를 가지고 있습니다.

 

Exception과 Error의 차이

자바에서 프로그램을 작성할 때 자바 문법에 맞지 않는 코드를 작성하고 컴파일하면,

자바 컴파일러는 문법 오류(syntax error)를 발생시킵니다.

 

또한, 자바 문법에는 맞게 작성했지만 프로그램이 실행되면서 예상하지 못한 오류가 발생할 수 있습니다.

 

이렇게 시스템이 동작하는 도중 예상하지 못한 사태가 발생하여

실행 중인 프로그램이 영향을 받는 것을 오류(error)와 예외(exception)입니다.

 

오류는 시스템 레벨에서 프로그램에 심각한 문제를 야기하여 실행 중인 프로그램을 종료시킵니다.

이러한 오류는 개발자가 미리 예측하여 처리할 수 없는 것이 대부분이므로,

오류에 대한 처리는 할 수 없습니다.

 

하지만 예외는 오류와 마찬가지로 실행 중인 프로그램을 비정상적으로 종료시키지만,

발생할 수 있는 상황을 미리 예측하여 처리할 수 있습니다.

 

RuntimeException과 RE가 아닌 것의 차이

RuntimeException을 보통 Uncheckted Exception, 그 외의 모든 예외를 Unchecked Exception이라고 부릅니다.

 

UncheckedException은 개발자가 해당 오류를 처리하거나 던질 필요가 없는 예외입니다.

Checked Exception은 코드에서 반드시 처리가 되어야 하는 예외입니다.

 

커스텀한 예외 만드는 방법

throw를 할 때, 다른 사람이나 언어가 제공한 예외를 사용할 수 있습니다.

Java는 사용할 수 있는 많은 예외 클래스를 제공하지만, 직접 작성할 수도 있습니다.

 

아래 조건을 모두 충족시킨다면, 예외 클래스를 직접 작성하는 것이 좋습니다.

  1. Java에서 지원하는 않는 예외가 필요하다
  2. 다른 벤더가 작성한 클래스에서 발생한 예외와, 당신이 작성한 예외를 구별할 수 있다면 사용자에게 도움이 된다.
  3. 코드에서 관련 예외가 2개 이상 발생한다.
  4. 다른 사람의 예외를 사용하는 경우 사용자가 해당 예외에 액세스 할 수 있다. 비슷한 질문으로 당신의 패키지는 독립적이고, 스스로 관리된다.

예외 클래스는 집합 구조를 만들 수 있습니다.

 

이는, 부모 예외 클래스를 상속하는 구조로 만들 수 있습니다.

작성하는 대부분의 프로그램은 Exception의 하위면 충분합니다.

 

참고

https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html

 

Lesson: Exceptions (The Java™ Tutorials > Essential Classes)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

http://tcpschool.com/java/java_exception_intro