본문 바로가기

Reading Record/이펙티브자바

[아이템69 ~77] 예외

아이템 69 예외는 진짜 예외일 때 사용하라.

예외를 어설프게 성능 최적화 용도로 사용하지 마라.
jvm설계 자체가 구문에 대한 최적화를 해놓았다.

아래 예제를 보자.

    //예외를 이용한 loop
    public void optimizeLoop() {
        Mountain range[] = new Mountain[10];
        try {
            int i = 0;
            while (true) {
                range[i++].climb();
            }
        } catch (Exception e) {
        }
    }

    //정상적인 loop
    public void optimizeLoop2() {
        Mountain range[] = new Mountain[10];
        for (Mountain i : range) {
            i.climb();
        }
    }

    private static class Mountain {
        public void climb() {
        }
    }

기본적으로 배열에 접근하면 경계 검사를 한다. 또한 일반적인 반복문경계검사를 한다.
이 두번의 경계검사가 중복으로 느껴지므로, 예외를 통한 loop로 일반적인 반복문경계검사를 제거하려는 시도였다.

하지만 잘못된 추론이다. 여기서 나온 표준 관용구 forEach문은 이미 jvm최적화완료되어 경계검사를 시도하지 않는다.

잘 설계된 API는 정상적인 흐름에서 예외를 사용하지 않는다.

//=============== 
// 상태 검사 메서드와 상태 의존메서드를 제공
    public boolean hasEngine() {
        return engine != null;
    }

    public Point move() {
        return new Point(engine.power());
    }


//=================
// 상태 검사 메서드를 제공하지 않고 정상적인 값을 반환할 수 없다면 특정 값을 제공
    public Point moveValue() {
        if (engine == null) {
            return new Point(0);
        }
        return new Point(engine.power());
    }
//===============
// optional 제공
    public Optional<Point> moveOptional() {
        if (engine == null) {
            return Optional.empty();
        }
        return Optional.of(new Point(engine.power()));
    }

위 예제는 정상적인 흐름에서 예외를 사용하지 않고 사용할 수 있도록 api를 제공하는 방식이다.

  1. 상태 검사메서드의존 메서드를 제공하는 방식

  2. 올바르지 않은 상태일때 특정 값을 제공

  3. 올바르지 않은 상태를 대비해 optional 제공

위 세가지 방식으로 해당 api에서 올바르지 않은 상태일때 exception을 발생시키지 않는 방법이다.

여기서 2,3번째 방식은 의존메서드에 상태 검사가 중복으로 들어갈때 사용하거나, 멀티 스레드 환경에서 상태 검사 후 의존메서드를 부르는 찰나원하지 않는 결과가 발생할 수 있으므로 해당 상황에 대비하여 사용하는 것이 좋다.



아이템 70 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 비검사 예외를 사용하라.

Checked Exception을 발생시켜야 하는 상황과 Unchecked Exception을 발생시켜야 하는 상황은 매우 헷갈린다.

검사 예외

아이템 70에서는 해당 구분을 호출하는 쪽에서 복구할 수 있는 상황에는 Checked Exception을 발생시키라고 소개하고있다.

해당 api를 호출하는 쪽에서 복구할 수 있는 상황임을 암시하기 위해 Checked Exception을 던지면, 사용자는 해당 exception을 어떻게든 처리해야한다.
(throw하든 catch하든)

그러므로 복구가 가능한 상황에는 검사예외로 강제 예외처리 구문을 추가하도록 한다.

간혹 검사예외를 catch만 하고 아무런 조치도 취하지 않는 경우가 있는데, 비추다.

굳이 꼭 무시해도 될 상황이라면 변수 이름이라도 ignored로 바꾸고 주석이라도 달아놓아야 한다.

비검사 예외

비검사 예외는 전제조건을 만족시키지 못했을때 발생한다.

배열은 0~n-1까지 접근 가능하다. 그 외의 접근을 시도할 경우 전제 조건을 만족시키지 못해 예외를 발생한다.

이런 프로그래밍 오류는 비검사 예외를 사용하도록 하자.

에러

보통 jvm 자원 부족, 불변식 깨짐 등에서 Error가 발생한다.

Error는 프로세스가 더이상 수행될 수 없는 상황이므로 해당 프로세스를 종료한다.

java에서 error는 관례상 상속해서 재구현하거나 하는 일은 추천하지 않는다.

상태 제공 메서드

exception이 발생한 경우 어떤 상태때문에 예외가 발생했는지 알아야 쉽게 복구가 가능하다.

그래서 상태를 확인할 수 있는 메서드를 제공해 주면 좋다.

위 메서드를 제공하지 않는다면, 에러 메시지를 파싱해야하는 힘든 노동을 해야한다...



아이템 71 필요없는 검사 예외 사용은 피해라

검사예외를 제대로 사용하는 경우에 대해 알아보자.

  1. API를 제대로 사용할 경우에도 예외가 발생할 경우
  2. 프로그래머가 의미 있는 조치를 취할 수 있는 경우 (복구 가능한 예외)

