Skip to main content

10. 예외

예외를 제대로 활용하면 프로그램의 가독성, 신뢰성, 유지보수성이 높아지지만 잘못 사용하면 역효과만 나타나는 양날의 검이다.

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

item 69

try {
int i = 0;
while(true)
range[i++].climb();
} catch ( ArrayIndexOutOfBoundException e) {
}
  • 이번 아이템은 이 코드 하나로 설명이 끝난다.

  • 배열의 인덱스를 탐색하다가 마지막 부분에서 ArrayIndexOutOfBoundException 예외가 발생하면 끝을 내는 코드이다.

  • 예외를 써서 루프를 종료시키는 방법은 아주 나쁘다. 가령 다음 표준 관용구를 사용한 반복문 코드보다 나은 점이 없다.

for(Mountatin m : range) {
m.climb();
}

예외를 사용한 종료가 나쁜 이유#

  • 예외는 에외 상황에 쓸 용도로 설계된 것이다.

  • 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.

  • 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다.

요약#

  • 예외는 예외 상황에서만 쓰자

  • 정상적인 제어 흐름에서 사용해서는 안되며 이를 프로그래머에게 강요하는 API를 만들어서도 안된다.

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

item 70

  • 자바에서는 문제 상황을 알리는 Throwable 타입으로 검사 예외, 런타임 예외, 에러 이렇게 총 세 가지를 제공한다.

  • 호출하는 쪽에서 복구하리라 여겨지는 상황이면 검사 예외를 사용하는 편이 좋다.

  • 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자. 가령 배열의 인덱스 범위를 넘는 경우와 같이 말이다.

  • 에러의 경우 JVM 자원 부족, 불변식 깨짐 등 더 이상 수행할 수 없는 경우를 나타낼 때 사용한다.

  • Exception, RuntimeException, Error를 상속하지 않는 Throwable을 만들수도 있지만 그렇게 하지 않는게 더 좋다.

요약#

  • 복구할 수 있는 상황이라면 검사 예외

  • 프로그래밍 오류라면 비검사 예외 (확실하지 않은 경우)

  • 검사 예외도 아니오, 런타임 예외도 아닌 Throwable은 정의하지도 말자.

  • 검사 예외라면 복구에 필요한 정보를 알려주는 메서드도 제공하자.

필요 없는 검사 예외 사용은 피하라#

item 71

  • 일반적으로 예외를 발생하면 해당 메소드에서 catch로 잡아두던가, 더 바깥으로 던져서 문제를 전파시켜야한다.

  • 어느쪽이든 API 사용자에게 부담을 주는 행위이며, 심지어 자바 8의 스트림 안에서는 직접 사용이 안되기 때문에 부담이 커졌다. (실제로 많이 겪은 이슈, 스트림 내부에서 예외를 잡을 수는 있는데 코드가 매우 지저분해짐)

  • 그렇기에 가급적이면 검사 예외를 안던지는 방향을 고민해볼 필요가 있다.

요약#

  • 꼭 필요한 곳에서만 사용한다면 검사 예외는 안정성을 높여주지만, 남용한다면 고통스러운 API가 되어버린다.

  • API 호출자가 예외 상황을 복구할 방법이 없다면 비검사 예외를 던지자.

  • 복구가 가능하고, 호출자가 처리를 해주길 바란다면 우선 옵셔널을 반환하는 것을 고려해보자.

표준 예외를 사용하라#

item 72

  • 표준 예외를 재사용해야하는 이유는 무엇일까?

  • 우리가 만든 API를 다른 사람이 익히고, 사용하기 쉽다는 점에 있다.

  • 많은 프로그래머들은 이미 익숙해진 규약을 그대로 따르기 떄문이다.

추상화 수준에 맞는 예외를 던지라#

item 73

  • 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야한다.

  • 이를 예외 번역, Exception Translation이라 부른다.

try {
...
}
catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
  • 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 예외 연쇄를 사용하는게 좋다.

  • 예외 연쇄란 문제의 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다.

