[JAVA] 예외 처리

SungBum Park·2021년 3월 7일
0

JAVA

목록 보기
9/9
post-thumbnail

Exception(예외)과 Error(에러)의 차이는?


Exception과 Error는 둘 다 자바 프로그램의 비정상적인 상황을 처리하기 위한 객체이다. 따라서 둘 다 Throwable 이라는 객체를 상속하고 있다.

여기서 Error는 애플리케이션이 아닌 시스템 수준에서의 비정상적인 상황을 말한다. 예를 들어, stack overflow 또는 out of memory 에러가 있다. 이는 개발자가 코드를 통해 처리할 수 없는 수준이다.

반면에 Exception은 대부분 애플리케이션에서 발생하는 비정상적인 상황을 말한다. 따라서 충분히 예상가능하며, 이를 코드를 통해 처리할 수 있다.

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


출처: https://madplay.github.io/post/java-checked-unchecked-exceptions

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


위 예외 계층 구조에도 나타나있듯이, exception은 unchecked exception과 checked exception으로 나뉜다.

Unchecked Exception은 이름 그대로 미리 검사하지 않는 예외로 Runtime Exception이다. 즉, 컴파일 단계가 아닌 실행 단계에서 검사하는 예외이다. 이는 Error도 포함하는데, 에러 역시 컴파일 단계에서 알 수 없다. 메모리가 부족하다던가, 스택이 터졌다던가, 쓰레드가 죽었다는 것은 컴파일에서는 알 수 없고, 실제로 실행 중간에 발생하는 문제점들이다. Unchecked Exception도 마찬가지로, 컴파일 단계에서는 알 수 없고, 직접 실행하는 시점에 알 수 있는 문제를 처리하는 객체들이다.

Checked Exception은 unchecked exception과 반대로, 컴파일 단계에서 비정상적인 상황을 처리하는 방식이다. 즉, RuntimeException과는 반대인 것이다. 컴파일 시점에 문제를 처리하기 때문에 반드시 명시적으로 이 문제를 처리하는 코드를 추가해야한다. 이러한 방어 코드가 없으면 컴파일 에러가 발생한다.

Checked Exception VS Unchecked Exception

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


자바에서 Exception을 처리하는 방법은 크게 두 가지가 있다.

  • 예외 처리
  • 예외 전환
  • 예외처리 회피

1. 예외 처리

예외를 처리하는 첫 번째 방법은 예외가 발생한 시점에 바로 방어 코드를 삽입하여 처리하는 것이다. 바로 try-catch 구문을 사용하는 것이다. 이는 try, catch 키워드를 제외한 여러 키워드가 존재하고, 자바 버전이 올라가면서 더 발전된 형태의 try-catch 구문도 나왔다.

또한, try-catch를 통해 어떻게 예외를 처리할지도 여러가지 방법이 존재한다.

먼저, try-catch의 여러가지 사용 방법을 살펴보자.

try-catch

try {
		// 예외가 발생할 수 있는 코드
} catch (Exception e) {
		// 예외 처리
}
  • try 블록은 예외가 발생할 수 있는 코드를 감싼다.
    • 블록은 최대한 코드를 적게 포함하는 것이 좋다. 즉, 예외가 발생할 최소한의 코드를 감싸는 것이 가독성에 좋다.
  • catch 블록은 try 블록에서 발생한 코드를 처리한다.
    • catch 의 매개변수는 예외 처리의 최상위 클래스인 Throwable 을 상속하는 클래스어야 한다.
    • 상위 클래스인 Exception 과 같은 클래스를 사용하기보다는, 가장 하위 클래스를 선언하여 명시적으로 어떤 예외인지 표현하는 것이 좋다.
  • try 블록에서 catch 의 매개변수로 지정한 예외가 발생하지 않으면, catch 블록 내부는 스킵한다.

try-catch-finally

