[Java] 예외처리
본문 바로가기
Java

[Java] 예외처리

by IYK2h 2023. 5. 19.
728x90

예외처리

 

목차

  • 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
  • 자바가 제공하는 예외 계층 구조
  • Exception과 Error의 차이는?
  • RuntimeException과 RE가 아닌 것의 차이는?
  • 커스텀한 예외 만드는 방법

 

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

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

The try block

try Block은 예외가 발생할 수 있는 코드를 넣는 곳입니다. 여기서 발생한 예외는 Catch Block을 통해 전달됩니다.

The catch block

Catch Block은 try 블록에서 발생한 예외 코드나 예외 객체를 Arguement로 전달받아 그 처리를 담당합니다.

The finally block

finally block은 try block이 끝난 후 또는 예상치 못한 에러가 발생 했을때도 실행을 보장합니다.

하지만 만약에 JVM이 try catch block을 실행 중에 종료된다면 finally block은 실행되지 않습니다.

마찬가지로 Thread가 try catch block을 실행 중에 interrupt 당하거나 kill 당한다면 finally block은 실행되지 않습니다.

finally block은 cleanup code를 넣어서 resource leak을 막을 용도로 사용하는게 좋습니다.

The try-with-resources Statement

try-with-resource는 Java 7에서 들어온 기능으로 try block에서 사용할 resource를 선언하는 식으로 사용됩니다.

finally block 대신에 resource를 자동으로 반환하기 위해 사용합니다.

여기서 말하는 리소스는 java.lang.AutoClosable 이나 java.io.Closable을 구현한 오브젝트를 말하며 반드시 close 되야하는 리소스 입니다.

예시는 다음과 같습니다.

// Java 7 이후 try-with-resources
static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
                   new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

// Java 7 이전
static String readFirstLineFromFileWithFinallyBlock(String path)
                                                     throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        if (br != null) br.close();
    }
}

Specifying the Exceptions Thrown by a Method

catch block을 통해 예외를 처리하는 것도 좋은 방법이지만 때로는 현재 메소드에서 예외를 처리하는 것보다 Call Stack에 있는 상위 메소드에서 예외를 처리하게 두는 것이 좋을 때가 있습니다.

이 경우 catch block이 없으므로 메소드에서 throw keyword를 통해 예외가 발생함을 알려줘야 합니다.

예시는 다음과 같습니다.

public void writeList() throws IOException, IndexOutOfBoundsException {
    PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
    for (int i = 0; i < SIZE; i++) {
        out.println("Value at: " + i + " = " + list.get(i));
    }
    out.close();
}

여기서 IndexOutOfBoundsException은 RuntimeException의 하위 클래스인 Unchekced Exception이므로 복구할 방법이 없으므로 지우는게 좋습니다.

The throw Statement

때로는 예외를 발생시켜야 할 때가 있습니다. 그건 throw keyword를 통해 가능합니다.

이런 발생시킬 수 있는 예외는 Exception의 최상위 클래스인 Throwable class의 하위 클래스라면 모두 가능합니다.

그러므로 Throwable class를 상속받은 사용자 정의 클래스를 구현해서 예외를 발생시킬 수 있습니다.

예제는 다음과 같습니다.

public Object pop() {
    Object obj;

    if (size == 0) {
        throw new EmptyStackException();
    }

    obj = objectAt(size - 1);
    setObjectAt(size - 1, null);
    size--;
    return obj;
}

Chained Exceptions

이렇게 예외를 발생시켜서 Chained Exceptions을 만들 수도 있습니다.

Chained Exceptions은 예외에 대한 응답으로 다른 예외를 발생시키는 걸 말합니다. 한 예외로 인해 다른 예외가 발생함을 아는게 때로는 유용한 정보가 될 수 있습니다.

예를들면 0으로 나눠서 발생하는 ArithmeticException이 일어났다고 생각해봅시다. 이때 근본적 원인은 I/O error 때문에 0으로 나눈 상황이였습니다. 하지만 이 경우 실제적인 예외의 원인을 알 수 없고 발생하는 예외는 ArithmeticException 입니다. 이런 문제를 Chained Exceptions을 통해 해결할 수 있습니다.

예시는 다음과 같습니다.

public class MyChainedException {

