10. 예외
예외를 제대로 활용하면 프로그램의 가독성, 신뢰성, 유지보수성이 높아지지만 잘못 사용하면 역효과만 나타나는 양날의 검이다.
#
예외는 진짜 예외 상황에서만 사용하라item 69
이번 아이템은 이 코드 하나로 설명이 끝난다.
배열의 인덱스를 탐색하다가 마지막 부분에서
ArrayIndexOutOfBoundException
예외가 발생하면 끝을 내는 코드이다.예외를 써서 루프를 종료시키는 방법은 아주 나쁘다. 가령 다음 표준 관용구를 사용한 반복문 코드보다 나은 점이 없다.
#
예외를 사용한 종료가 나쁜 이유예외는 에외 상황에 쓸 용도로 설계된 것이다.
코드를 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이라 부른다.
예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 예외 연쇄를 사용하는게 좋다.
예외 연쇄란 문제의 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다.
대부분의 표준 예외는 예외 연쇄용 생성자를 갖추고 있다.
무턱대고 예외를 전파하는 것 보다, 예외 번역이 우수하지만, 그렇다고 남용해서는 안된다.
가급적이면 저수준 예외가 성공하도록 하여 하위 계층에서는 예외가 발생하지 않도록 하는 것이 최선이다.
만약, 하위 계층에서 예외를 피할 수 없다면, 상위 계층에서
java.util.loggin
같은 적절한 로깅을 추가하고 조용히 넘어가게 하는 것도 방법이다.
#
요약하위 계층의 예외를 예방하거나, 스스로 처리할 수 없는 경우 그 예외를 상위 계층에 그대로 노출시키기 애매하다면, 예외 번역을 사용해보자.
예외 연쇄를 사용하면 상위 계층에 맥락에 어울리는 고수준 예외를 던지면서 근본 원인도 함께 알려주는 것이 가능하다.
그러나 예외 번역은 남용하기 보다는 꼭 필요한 시점에 사용해주자.
#
메서드가 던지는 모든 예외를 문서화하라item 74
검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.
한 클래스 안에 같은 이유로 같은 예외를 던진다면, 그 예외를 클래스 설명에 추가하는 방법도 있다.
#
예외의 상세 메시지에 실패 관련 정보를 담으라item 75
예외를 잡지 못하고 프로그램이 실패하면 자바는 일반적으로 예외의 스택 트레이스를 자동으로 출력한다.
보통은 예외 클래스의 이름 뒤에 상세 메시지가 붙는 형태인데, 이 정보로 실패 원인을 찾는 경우가 많다.
실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야한다.
예외는 실패와 관련된 정보를 얻을 수 있는 접근자 메서드를 적절히 제공하는 편이 좋다.
#
가능한 한 실패 원자적으로 만들라item 76
호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야한다.
마치 트랜잭션의 롤백을 떠올리면 된다.
#
불변 객체이러한 실패 원자 상태를 만들기 가장 간단한 방법은, 불변 객체로 설계하는 것이다.
불변 객체는 태생적으로 실패 원자적이다. (실패하면 메모리에서 지워버리면 되니까)
#
가변 객체가변 객체의 메서드를 실패 원자성으로 만드는 가장 흔한 방법은, 직접 수행에 앞서 매개변수의 유효성을 검사하는 것이다.
실패 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 전방에 배치하는 것이다.
#
임시 복사본- 객체의 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원본과 교체하는 방법도 존재한다.
#
복구 코드작업 중 실패를 가로채는 복구 코드를 작성하여, 작업 전 상태로 되돌리는 방법도 있다.
주로 디스크 기반의 내구성을 보장해야하는 자료구조에서 주로 쓰인다만, 자주 쓰이는 방법은 아니다.
#
결론실패 원자성은 일반적으로 권장되나, 항상 달성할 수 있는 것은 아니다.
가령 두 쓰레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 꺠질 수 있다.
ConccurentModificationException을 잡아냈다고 해서 그 객체가 여전히 쓸 수 있는 상태라고 가정해선 안된다.
실패 원자성은 지키면 좋지만, 그걸 달성하기 위해 너무 많은 비용이 들거나 복잡도가 큰 연산의 경우라면 굳이 할 필요는 없다.
문제가 무엇인지 알면 실패 원자성을 공짜로 얻을 수 있는 경우가 더 많다.
#
예외를 무시하지 말라item 77
- 뻔한 조언이지만, 예외는 무시해선 안된다.
catch 블록을 비워두면 예외가 존재할 이유가 없어진다.
비유하자면 화재경보를 무시하는 수준을 넘어 아예 꺼버려 다른 누구도 화재가 발생했는지 모르게 되어버린다.
예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수 이름도 ingonred로 바꾸자.
#
Reference이펙티브 자바 Effective Java 3/E
저자 : 조슈아 블로크