try {
		// 예외가 발생할 수 있는 코드
} catch (Exception e) {
		// 예외 처리
} finally {
		// 예외 발생 여부와 관계없이 try 문 이후 바로 실행되는 코드
}
  • finally 블록은 try 블록 내부에서 예외 발생 여부와 관계없이 실행되는 코드를 담는 블록이다.
    • 예외가 발생하면, catch 블록 이후에 실행
    • 예외가 발생하지 않으면, try 블록 이후에 실행

다중 catch

try {
		// 예외가 발생할 수 있는 코드
} catch (Exception1 e) {
		// Exception1 예외 처리
} catch (Exception2 e) {
		// Exception2 예외 처리
} catch (Exception3 e) {
		// Exception3 예외 처리
}
  • catch 블록은 여러 개를 선언할 수 있다.

자바 7 버전부터는 Multicatch 블록을 제공한다.

try {
		// 예외가 발생할 수 있는 코드
} catch (Exception1 | Exception2 | Exception3 e) {
		// Exception1 or Exception2 or Exception3 예외 처리
}
  • 하나의 catch 문 매개변수에서 | 키워드를 통해 OR을 표현할 수 있다.
  • 여러 예외가 공통적인 로직을 통해 처리된다면, 유용하게 사용할 수 있다.
    • 물론, 여러 예외를 multicatch로 잡아서 내부 블록에서 분기처리로도 구분할 수 있다.
  • 단, 상속관계에 있는 클래스들은 동시에 명시할 수 없다.
    • catch (Throwable | Exception e) 와 같은 구문은 사용할 수 없음

try-with-resources

Stream과 같이 자원을 할당받아서 사용할 때는 명시적으로 close() 를 사용하여 자원을 다시 반납해야 한다. 그래야 불필요한 자원 소모를 줄일 수 있기 때문이다. 그리고 이러한 객체는 특히 여러 exception이 발생하여 처리를 해줘야 하는 경우가 대부분이다.

그렇다면, 예외가 발생하면 close() 는 어떻게 호출할까? 바로 finally 블록에 명시하면 된다. 하지만, 이러한 작업은 단순 반복이며, 실수할 확률이 있다.

이를 해결하기 위해 자바 7버전부터는 try-with-resources 를 지원한다.

try (Resource s = new Resource(...)) {
	// 자원을 사용하여 처리하는 코드
} catch (Exception e) {
	// 예외 처리
}
  • try 키워드에 매개변수로 자원 관련된 객체를 선언할 수 있다. (아래와 같이 여러 객체를 선언할 수도 있다.)
try (Resource1 s1 = new Resource(...);
	Resource2 s2 = new Resource(...)) {
	// ...
} ...
  • close()AutoCloseable 를 구현한 객체에 한해서 자동으로 호출해준다.
    • AutoClosable 역시 자바 7 버전 이후로 추가되었다.
    • 자바 내부에서 제공하는 자원 관련 클래스 대부분은 이를 구현하고 있다.

지금까지는 예외를 어떤 문법으로 처리할 수 있는지 살펴보았다. 여기서 더 중요한 사실은 이러한 방법을 통해 어떤 방식으로 예외를 처리할 지이다. 예외 처리에 정답은 없지만, 최대한 효율적이고 각자 상황에 맞는 필요한 방식을 사용해야 한다. try-catch 문을 사용한다고 해서, 예외를 잘 처리했다고 보기는 힘들다.

예외 처리의 중요한 점은 예외를 방어 코드에서 처리할 수 있는 경우를 제외하면, 어디서 예외가 발생했는지 정확히 보여주는 것이다.

2. 예외 전환

예외 전환은 말 그대로 다른 예외로 전환하는 것을 말한다. 대표적으로 checked exception을 unchecked exception으로 변경하여 던지는 경우가 있다.

try {
		// ...
} catch (SQLException e) {
		throw new IllegalArgumentException();
}
  • 예외 전환은 throw 키워드를 사용한다.
  • 커스텀한 unchecked exception을 던지는 경우가 많다.

3. 예외처리 회피

