Java 예외 처리

소개

예외는 프로그램 실행 중 발생할 수 있는 오류 이벤트로, 정상적인 흐름을 방해합니다. 자바는 자바 예외 처리라고도 알려진 강력하고 객체지향적인 예외 상황 처리 방법을 제공합니다.

자바에서 예외는 사용자가 잘못된 데이터를 입력하거나 하드웨어 오류, 네트워크 연결 실패, 또는 데이터베이스 서버 다운과 같은 다양한 상황에서 발생할 수 있습니다. 특정 예외 상황에서 어떻게 처리할지 지정하는 코드를 예외 처리라고 합니다.

예외 던지기와 예외 잡기

자바는 문장 실행 중에 오류가 발생하면 예외 객체를 생성합니다. 예외 객체에는 메소드 계층 구조, 예외가 발생한 라인 번호, 예외의 유형과 같은 디버깅 정보가 포함되어 있습니다.

메소드에서 예외가 발생하면 예외 객체를 생성하여 런타임 환경에 전달하는 과정을 “예외를 던지는 것”이라고 합니다. 프로그램의 정상 흐름이 중단되고 Java 런타임 환경 (JRE)은 예외 처리기를 찾으려고 시도합니다. 예외 처리기는 예외 객체를 처리할 수 있는 코드 블록입니다.

  • 예외 처리기를 찾는 논리는 오류가 발생한 메소드에서 검색을 시작합니다.
  • 적절한 처리기가 발견되지 않으면 호출자 메소드로 이동합니다.
  • 계속해서 진행됩니다.

따라서 메소드의 호출 스택이 A->B->C이고 메소드 C에서 예외가 발생하면 적절한 처리기를 찾는 검색은 C->B->A로 이동합니다.

적절한 예외 처리기가 발견되면 예외 객체가 처리기로 전달됩니다. 처리기는 “예외를 잡는 것”이라고 합니다. 적절한 예외 처리기가 없으면 프로그램이 종료되고 예외에 대한 정보가 콘솔에 출력됩니다.

Java 예외 처리 프레임워크는 런타임 오류만 처리하기 위해 사용됩니다. 컴파일 타임 오류는 코드를 작성하는 개발자가 수정해야하며, 그렇지 않으면 프로그램이 실행되지 않습니다.

Java 예외 처리 키워드

자바는 예외 처리를 위한 특정 키워드를 제공합니다.

  1. throw – 오류가 발생하면 예외 객체가 생성되고 자바 런타임은 예외를 처리하기 위해 처리를 시작합니다. 때로는 코드에서 명시적으로 예외를 발생시키고자 할 수도 있습니다. 예를 들어, 사용자 인증 프로그램에서 비밀번호가 null인 경우 클라이언트에게 예외를 발생시켜야 합니다. throw 키워드는 예외를 런타임에 던져서 처리하도록 합니다.
  2. throws – 메소드에서 예외를 던지고 처리하지 않을 경우, 메소드 선언부에 throws 키워드를 사용하여 호출하는 프로그램에 해당 메소드에서 발생할 수 있는 예외를 알려야 합니다. 호출하는 메소드에서는 이러한 예외를 처리하거나 throws 키워드를 사용하여 호출자 메소드로 전파할 수 있습니다. throws 절에는 여러 예외를 제공할 수 있으며, main() 메소드와 함께 사용할 수도 있습니다.
  3. try-catch – 코드에서 예외 처리를 위해 try-catch 블록을 사용합니다. try는 블록의 시작이고 catchtry 블록의 끝에 위치하여 예외를 처리합니다. try 블록에는 여러 개의 catch 블록을 가질 수 있습니다. try-catch 블록은 중첩될 수도 있습니다. catch 블록은 Exception 타입의 매개변수를 필요로 합니다.
  4. 마침내finally 블록은 선택 사항이며 try-catch 블록과 함께 사용할 수 있습니다. 예외가 실행 프로세스를 중단하므로 닫히지 않을 수 있는 일부 리소스가 있을 수 있습니다. 이 때 finally 블록을 사용할 수 있습니다. finally 블록은 예외가 발생했든 발생하지 않았든 항상 실행됩니다.

