예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다. 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
try {
int i = 0;
while(true) {
range[i++].climb()
}
} catch (ArrayIndexOutOfBoundsException e) {
}
해당 코드의 문제점
첫 번째 예시에서는 왜 Exception을 던져줬을까? 바로 JVM에서 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데, 일반적인 반복문도 배열 경계에 도달하면 종료된다는 희망에 던져졌을 것이라고, 책에서 말해준다. 하지만, 책에서는 세가지 면에서 잘못된 추론이라고 한다.
- 예외는 예외 상황에 쓸 용도로 설계되었으므로 예외에서는 최적화에 별로 신경 쓰지 않았을 가능성이 크다.
- 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한 된다.
- 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않는다. JVM이 알아서 최적화해 없애준다.
해당 코드를 다시 리팩토링 한다면,
for (Mountain m : range) {
m.climb();
}
아래와 같은 장점을 가지게 된다.
요번 아이템은 바로 장점 중에 두번째에 해당하는 Exception에 올바른 쓰임에 대해 2가지 항목으로 정리하며 끝내고 있다.
throw, try-catch를 이용하여 예외처리를 강제화한다.RuntimeException을 상속 받아야한다.Error는 상속 받지 말아야할 뿐만아니라, 직접 throw 하는 일도 없어야한다. (AssertionError 제외)Exception, RuntimeException, Error를 상속하지 않은 throwble은 만들지 말자뭘 사용할지 모르겠다면 아마 비검사 예외를 사용하는 것이 더 나을 것이다 (아이템 71 참고)
Error 클래스를 상속해 하위 클래스를 만드는 일 → 하지말자Exception, RuntimeException, Error를 상속하지 않은 throwble → 만들지 말자(1) try/catch 블록, throws
public class Example {
public void example() {
try {
} catch (IOException e) {
}
try {
} catch (IOException e) {
}
try {
} catch (IOException e) {
}
...
}
}
(2) Java 8, Stream
public class Test {
public static void main(String[] args) {
String[] names = {"자바", "파이썬", "고", "Trigger"};
Arrays.stream(names)
.map(Example::verifyName) // <--- compile error
.collect(toSet());
}
public static class Example {
public String verifyName(String name) throws Exception {
if (name == "trigger") {
throw new Exception("fail");
}
return name;
}
}
}
(1) Optional
Long userId = 1L;
try {
User user = userService.findUserById(userId);
} catch (Exception e) {
log.error("fail to find user, userId: {}", userId)
user = new User(userId);
}Long userId = 1L;
User user = repository.findUserById(userId)
.orElseGet(() -> new User(userId)); (2) 메서드를 두 개로 분할하여 비검사 예외로 바꾼다.
이 방식에서 첫 번째 메서드는 예외가 던져질지 여부를 boolean 값으로 반환한다.
예시
try {
obj.action(args);
} catch (TheCheckedException e) {
... // 예외 상황에 대처한다.
} if(obj.actionPermitted(args)){
obj.action(args);
} else {
... // 예외 상황에 대처한다.
}하지만 외부 요안에 의하여 상태가 변할 수 있다면, 이 방법은 적절치 않다.
- 꼭 필요한 곳에만 사용한다면, 검사 예외는 프로그램의 안정성을 높여준다. 하지만 남용하면 쓰기 어려운 API 를 낳는다.
- 예외 상황에서 복구할 방법이 없다면 비검사 예외를 던지자.
- 복구가 가능하고 호출자가 그 처리를 해주길 바란다면, 우선 옵셔널을 반환해도 될지 고민하자.
- 옵셔널만으로는 상황을 처리하기에 충분한 정보를 제공할 수 없을 때만 검사 예외를 던지자.
자바 라이브러리는 대부분 API 에서 쓰기에 충분한 수의 예외를 제공한다.
표준 예외를 재사용하면 얻는 게 많다.
호출자가 인수로 부적절한 값을 넘길 때 던지는 예외
예시
public class MainRunner {
List<Integer> list = new ArrayList<>();
public void positiveAdd(Integer number) {
if (number < 0) {
throw new IllegalArgumentException("음수값은 허용하지 않음");
}
list.add(number);
}
public static void main(String[] args) {
MainRunner mainRunner = new MainRunner();
mainRunner.positiveAdd(-1);
}
}
대상 객체의 상태가 호출된 메서드를 수행하기에 적합하지 않을 때 주로 사용한다.
예시
public class MainRunner {
List<Integer> list = new ArrayList<>();
public void evenIndexAdd(Integer number){
if(list.size()%2 != 0){
throw new IllegalStateException("짝수 인덱스에 들어갈 준비가 되지 않음");
}
list.add(number);
}
public static void main(String[] args) {
MainRunner mainRunner = new MainRunner();
mainRunner.evenIndexAdd(0);
mainRunner.evenIndexAdd(1);
}
}
null 값을 허용하지 않는 메서드에 null 을 건네는 경우 사용한다.
예시
public class MainRunner {
List<Integer> list = new ArrayList<>();
public void add(Integer number){
if(Objects.isNull(number)){
throw new NullPointerException("값이 null 입니다.");
}
list.add(number);
}
public static void main(String[] args) {
MainRunner mainRunner = new MainRunner();
mainRunner.add(null);
}
}
시퀀스의 허용범위를 넘는 값을 건널때 사용한다.
예시
public class MainRunner {
List<Integer> list = new ArrayList<>();
public void add(Integer number){
if(list.size()>1){
throw new IndexOutOfBoundsException("요소를 3개 이상 설정 할 수 없습니다.");
}
list.add(number);
}
public static void main(String[] args) {
MainRunner mainRunner = new MainRunner();
mainRunner.add(1);
mainRunner.add(2);
mainRunner.add(3);
}
}
단일 스레드에서 사용하려고 설계한 객체를 여러 스레드가 동시에 수정하려 할 때 사용함.
클라이언트가 요청한 동작을 대상 객체가 지원하지 않을 때 던진다. 대부분 객체는 자신이 정의한 메서드를 모두 지원하니 흔히 쓰이는 예외는 아니다.
상위 계층에서 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던지는것
try {
...
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
/**
* ...
* @throws IndexOutOfBoundsException ...
*/
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: " + index);
}
}
예외 연쇄를 사용해서 번역할 수도 있다.
예외 연쇄 : 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식
Throwable.getCause()를 통해 언제든 저수준 예외를 꺼내볼 수 있다
저수준 예외가 디버깅에 도움을 줄 수 있다
try {
...
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
class HigherLevelException extends Exception {
HigherLevelException(Throwable cause) {
super(cause);
}
}
하지만 예외 번역을 남용해서는 안된다.
가능하다면 저수준 메서드가 반드시 성공하도록 하여 아래 계층에서는 예외가 발생하지 않도록 하는 것이 최선이다
어떻게? 상위 계층 메서드의 매개변수 값을 하위 계층 메서드로 건네기 전에 미리 검사해서 예외가 발생할 만한 매개변수를 하위 계층 메서드로 전달되지 않게끔 해서.
public void function1(...) {
//미리 검사
function2(...);
}
하위 계층에서의 예외를 피할 수 없다면?
상위 계층에서 조용히 처리하여 예외를 API 클라이언트에까지 전파하지 않고 로그를 남길 수도 있다
이렇게 하면 클라이언트 코드에 문제를 전파하지 않음 + 로그분석을 통해 조치를 취할 수 있음
public void function1(...) {
...
try {
function2(...);
} catch(...) {
//로그 기록
}
}
체크 예외는 항상 개별적으로 선언하라, 또한 어떠한 조건에서 예외가 발생하는지 기재하라
@throws 태그를 달아서 예외가 발생하는 조건을 설명하면 된다.Exception, Throwable 이 가장 대표적임)Exception 이나 Throwable 이 있다.언체크 예외는 다룰 수가 없다. 어떻게 문서화 해야할까?
Javadoc의 @throws 태그를 사용하여 체크 예외에 대한 설명을 하라, 다만 언체크 예외에 throws 키워드를 사용하면 안된다.
@throws 태그로 명시하였다면, 예외가 체크되지 않았다는 강력한 시각적 암시를 나타낸다.하나의 클래스의 여러 메서드가 던지는 예외가 같은 이유로 설명된다면, 클래스 수준의 문서화 주석을 남겨라
NullpointerException 일 것이다. 예시를 살펴보자.
public static void main(String[] args) {
Member member = Member.create("kjj", "재준", 99);
}
누군가 만든 Member.create() 호출하여 새로운 회원을 생성하고자 했을때