    public void main(String[] args) {
        try {
            throw new ArithmeticException("Top Level Exception.")
              .initCause(new IOException("IO cause."));
        } catch(ArithmeticException ae) {
            System.out.println("Caught : " + ae);
            System.out.println("Actual cause: "+ ae.getCause());
        }
    }    
}

initCause() method를 통해 현재 발생한 예외에 원인 에외를 넣을 수 있습니다.

getCause() method를 통해 실제 예외의 원인을 볼 수 있습니다.

Exceptions 장점

Exception을 사용했을 때 프로그램에서의 이점을 다양한 예를 통해 보겠습니다.

장점 1: Separating Error-Handling Code from "Regular" Code

파일을 읽는 프로그램을 만든다고 생각해보겠습니다. 실행 흐름은 다음과 같습니다.

readFile {
    open the file;
    determine its size;
    allocate that much memory;
    read the file into memory;
    close the file;
}

traditional programming에서 코드를 짜는데 error detection과 reporting, handling 이런 코드가 main logic이랑 섞여 있으면 프로그램 실행 흐름을 알기 어렵습니다.

아래와 같이 코드가 만들어 질 것입니다.

errorCodeType readFile {
    initialize errorCode = 0;
    
    open the file;
    if (theFileIsOpen) {
        determine the length of the file;
        if (gotTheFileLength) {
            allocate that much memory;
            if (gotEnoughMemory) {
                read the file into memory;
                if (readFailed) {
                    errorCode = -1;
                }
            } else {
                errorCode = -2;
            }
        } else {
            errorCode = -3;
        }
        close the file;
        if (theFileDidntClose && errorCode == 0) {
            errorCode = -4;
        } else {
            errorCode = errorCode and -4;
        }
    } else {
        errorCode = -5;
    }
    return errorCode;
}

하지만 Exception을 쓰면 이런 흐름들을 뺄 수 있습니다.

readFile {
    try {
        open the file;
        determine its size;
        allocate that much memory;
        read the file into memory;
        close the file;
    } catch (fileOpenFailed) {
       doSomething;
    } catch (sizeDeterminationFailed) {
        doSomething;
    } catch (memoryAllocationFailed) {
        doSomething;
    } catch (readFailed) {
        doSomething;
    } catch (fileCloseFailed) {
        doSomething;
    }
}

장점 2: Propagating Errors Up the Call Stack

Exception의 두번째 특징은 발생한 예외를 현재에서 처리하지 않고 Method Call Stack으로 전파할 수 있다는 것입니다.

Main Program에 다음과 같은 메소드를 호출한다고 생각해봅시다.

method1 {
    call method2;
}

method2 {
    call method3;
}

method3 {
    call readFile;
}

Traditional error-notification techniques에서는 런타임 시스템을 이용해 Exception Handler를 찾지 않고 에러 코드를 리턴해 예외를 처리합니다.

method1 {
    errorCodeType error;
    error = call method2;
    if (error)
        doErrorProcessing;
    else
        proceed;
}

errorCodeType method2 {
    errorCodeType error;
    error = call method3;
    if (error)
        return error;
    else
        proceed;
}

errorCodeType method3 {
    errorCodeType error;
    error = call readFile;
    if (error)
        return error;
    else
        proceed;
}

하지만 자바에서는 현재 예외를 처리할 수 있는 Exception Handler가 없다면 특정 예외를 처리할 수 있는 Handler를 찾기 위해 Call Stack을 역방향으로 검색합니다. 따라서 예외를 처리할 메소드만 정확히 예외를 감지하고 처리하면 됩니다.

method1 {
    try {
        call method2;
    } catch (exception e) {
        doErrorProcessing;
    }
}

method2 throws exception {
    call method3;
}

method3 throws exception {
    call readFile;
}

장점 3: Grouping and Differentiating Error Types

프로그램 내에서 발생하는 모든 예외는 Object입니다. 그러므로 예외를 그룹화하거나 분류할 수 있습니다.

예를들면 Java.io에서 정의한 IOException과 그 하위 클래스가 있습니다.

IOException은 I/O를 수행할 때 일어날 수 있는 모든 예외를 말하며 그 하위 클래스들은 좀 더 구체적인 내용을 말합니다.