예외처리 회피는 예외가 발생한 시점에 처리를 하지 않고, 호출한 메서드로 예외 처리 책임을 넘기는 것을 말한다.

public void findById(Long id) throws SQLException {
		// ...
}
  • 메서드에 throws 키워드를 통해 어떤 예외를 넘길 지 명시한다.
  • 예외가 발생하면, 현재 메서드가 아닌 호출한 메서드에서 처리해야 한다.
    • 특히 checked exception 인 경우 호출한 쪽에서 반드시 처리해야 한다.
  • 예외처리 회피는 정말 필요한 경우에 사용하는 것이 좋다.

커스텀한 예외 만드는 방법


커스텀한 예외를 만드는 경우는 좀 더 명시적으로 예외를 표현하고 싶을 때나 내부에서 좀 더 로직을 처리하고 싶은 경우가 있을 것이다.

Unchecked Exception을 만들고 싶을 때는 RuntimeException 을 상속받고,

Checked Exception을 만들고 싶을 때는 Exception 을 상속 받아 커스텀하게 만들 수 있다.

커스텀 예외를 만들 때 주의할 점은 이전 예외 정보를 잃어버리지 않도록 해야 한다. 이를 위해 이전 예외를 함께 저장하는 생성자를 호출하여 사용하는 것을 추천한다.(아래 예제 참고)

Unchecked Exception 커스텀 예외 만들기

public class CustomUncheckedException extends RuntimeException {
    public CustomUncheckedException(String message, Throwable cause) {
        super(message, cause);
    }
}

Checked Exception 커스텀 예외 만들기

public class CustomUncheckedException extends RuntimeException {
    public CustomUncheckedException(String message, Throwable cause) {
        super(message, cause);
    }
}

EFFECTIVE JAVA (3/E)


예외는 진짜 예외 상황에만 사용하라.

try {
	int i = 0;
    while(true)
    	range[i++].climb();
} catch (ArrayIndexOutOfBoundException e) {
}
  • 위는 표준 관용구 대신 성능 향상을 위해 예외를 활용한 매우 극단적인 예제이다.(심지어, JVM 최적화로 인해 표준 관용구보다 느리다.)

예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다.

잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
예를 들어, Iterator 인터페이스는 상태 의존적 메서드와 상태 검사 메서드를 제공하여 클라이언트가 표준 관용구에서 예외 검사없이 사용할 수 있도록 한다.

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
	Foo foo = i.next();
    	// ...
}
  • next(): 상태 의존적 메서드
  • hasNext(): 상태 검사 메서드

복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라.

자바는 문제 상황을 알리는 타입(throwable)을 다음과 같이 제공한다.

검사 예외(Checked Exception)

검사 예외는 호출하는 쪽에서 복구하리라 여겨지는 상황에 사용한다. (검사 예외와 비검사 예외를 나누는 기본적인 규칙) 이는 일반적으로 복구할 수 있는 조건일 때 발생한다.

비검사 예외(Unchecked Exception)

비검사 예외는 프로그램에서 잡을 필요가 없거나 혹은 통상적으로 잡지 말아야 하는 예외이다. 이는 복구가 불가능하거나 더 실행해봐야 득보다 실이 많다는 뜻이다. 비검사 예외는 런타임 예외와 에러 두 가지로 나뉜다.

런타임 예외: 프로그래밍 오류를 나타낼 때 사용한다.

에러: 보통 JVM의 자원 부족, 불변식 깨짐 등 더 이상 수행을 계속할 수 없을 때 사용한다. 일반적으로 개발자가 직접 Error 클래스를 상속하는 것은 자제해야 한다.

비검사 예외를 만들 때 주의할 것은 반드시 RuntimeException 의 하위 클래스로 만들어야 한다는 점이다.(다른 클래스를 상속하는 하위 클래스도 있지만, 대부분 추천하지 않는다.)

참고자료


profile
https://parker1609.github.io/ 블로그 이전

0개의 댓글