try {
...
}
catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
  • 대부분의 표준 예외는 예외 연쇄용 생성자를 갖추고 있다.

  • 무턱대고 예외를 전파하는 것 보다, 예외 번역이 우수하지만, 그렇다고 남용해서는 안된다.

  • 가급적이면 저수준 예외가 성공하도록 하여 하위 계층에서는 예외가 발생하지 않도록 하는 것이 최선이다.

  • 만약, 하위 계층에서 예외를 피할 수 없다면, 상위 계층에서 java.util.loggin 같은 적절한 로깅을 추가하고 조용히 넘어가게 하는 것도 방법이다.

요약#

  • 하위 계층의 예외를 예방하거나, 스스로 처리할 수 없는 경우 그 예외를 상위 계층에 그대로 노출시키기 애매하다면, 예외 번역을 사용해보자.

  • 예외 연쇄를 사용하면 상위 계층에 맥락에 어울리는 고수준 예외를 던지면서 근본 원인도 함께 알려주는 것이 가능하다.

  • 그러나 예외 번역은 남용하기 보다는 꼭 필요한 시점에 사용해주자.

메서드가 던지는 모든 예외를 문서화하라#

item 74

  • 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.

  • 한 클래스 안에 같은 이유로 같은 예외를 던진다면, 그 예외를 클래스 설명에 추가하는 방법도 있다.

예외의 상세 메시지에 실패 관련 정보를 담으라#

item 75

  • 예외를 잡지 못하고 프로그램이 실패하면 자바는 일반적으로 예외의 스택 트레이스를 자동으로 출력한다.

  • 보통은 예외 클래스의 이름 뒤에 상세 메시지가 붙는 형태인데, 이 정보로 실패 원인을 찾는 경우가 많다.

  • 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야한다.

  • 예외는 실패와 관련된 정보를 얻을 수 있는 접근자 메서드를 적절히 제공하는 편이 좋다.

가능한 한 실패 원자적으로 만들라#

item 76

  • 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야한다.

  • 마치 트랜잭션의 롤백을 떠올리면 된다.

불변 객체#

  • 이러한 실패 원자 상태를 만들기 가장 간단한 방법은, 불변 객체로 설계하는 것이다.

  • 불변 객체는 태생적으로 실패 원자적이다. (실패하면 메모리에서 지워버리면 되니까)

가변 객체#

  • 가변 객체의 메서드를 실패 원자성으로 만드는 가장 흔한 방법은, 직접 수행에 앞서 매개변수의 유효성을 검사하는 것이다.

  • 실패 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 전방에 배치하는 것이다.

임시 복사본#

  • 객체의 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원본과 교체하는 방법도 존재한다.

복구 코드#

  • 작업 중 실패를 가로채는 복구 코드를 작성하여, 작업 전 상태로 되돌리는 방법도 있다.

  • 주로 디스크 기반의 내구성을 보장해야하는 자료구조에서 주로 쓰인다만, 자주 쓰이는 방법은 아니다.

결론#

  • 실패 원자성은 일반적으로 권장되나, 항상 달성할 수 있는 것은 아니다.

  • 가령 두 쓰레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 꺠질 수 있다.

  • ConccurentModificationException을 잡아냈다고 해서 그 객체가 여전히 쓸 수 있는 상태라고 가정해선 안된다.

  • 실패 원자성은 지키면 좋지만, 그걸 달성하기 위해 너무 많은 비용이 들거나 복잡도가 큰 연산의 경우라면 굳이 할 필요는 없다.

  • 문제가 무엇인지 알면 실패 원자성을 공짜로 얻을 수 있는 경우가 더 많다.

예외를 무시하지 말라#

item 77

  • 뻔한 조언이지만, 예외는 무시해선 안된다.
try {
...
} catch (SomeException e) {
}
  • catch 블록을 비워두면 예외가 존재할 이유가 없어진다.

  • 비유하자면 화재경보를 무시하는 수준을 넘어 아예 꺼버려 다른 누구도 화재가 발생했는지 모르게 되어버린다.

  • 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수 이름도 ingonred로 바꾸자.

try {
...
} catch (SomeException ignored) {
// 모종의 이유로 어떤 예외에 대한 처리도 하지 않는다.
}

Reference#

스크린샷 2021-04-16 오후 4 24 23

이펙티브 자바 Effective Java 3/E

저자 : 조슈아 블로크

Last updated on