IOException의 하위 클래스인 FileNotFoundException은 디스크에서 파일을 찾을 수 없을 때 발생합니다.

이를 이용해 catch statement에 아주 구체적인 예외 클래스를 처리하도록 할 수 있지만 예외 슈퍼 클래스를 지정해 그룹 단위로 예외를 처리하게 할 수 있습니다.

catch (IOException e) {
    // Output goes to System.err.
    e.printStackTrace();
    // Send trace to stdout.
    e.printStackTrace(System.out);
}

이 catch statement은 FileNotFoundException부터 EOFException 등 모든 IOException을 다 처리할 수 있습니다.

throw

throw 키워드는 특정한 시점에 의도적으로 예외를 던지기 위해 사용된다.

public static void test() throws Exception {
    Exception e = new Exception("의도적인 예외");
    throw e;
}

throw를 사용한 시점에서 무조건 예외가 발생한다. 이 예외는 try-catch로 처리하거나 혹은 throws 키워드를 사용하여 메소드를 사용한 곳에 예외를 던져줘야 한다.

throws

throws 키워드를 통해 메서드에 예외를 선언할 수 있다. 여러 개의 메서드를 쉼표로 구분해서 선언할 수 있다.

public void method() throws FileNotFoundException, ParseException, IOException {
	...
}

 

thorws는 메서드 선언부에 예외를 선언해둠으로써 해당 메서드를 사용하는 사람들이 어떤 예외를 처리해야 하는 지를 알려주는 역할을 한다.

 

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

Exception : checked

컴파일러가 예외 처리 여부를 체크한다. 예외 처리 필수

  • IO Exception : 입출력 예외
  • Class Not Found Exception : 클래스가 존재 X

Runtime Exception : unchecked

컴파일러가 예외 처리 여부를 체크 안한다. 예외 처리 선택

프로그래머의 실수로 발생하는 예외

  • Arithmetic Exception : 산술 계산 예외. ex) 0으로 나누기
  • Null Pointer Exception : 널 포인터 예외
  • Class Cast Exception : 형변환 예외
  • Index Out Of Bounds Exception : 배열 번위 벗어난 예외
    • Array Index Out Of Bounds Exception
    • String Index Out Of Bounds Exception
  • IllegalArgument
  • java.lang.IllegalArgumentException은 적합하지 않거나(illegal) 적절하지 못한(inappropriate) 인자를 메소드에 넘겨주었을 때 발생

Spring framework에서 Transaction 설정과 관련하여 Unchecked Exception에 대해 roll-back기능을 지원한다. 이것은 Spring framework의 transaction 설정이 제공하는 것이다. 순수 Java 언어에서 지원하는 기능이 아니다. Java가 제공하는 Unchecked Exception에는 roll-back 기능이 없다.

 

Exception과 Error의 차이는?

Exception

개발자가 구현한 로직(코드)에서 발생한다. 발생할 상황을 미리 예측하여 처리할 수 있다. 예외 처리는 개발자가 할 수 있기 때문에 예외를 구분하고 그에 맞는 처리 방법을 적용해야 한다.

추가로, Exception이 발생하면 Exception Handler가 이를 처리하게 된다. Call stack을 이용하여 역방향으로 Exception Handler를 탐색한다. 탐색하면서 Exception Handler가 처리할 수 있는 Exception 타입인지 체크하고 아니라면 상위로 전달된다.

발생한 Exception에 대해서 처리가 가능한 Handler를 찾을 때 까지 상위로 전달되면서 찾아간다. 적절한 Handler를 찾지 못한다면 JVM까지 전달되고, 최종적으로 JVM이 Exception을 처리하게 된다.

Error

시스템의 비정상적인 상황이 생겼을 때 발생한다. 메모리 부족, 스택오버플로우 등. 시스템 레벨에서 발생하기 때문에 심각한 수준의 오류이다. 개발자가 미리 예측하여 처리할 수 없기 때문에 예외 처리 방법을 적용할 수 없다.

 

RuntimeException과 RE가 아닌 것의 차이는?

자바에서 RuntimeException은 Unchecked Exception으로 RuntimeException이 아닌것은 Checked Exception으로 분류됩니다.