예외 처리 예제

ExceptionHandling.java
package com.journaldev.exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandling {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try {
			testException(-5);
			testException(-10);
		} catch(FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("Releasing resources");
		}
		testException(15);
	}

	public static void testException(int i) throws FileNotFoundException, IOException {
		if (i < 0) {
			FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
			throw myException;
		} else if (i > 10) {
			throw new IOException("Only supported for index 0 to 10");
		}
	}
}
  • testException() 메서드는 throw 키워드를 사용하여 예외를 throw합니다. 메서드 시그니처는 throws 키워드를 사용하여 호출자에게 throw할 수 있는 예외의 타입을 알려줍니다.
  • main() 메서드에서는 try-catch 블록을 사용하여 예외를 처리합니다. 처리하지 않을 경우에는 main() 메서드의 throws 절을 사용하여 런타임으로 전파합니다.
  • testException(-10)은 예외 때문에 실행되지 않고 finally 블록이 실행됩니다.

printStackTrace()는 디버깅 목적으로 Exception 클래스에서 유용한 메서드 중 하나입니다.

이 코드는 다음을 출력합니다:

Output
java.io.FileNotFoundException: Negative Integer -5 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread "main" java.io.IOException: Only supported for index 0 to 10 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)

주의해야 할 몇 가지 중요한 점:

  • try 문 없이는 catch 또는 finally 절을 가질 수 없습니다.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • try-catch-finally 블록 사이에는 어떤 코드도 작성할 수 없습니다.
  • try 문 하나에 여러 개의 catch 블록을 가질 수 있습니다.
  • try-catch 블록은 if-else 문과 유사하게 중첩될 수 있습니다.
  • try-catch 문에는 finally 블록 하나만 있을 수 있습니다.

Java 예외 계층

이전에 언급한 대로, 예외가 발생하면 예외 객체가 생성됩니다. Java 예외는 계층적이며 상속을 사용하여 다른 유형의 예외를 분류합니다. Throwable은 Java 예외 계층의 부모 클래스이며 두 개의 하위 객체인 ErrorException이 있습니다. Exception은 Checked Exception과 Runtime Exception으로 나뉩니다.

  1. 에러: Error는 응용 프로그램의 범위를 벗어난 예외적인 상황으로, 예측하고 복구할 수 없습니다. 예를 들어, 하드웨어 장애, Java 가상 머신 (JVM) 충돌 또는 메모리 부족 오류입니다. 따라서 별도의 Error 계층이 있으며 이러한 상황을 처리하지 않아야 합니다. 일반적인 Error로는 OutOfMemoryErrorStackOverflowError가 있습니다.
  2. 확인된 예외: 확인된 예외는 프로그램에서 예상할 수 있는 예외적인 상황이며, 이를 복구하려고 시도합니다. 예를 들어, FileNotFoundException입니다. 이 예외를 catch하여 사용자에게 유용한 메시지를 제공하고 디버깅 목적으로 적절하게 로그에 기록해야 합니다. Exception은 확인된 예외들의 부모 클래스입니다. 확인된 예외를 던지는 경우, 동일한 메소드에서 catch해야 하거나 throws 키워드를 사용하여 호출자에게 전파해야 합니다.
  3. 런타임 예외: 런타임 예외는 잘못된 프로그래밍으로 인해 발생합니다. 예를 들어, 배열에서 요소를 검색하려고 시도하는 경우 배열의 길이를 먼저 확인해야 합니다. 그렇지 않으면 런타임에서 ArrayIndexOutOfBoundException이 발생할 수 있습니다. RuntimeException은 모든 런타임 예외들의 부모 클래스입니다. 메소드에서 런타임 예외throw하는 경우, 메소드 선언부의 throws 절에 지정할 필요가 없습니다. 더 나은 프로그래밍으로 런타임 예외를 피할 수 있습니다.

