본문 바로가기

Java

10. Exceptions ( 예외 처리 ) - Effective Java 3th

예외처리는 적절하게 사용하면 가독성, 안정성이 올라가지만, 적절하게 사용하지 않으면 오히려 잘못된 길로 가게 된다.

 

Item 69. 예외 상황을 위해서만 예외처리를 하여라.

 

// Horrible abuse of exceptions. Don't ever do this!
try {
  int i = 0;
  while(true)
  	range[i++].climb();
  } catch (ArrayIndexOutOfBoundsException e) {
}

위의 코드는 결국 range 범위를 넘어가면 exception을 발생시킬 것이다. 하지만 이 코드는 결국 아래와 같다.

for (Mountain m : range)
	m.climb();

Exception은 이름 그대로, 예외 상황을 위해서만 사용되어야지 원래 control flow를 위해 사용되어서는 안된다.

 

잘 설계된 API는 client에게 코드흐름에서 exception을 무조건 사용하라고 강요하지 않는다.

상태 의존적인(state-dependent)한 함수는 일반적으로 상태 의존적인 함수가 실행하기 적절한지 판단해주는 상태 테스트(state-testing)를 가지고 있어야 한다. ( 이렇게 말로 하니 어렵다. 아래 예제를 보자 )

 가장 대표적인 예가 Iterator 인터페이스이다.

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
  Foo foo = i.next();
  ...
}
  • hasNext() : state-testing method
  • next() : state-dependent method

만약 state-testing 함수가 없다면 아래와 같이 구현을 해야 할 것이다.

// Do not use this hideous code for iteration over a collection!
try {
  Iterator<Foo> i = collection.iterator();
  while(true) {
    Foo foo = i.next();
    ...
  }
  } catch (NoSuchElementException e) {
}
State-testing method(함수로 체크할 것인가) vs empty optional ( 빈 값을 돌려줄 것인가 )

반환값으로 판단하는게 좋은 경우는 해당 객체가 동시에 접근이 가능한경우 이거나 외부로 부터 상태값이 영향을 받는 경우이다. 왜냐하면 위와 같은 경우는 state-testing 함수가 같은 결과를 반환할 것이라는 100% 보장이 되지 않는다. ( concurrency 문제 )

 

 보통은 state-testing method를 사용하는 것이 선호된다. 가독성이 좋고, 잘못된 경우를 잡기가 더 쉽다. 무슨 말이냐 하면, optional return 값을 실수로 체크하지 않으면, 알수 없는 오류를 맞이할 것이기 때문이다. 

 

Item 70. 프로그래밍 error를 위해서 Runtime exception, 회복을 위해서는 checked exception을 사용하라

자바는 세가지 throwable을 제공한다

  1. checked exception
  2. runtime exception
  3. error

어떤 상황에서 무조건 해당 throwable을 써야한다는 규칙은 없지만, 일반적인 규칙은 있다.

 

Checked exception은 예외 발생시 해당 에러와 관련하여 처리하길 기대하는 경우 사용하라.

 사용자가 recover하기를 기대하는 상황이라면 checked exception을 사용한다.

 사용자가 checked exception을 던지는 API를 사용하게 되면, 그 상황으로부터 회복해야 할 의무를 지니게 된다. 사용자는 그냥 exception을 끝까지 던져버림으로써 무시할 수도 있지만 이는 좋지 않은 방법이다. ( Item 77 참고 )

 

Unchecked Exception : runtime exceptions and errors

일반적으로 unchecked exception이나 error를 뱉는 경우 회복이 불가능하거나 실행이 중지된다.

 

 Runtime exception은 프로그래밍 에러를 나타낼 때 사용한다. Runtime exception은 대부분 전제 조건을 위반한 경우 발생한다. 예를 들어, ArrayIndexOutOfBoundsException과 같은 것이 있다. iterator를 이용하여 precondition을 체크 했다면 발생하지 않지만, 그냥 배열에 접근했다면 프로그래머의 실수에 의해 해당 exception이 일어날 수 도 있다.

 

회복되는 경우도 있고 회복이 되지 않는 경우 내가 어떻게 판단해?

위에서 본 배열과 관련된 exception 상황 중에 일시적으로 자원이 고갈되어 에러가 발생한다면? API 설계자가 자원 고갈과 관련되어 회복할 수 있다고 판단되면 checked exception을 사용하는거고, 아니라면 runtime exception을 사용하는 것이다. 판단하기 애매하다고 생각 된다면 Item 71을 읽어보자.

 

 API를 설계하면서 exception은 우리가 에러에 관한 정보를 모두 담을 수 있는 완벽한 객체라는 것을 잊어서는 안된다. 이러한 exception을 제대로 던지지 않아, 사용자가 에러와 관련되어 정보를 찾아다니는 것은 매우 좋지 않는 프로그래밍 방법이다. ( Item 12 ) 

 

 chekced exception에는 recovery를 위해 필요한 정보를 무조건 담아서 보내주자. 예를 들어, 카드로 상품을 구입하다가 잔고가 부족하여 결제가 실패한 경우, 결제 실패 exception에 얼마가 부족했는지도 담아서 보내주도록 구현을 하자. 이와 관련해서는 Item 75에서 더 자세히 살펴보자.

 

Item 71. Checked exceptions를 쓸데없이 사용하지 마라.

많은 자바 개발자들이 checked exception을 별로 좋아하지 않지만, 적절하게 사용한다면 매우 유용하다. return code와 unchecked exception과 다르게 개발자에게 해당 문제를 처리하도록 강제할 수 있고, 프로그램의 신뢰도를 높여줄 수 있다. 

 

 만약에 내가 개발한 API의 exception을 다른 개발자가 아래와 같이만 다룬다면?

} catch (TheCheckedException e) {
	throw new AssertionError(); // Can't happen!
}

} catch (TheCheckedException e) {
  e.printStackTrace(); // Oh well, we lose.
  System.exit(1);
}

이게 최선이라면 그냥 unchecked exception을 사용하는게 낫다. 

 

만약 checked exception을 제거하고 싶다면 optional을 리턴하라.

exception을 제거해서 뭔가 간단해 보이겠지만, 단점은 exception처럼 에러와 관련된 추가적인 정보를 담지 못한다.

 

그리고 아래 코드처럼 checked exception을 unchecked exception으로 바꿀수도 있다.

// Invocation with checked exception
try {
	obj.action(args);
} catch (TheCheckedException e) {
	... // Handle exceptional condition
}

원래 이랬던 코드를 함수로 나누면

// Invocation with state-testing method and unchecked exception
if (obj.actionPermitted(args)) {
	obj.action(args);
} else {
	... // Handle exceptional condition
}

항상 이 방법이 옳은 것은 아니다. API 사용자 입장에서는 더 쓰기 좋지만, 코드는 위에가 더 이쁘다....(그런가?)

하지만 아래 코드는 Item 69에서 살펴본 것처럼 multi thread와 관련해서 이슈가 있다고 생각하면 사용해서는 안된다.