위 두가지 상황이 아닐 경우는 비검사 예외를 사용하자.

검사 예외를 회피하는 방법으론 Optinal값 제공, 상태검사,의존 메서드 제공등이 있다.

예제코드는 위 코드를 참고.



아이템 72 표준예외를 사용하자.

이미 자바는 많은 표준 예외들을 제공한다.

자바가 정의한 표준예외들만 잘 사용해도 대다수의 예외케이스가 해결되므로 사용하기 좋은 예외들을 알아보자.

1.IllegalArgumentException -> 허용하지 않는 인수값을 던졌을때

2.IllegalStateException-> 해당 객체가 메서드를 수행할 수 없는 상태일때

3.NullPointerException-> null을 허용하지 않는 메서드에 null을 줄때

4.UnsupportedOperationException -> 호출한 메서드가 지원하지 않을때

5.IndexOutOfBoundsException -> 인덱스 범위르 넘을때

6.ConcurrentModificationException-> 동시수정이 발생했을때

위 여섯가지 표준 예외만 잘 사용해도 왠만한 상황들은 다 해결이 가능하다.

위 예외중 헷갈리는 예외는 1번과 2번이다. 메서드에 만약 어떤 값을 넘기든 실패하는 상황이라면 2번, 그렇지 않다면 1번으로 생각하면 헷갈리지 않을것이다.



아이템 73 추상화 수준에 맞는 예외를 던져라

여러개의 클래스 혹은 메서드로 추상화되어 있는 경우 가끔 뜬금없는 예외가 발생하는 걸 본 적 있을것이다.

추상화된 아랫단계에서 사용자가 알 수 없는 익셉션이 발생한다면, 사용자는 해당 예외가 뭐떄문에 일어났는지 감을 못 잡을 것이다.

위 같은 상황을 어떻게 처리할지 알아보자.

  1. 저수준 예외를 잡아 고수준 예외로 바꿔준다.

    try{
    
    }catch(LowLevelException e){
      throw new HighLevelException(e); -> 저수준 예외를 래핑하여 고수준에서 저수준 예외 원인을 볼 수 있게한다.
    }
  2. 저수준 단계에서 예외발생이 없도록 하고, 예외를 고수준 추상화 단계에서 발생하도록 설계한다.



아이템 74 메서드가 던지는 모든예외를 문서화하라.

예외 문서화 방법

  1. 메서드가 던지는 모든 검사예외를 @throws 목록으로 정리해야 한다.

    해당 사용자는 검사예외를 해당 목록을 보고 적절히 대처하는 코드를 짤 수 있다.

  1. 비검사 예외도 정리하면 좋다. 하지만 @throws목록에 정리하는 것 보단, 다른 곳에 정리하자.
    해당 메서드가 사용하는 클래스들에서도 비검사 예외는 발생할 수 있고, 버전업을 통해 예외항목이 달라진다면, 유지보수하기 힘들기 때문이다.

    (*검사예외는 무조건 대응해야하므로 해당사항 없음)

  1. 한 클래스에 공통적인 예외가 같은 이유로 여러 메서드에서 발생한다면, 해당 예외는 클래스에 정의하는. 것도 방법이다.


아이템 75 예외의 상세 메시지에 실패 관련 메시지를 담아라

예외를 복구하기 위해서는 예외상황에 대한 상세 메시지에 실패와 관련된 메시지를 담아야한다.

예를 들어 IndexOutOfBoundsException에 최소,최대,요청값이 같이 남는다면, 사용자는 더 쉽게 예외상황을 파악할 수 있을것이다.

  • 주의할 점
    • 암호화 정보, 개인 정보는 상세메시지에 담지말자.


아이템 76 가능한 실패 원자적으로 만들라

실패 원자적이란?

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

실패 원자성을 유지하는 방법

  1. 상태 변경전 변경 가능한지 체크해라. (실패 유발 코드는 무조건 상태 변경전 코드 앞에 두어라)
public Element pop(){
  if(stack.length == 0){
    throw new Exception();
  }
  ...
}
  1. 객체 상태 변경시 임시 복사본을 사용해라.
    • 예를 들어 객체의 내부 상태를 변경할때 변경 대상의 값을 다른 변수에 복사하고, 연상 작업을 복사 변수에서 수행하면 실패가 일어나도 원래상태를 보존할 수 있다.
  • 주의사항
    • 동시성 오류같은 경우는 일반적으로 예외를 잡더라도 객체를 재사용하기 어렵다.
    • 동시성 오류를 잡기 위해선 보통 두개의 스레드가 객체의 상태를 변경할때 서로 각자의 기본 상태가 달라지는 걸 캐치하여 발견하기 떄문에 두 스레드 중 어떤것이 원자성을 유지할 수 있는 상태인지 구분하기 어렵다.


아이템 77 예외를 무시하지 마라

아이템 70을 참고