예외 클래스의 유용한 메소드들

Java Exception 및 그 하위 클래스는 특정한 메소드를 제공하지 않으며, 모든 메소드는 기본 클래스인 Throwable에 정의되어 있습니다. Exception 클래스는 다른 종류의 Exception 시나리오를 지정하기 위해 생성되었으므로, 우리는 쉽게 원인을 식별하고 해당 유형에 따라 Exception을 처리할 수 있습니다. Throwable 클래스는 상호 운용성을 위해 Serializable 인터페이스를 구현합니다.

Throwable 클래스의 유용한 일부 메소드는 다음과 같습니다:

  1. public String getMessage() – 이 메소드는 Throwable의 메시지 String을 반환하며, 예외를 생성할 때 생성자를 통해 메시지를 제공할 수 있습니다.
  2. public String getLocalizedMessage() – 이 메소드는 하위 클래스가 호출하는 프로그램에 로케일별 메시지를 제공하기 위해 재정의할 수 있도록 제공됩니다. 이 메소드의 Throwable 클래스 구현은 예외 메시지를 반환하기 위해 getMessage() 메소드를 사용합니다.
  3. public synchronized Throwable getCause() – 이 메소드는 예외의 원인을 반환하거나 원인이 알려지지 않은 경우 null을 반환합니다.
  4. public String toString() – 이 메소드는 Throwable에 대한 정보를 String 형식으로 반환하며, 반환된 StringThrowable 클래스의 이름과 로케일별 메시지를 포함합니다.
  5. public void printStackTrace() – 이 메서드는 스택 추적 정보를 표준 오류 스트림에 출력합니다. 이 메서드는 오버로드되어 있으며, 우리는 PrintStream 또는 PrintWriter를 인수로 전달하여 스택 추적 정보를 파일이나 스트림에 작성할 수 있습니다.

Java 7 자동 리소스 관리 및 Catch 블록 개선

한 번의 try 블록에서 많은 예외를 catch하는 경우, catch 블록 코드는 대부분 오류를 기록하기 위한 중복 코드로 구성되는 것을 알 수 있습니다. Java 7에서 개선된 catch 블록이 도입된 기능 중 하나는 한 번의 catch 블록에서 여러 예외를 처리할 수 있다는 것입니다. 이 기능을 사용한 catch 블록의 예는 다음과 같습니다:

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

예외 객체가 final이며 catch 블록 내에서 수정할 수 없다는 제약 사항이 있으며, 자세한 분석은 Java 7 Catch 블록 개선에서 읽을 수 있습니다.

대부분의 경우, 리소스를 닫기 위해 finally 블록을 사용합니다. 때로는 이를 닫는 것을 잊고 리소스가 고갈될 때 런타임 예외가 발생합니다. 이러한 예외는 디버그하기 어렵고, 해당 리소스를 사용하는 각 위치를 조사하여 닫았는지 확인해야 할 수도 있습니다. 자바 7에서는 try-with-resources라는 개선 사항이 도입되었습니다. 이를 사용하면 리소스를 try 문 자체에서 생성하고 try-catch 블록 내에서 사용할 수 있습니다. 실행이 try-catch 블록을 벗어나면 런타임 환경에서 이러한 리소스를 자동으로 닫습니다. 다음은 이 개선 사항을 사용한 try-catch 블록의 예입니다:

try (MyResource mr = new MyResource()) {
	System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
	e.printStackTrace();
}

A Custom Exception Class Example

자바는 우리가 사용할 수 있는 많은 예외 클래스를 제공하지만, 때로는 사용자 정의 예외 클래스를 만들어야 할 수도 있습니다. 예를 들어, 적절한 메시지와 함께 특정 유형의 예외를 호출자에게 알리기 위해 사용자 정의 예외 클래스를 만들 수 있습니다. 에러 코드와 같은 추적을 위한 사용자 정의 필드를 가질 수도 있습니다. 예를 들어, 텍스트 파일만 처리하는 메서드를 작성한다고 가정해 봅시다. 그렇기 때문에 다른 유형의 파일이 입력으로 전송될 때 적절한 에러 코드를 호출자에게 제공할 수 있습니다.