Checked Exception의 경우 예외를 예측하고 복구할 수 있기 때문에 예외를 처리하는 Exception Handler가 필요합니다. 즉, try-catch-finally block을 만들어야 합니다.

RuntimeException은 try-catch-finally block을 만들지 않아도 됩니다.

 

커스텀한 예외 만드는 방법

다음의 조건에 부합된다면 예외 클래스를 만들어서 사용하고 그렇지 않다면 기존의 자바에서 제공하는 예외를 사용하면 됩니다.

  1. Do you need an exception type that isn't represented by those in the Java platform?
  2. if they could differentiate your exceptions from those thrown by classes written by other vendors?
  3. Does your code throw more than one related exception?
  4. If you use someone else's exceptions, will users have access to those exceptions?

Exception과 Runtime Exception 중 선택해서 상속하면 된다.

public class CustomException extend Exception {
  CustomException() { } // 매개 변수 없는 기본 생성자
  CustomException(String msg) {
    super(msg); // 예외 원인 전달하기 위한 String 매개 변수를 갖는 생성자
    						// 상속 받은 Exception Class의 생성자 호출
  }
}

사용자 정의 예외 클래스 이름은 Exception으로 끝나는 것이 좋다. 생성자는 위 코드와 같이 두 개를 선언하는 것이 일반적이다.

public void test_CustomException() throws CustomException {
  throw new CustomException("CustomException 예외를 던집니다.")
}

public static void main(String[] args) {
  try {
    test_CustomException();
  } catch (ArithmeticException e) {
    String message = e.getMessage();
    System.out.print(message);  // "CustomException 예외를 던집니다." 출력
    e.printStackTrace(); //예외의 발생 경로 추적
  }
}

rintStackTrace() : 예외 발생 당시의 호출스택에 있었던 메서드 정보와 예외 메시지를 화면에 출력한다. getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

연결된 예외

그래서 2가 방법으로 사용

  1. 세부적인 사항을 포괄적인 사항으로 포함시킬 때 사용
    • 한 예외가 다른 예외를 발생시킬 수 있다.
  2. cheked 예외를 unchecked 예외로 변경시 사용
    • 예외 A가 예외 B를 발생시키면, A는 B의 원인 예외 (cause exception)

세부적인 사항을 포괄적인 사항으로 포함시킬 때 사용

try {
   install();
} catch(InstallException e { // SpaceException과 MemoryException을 하나로 묶음
	e.printStackTrace();
} catch(Exception e) {
	e.printStackTrace();
}
void install() throws InstallException {
    try {
    	startInstall();		// SpaceException 발생
        copyFiles();
    } catch (SpaceException e) {
    	InstallException ie = new InstallException("설치 중 예외 발생");	// 예외 생성
        ie.initCause(e);	// InstallException의 원인 예외를 SpaceException으로 지정
        throw ie;		// InstallException을 발생시킨다.
    } catch (MemoryException me) {
    	...
    }
}

위 사진과 같이 연결됨으로써 설치중, 공간이 공간이 부족하다는 정보 표시가 된다.

예외 A가 예외 B를 발생시키면, A는 B의 원인 예외 (cause exception)

static void startInstall() throws SpaceException, MemoryException {
    if(!enoughSpace())		// 충분한 설치 공간이 없는 경우
    	throw new SpaceException("설치할 공간이 부족합니다.");
    
    if(!enoughMemory())		// 충분한 메모리가 없는 경우
    	throw new MemoryException("메모리가 부족합니다.");
}

 

위 코드를 아래와 같이 변경

static void startInstall() throws SpaceException {
    if(!enoughSpace())		// 충분한 설치 공간이 없는 경우
    	throw new SpaceException("설치할 공간이 부족합니다.");
    
    if(!enoughMemory())		// 충분한 메모리가 없는 경우
    	throw new RuntimeException(new MemoryException("메모리가 부족합니다.")); // 원인 예외
}

 

Reference1e

728x90

'Java' 카테고리의 다른 글

[Java] Enum  (2) 2023.06.14
[Java] 멀티쓰레드 프로그래밍  (3) 2023.06.09
[Java] 인터페이스(Interface)  (1) 2023.05.16
[Java] 패키지(Package)  (0) 2023.05.11
[Java] 상속(inheritance)  (2) 2023.05.02

댓글