만약 다음과 같은 메세지를 만나게 된다면 해당 메서드를 사용한 클라이언트는 왜 오류가나는지 전혀 알수없다.
결국에는 소스코드를 타고들어가서 예외를 발생시키는 원인을 분석하게된다.
이러한 행동은 결국 나의 리소스를 낭비하게 됩니다.
public static void main(String[] args) {
Member member = Member.create("kjj", "재준", 99);
}
Member.create() 을 호출했을때 예외가 발생한다면

이처럼 메세지, 대상필드, 입력값 등을 포함하여 예외를 발생시킨다면 다음과 같은 장점이 존재합니다.
호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 하는 메서드불변 객체로 만들기유효성을 검사하기public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
실패 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 앞에 배치하기임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체와 교체하는 것실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법실패 원자성은 일반적으로 권장되는 덕목이지만 항상 달성할 수 있는 것은 아니다. 예를 들어 두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있다.
따라서 ConcurrentModificationException을 잡아냈다고 해서 그 객체가 여전히 쓸 수 있는 상태라고 가정해서는 안 된다.
한편, Error는 복구할 수 없으므로 AssertionError에 대해서는 실패 원자적으로 만들려는 시도조차 할 필요가 없다.
실패 원자적으로 만들 수 있더라도 항상 그리 해야 하는건 아니다. 실패 원자성을 달성하기 위한 비용이나 복잡도가 아주 큰 연산도 있기 때문이다. 그래도 문제가 무엇인지 알고 나면 실패 원자성을 공짜로 얻을 수 있는 경우가 더 많다.
메서드 명세에 기술한 예외라면 설혹 예외가 발생하더라도 객체의 상태는 메서드 호출 전과 똑같이 유지돼야 하는 것이 기본 규칙이다. 이 규칙을 지키지 못한다면 실패 시의 객체 상태를 API 설명에 명시해야 한다. 이것이 이상적이나, 아쉽게도 지금의 API 문서 상당 부분이 잘 지키지 않고 있다.
API 설계자가 메서드 선언에 예외를 명시하는 까닭은 그 메서드를 사용할 때 적절한 조치를 취해달라고 말하는 것
→ 예외를 무시하는 것은 화재 경보를 무시하는 수준을 넘어 아예 꺼버리는 것고 같은 것이다
→ 예외를 절!대! 무시하지 말자 = try-catch의 catch 블럭을 비워두지 말자!
예외를 무시하면 그 프로그램은 오류를 내재한 채 동작하게 된다
어느 순간 문제의 원인과 관계 없는 곳에서 프로그램이 죽을 수도 있다
FileInputStream은 읽기 전용 스트림이고, 이를 닫을 때 예외가 발생하면 어떤 조치를 해줄 필요가 없다
이런 경우 catch 블록안에 주석으로 이유를 남기고, 예외 변수를 ignored로 표기한다
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4; // 기본값
try {
numColors = f.get(1L, TimeUnit.SECONDS);
} catch(TimeoutException | ExecutionException ignored) {
// 기본값을 사용한다(색상 수를 최소화하면 좋지만, 필수는 아니다)
}
검사 예외든, 비검사 예외든 예외를 무시하지 말자!
예외를 적절히 처리해서 오류를 피하거나, 최소한 무시하지 않고 바깥으로 전파되게라도 두어 디버깅 정보를 남긴 채 프로그램이 신속히 중단되게 하라.