먼저, MyException을 생성하세요:

MyException.java
package com.journaldev.exceptions;

public class MyException extends Exception {

	private static final long serialVersionUID = 4664456874499611218L;

	private String errorCode = "Unknown_Exception";

	public MyException(String message, String errorCode) {
		super(message);
		this.errorCode=errorCode;
	}

	public String getErrorCode() {
		return this.errorCode;
	}
}

그런 다음, CustomExceptionExample을 생성하세요:

CustomExceptionExample.java
package com.journaldev.exceptions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class CustomExceptionExample {

	public static void main(String[] args) throws MyException {
		try {
			processFile("file.txt");
		} catch (MyException e) {
			processErrorCodes(e);
		}
	}

	private static void processErrorCodes(MyException e) throws MyException {
		switch (e.getErrorCode()) {
			case "BAD_FILE_TYPE":
				System.out.println("Bad File Type, notify user");
				throw e;
			case "FILE_NOT_FOUND_EXCEPTION":
				System.out.println("File Not Found, notify user");
				throw e;
			case "FILE_CLOSE_EXCEPTION":
				System.out.println("File Close failed, just log it.");
				break;
			default:
				System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
				e.printStackTrace();
		}
	}

	private static void processFile(String file) throws MyException {
		InputStream fis = null;

		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
		} finally {
			try {
				if (fis != null) fis.close();
			} catch (IOException e) {
				throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
			}
		}
	}
}

우리는 다른 메서드에서 얻은 다른 유형의 에러 코드를 처리하기 위해 별도의 메서드를 가질 수 있습니다. 그 중 일부는 사용자에게 알리지 않기 위해 소비되고, 일부는 문제를 사용자에게 알리기 위해 다시 던질 것입니다.

여기서는 Exception을 확장하여 이 예외가 발생할 때마다 해당 메소드에서 처리되거나 호출자 프로그램에 반환되어야 합니다. RuntimeException을 확장하면 throws 절에 명시할 필요가 없습니다.

이것은 설계 결정이었습니다. 확인된 Exception을 사용하는 것은 예외를 기대하고 적절한 조치를 취하여 처리하는 데 개발자를 지원하는 장점이 있습니다.

Java에서 예외 처리에 대한 모범 사례

  • 구체적인 예외 사용하기 – 예외 계층의 기본 클래스는 유용한 정보를 제공하지 않으므로 Java에는 IOException와 같은 예외 클래스와 그 하위 클래스인 FileNotFoundException, EOFException 등이 많이 있습니다. 항상 구체적인 예외 클래스를 throw하고 catch하여 호출자가 예외의 근본 원인을 쉽게 파악하고 처리할 수 있도록 해야 합니다. 이렇게 하면 디버깅이 쉬워지며 클라이언트 응용 프로그램이 예외를 적절하게 처리할 수 있게 도와줍니다.
  • 조기에 예외 발생하기 또는 Fail-Fast – 예외를 가능한 한 빨리 throw하려고 해야 합니다. 위의 processFile() 메소드를 고려해보면 이 메소드에 null 인수를 전달하면 다음과 같은 예외가 발생합니다:
Output
Exception in thread "main" java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

디버깅 중에는 예외의 실제 발생 위치를 확인하기 위해 스택 추적을 주의 깊게 살펴봐야 합니다. 아래와 같이 구현 로직을 변경하여 이러한 예외를 빠르게 확인할 수 있습니다:

private static void processFile(String file) throws MyException {
	if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");

	// ... 추가 처리
}

그러면 예외 스택 추적은 예외가 발생한 위치를 명확하게 나타내는 메시지를 표시합니다:

Output
com.journaldev.exceptions.MyException: File name can't be null at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
  • 지연된 catch – Java에서는 체크된 예외를 처리하거나 메소드 시그니처에 선언해야 합니다. 때로는 예외를 catch하고 오류를 로그에 기록하는 경향이 있습니다. 그러나 이러한 방식은 호출자 프로그램이 예외에 대한 알림을 받지 못하기 때문에 해로울 수 있습니다. 예외를 적절히 처리할 수 있는 경우에만 예외를 catch해야 합니다. 예를 들어, 위의 메소드에서는 예외를 호출자 메소드로 다시 throw하여 처리하도록 합니다. 동일한 메소드는 예외를 다른 방식으로 처리하고자 하는 다른 애플리케이션에서 사용될 수 있습니다. 어떤 기능을 구현할 때는 항상 예외를 호출자에게 다시 throw하고 그들이 처리하는 방법을 결정하도록 해야 합니다.
  • 리소스 닫기 – 예외는 프로그램의 처리를 중단시키므로, 자원을 모두 finally 블록에서 닫거나 Java 7의 try-with-resources 기능을 사용하여 Java 런타임이 자동으로 닫도록 해야 합니다.
  • 예외 로깅 – 우리는 항상 예외 메시지를 로그에 기록해야하며, 예외를 throw 할 때 명확한 메시지를 제공하여 호출자가 왜 예외가 발생했는지 쉽게 알 수 있도록 해야합니다. 우리는 항상 예외를 소비하고 디버깅을 위한 예외에 대한 의미있는 세부 정보를 제공하지 않는 빈 catch 블록을 피해야합니다.
  • 다중 예외에 대한 단일 catch 블록 – 대부분의 경우, 예외 세부 정보를 기록하고 사용자에게 메시지를 제공합니다. 이 경우 Java 7 기능을 사용하여 단일 catch 블록에서 여러 예외를 처리해야합니다. 이 접근 방식은 코드 크기를 줄이고 더 깔끔하게 보일 것입니다.
  • 사용자 정의 예외 사용 – 설계 시에 예외 처리 전략을 정의하는 것이 항상 좋습니다. 예외를 throw하고 catch하는 대신, 오류 코드와 함께 사용자 정의 예외를 생성하고 호출 프로그램에서 이러한 오류 코드를 처리할 수 있습니다. 또한 다른 오류 코드를 처리하고 사용하는 유틸리티 메소드를 생성하는 것도 좋은 아이디어입니다.
  • 명명 규칙과 패키징 – 사용자 정의 예외를 생성할 때, 예외 클래스임을 이름 자체에서 알 수 있도록 끝에 Exception이라는 단어를 붙여야합니다. 또한 JDK (Java Development Kit)에서 수행되는 대로 패키징해야합니다. 예를 들어, IOException은 모든 IO 작업에 대한 기본 예외입니다.
  • 예외를 신중하게 사용하십시오 – 예외는 비용이 많이 들며 때로는 예외를 던질 필요가 전혀 없을 수도 있으며, 호출 프로그램에 작업이 성공했는지 여부를 나타내는 부울 변수를 반환할 수 있습니다. 이는 작업이 선택적인 경우에 유용하며, 실패로 인해 프로그램이 멈추는 것을 원하지 않을 때 유용합니다. 예를 들어, 제3자 웹 서비스에서 데이터베이스의 주식 시세를 업데이트하는 동안 연결이 실패하면 예외를 던지지 않도록 피할 수 있습니다.
  • 던져진 예외에 대해 문서화하기 – 메소드에서 던져지는 예외를 명확히 지정하기 위해 Javadoc @throws를 사용하십시오. 다른 애플리케이션이 사용할 수 있는 인터페이스를 제공하는 경우 매우 유용합니다.

결론

이 문서에서는 Java에서의 예외 처리에 대해 배웠습니다. throwthrows에 대해 알게 되었습니다. 또한 try (그리고 try-with-resources), catch, 그리고 finally 블록에 대해 알게 되었습니다.

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java