금요일, 4월 19
Shadow

#013 예외(Exceptions)

이 장에서는 효과적으로 예외를 사용하는 지침을 제공한다.


예외 상황에서만 예외를 사용하자

/ 예외의 터무니 없는 사용. 절대 이렇게 하지 말자!
try {
    int i = 0;
    while(true)
        range[i++].climb();
} 
catch(ArrayIndexOutOfBoundsException e) {
}

위의 코드는 예외를 이용하여 while 루프문을 종료시키고 있다.
배열의 범위를 벗어나는 최초의 배열 요소를 사용하려는 순간, ArrayIndexOutOfBoundsException 예외가 발생하고(throw), 검출되고(catch), 무시되면서 이 무한 루프는 종료된다.
이런 식의 코드는 본래의 목적을 혼란스럽게 하고 성능을 저하시키며, 코드가 제대로 동작하는 것을 보장하지 못한다.
코드의 취지와 무관한 버그가 생겨 우리도 모르는 사이에 루프 실행이 실패하고 버그를 감추게 되어 디버깅이 무척 복잡해진다.
예외는 예외적인 상황에서 사용하기 위해 설계된 것이니, 정상적인 흐름 제어에 예외를 사용하지 말자.


복구 가능 상황에는 checked 예외를 사용하고 런타임 예외는 프로그램 에러에 사용하자

자바에서는 던질 수 있는(throwable) 세 종류의 예외를 제공한다.

1. checked 예외
명시적으로 try-catch-finally 예외 처리를 해야하는 것
2. runtime 예외
JVM이 정상적으로 작동하는 동안에 발생하는 예외
3. error
프로그램이 catch 해서는 안되는 심각한 문제

언제 어떤 예외를 사용해야하는 것이 적합한지에 대해 프로그래머들 간에 혼선이 있을 수 있으나, 몇 가지 일반적인 규칙이 있다.
메소드 호출자가 예외 복구를 할 수 있는 상황에서는 checked 예외를 사용하자
unchecked 예외에는 runtime 예외와 error가 있으며, 이들은 catch할 필요가 없고 일반적으로는 catch해서도 안된다.
unchecked 예외나 에러가 발생했다는 것은 복구 불가능하고 계속 실행해봐야 더 해롭기만 한 상황이라는 것을 의미한다.

만일 프로그램에서 catch하지 않으면 그 예외에 적합한 에러 메세지가 출력되면서 현재 실행 중인 Thread가 중단된다.
프로그래밍 에러는 runtime 예외를 사용하자.
대부분의 runtime 예외는 API를 사용하는 client가 그 API의 명세에 설정된 규약을 지키지 않은 것을 말한다.

예를 들면, 배열의 인덱스 값은 0 부터 (배열길이 – 1) 사이의 값이어야 한다는 것이 배열 사용할 때의 규약이다.
ArrayIndexOutOfBoundsException 예외는 이러한 규약을 위반했다는 것을 나타낸다.
자바 언어 명세(JLS)를 보면 error는 JVM에서 사용하며, 자원 부족, 불변 규칙 위반에 따른 실패, JVM이 실행을 계속할 수 없는 상황 등을 나타낸다고 되어 있다.

이런 내용에 의거하여, 에러의 최상위 클래스인 Error를 상속받는 서브 클래스는 만들지 않는 것이 좋다.
우리가 구현하는 모든 unckecked 예외는 RuntimeException의 서브 클래스여야 한다.


checked 예외의 불필요한 사용을 피하자

checked 예외는 프로그래머가 예외 상황을 처리하지 않을 수 없도록 한다. 그런 점에서 checked 예외를 과용하면 API 사용을 불편하게 만들 수 있다.
어떤 메소드가 하나 이상의 checked 예외를 던진다면, 그 메소드를 호출한 코드에서는 하나 이상의 catch 블록에서 예외를 처리하거나,

public void DynamicDataTest() {

    try {
        URL url = new URL("http://www.jabook.org/DynamicData.class");
        InputStream is = url.openStream();
        FileOutputStream fos = new FileOutputStream("DynamicData.class");
        int i;
        while((i = is.read()) != -1) {
            fos.write(i);
            System.out.println("|");
        }
        fos.close();
        is.close();
        Class c = Class.forName("DynamicData"); 
        Object obj = c.newInstance(); 
        System.out.println(obj);
    }
    catch ( IOException e ) {
        e.printStackTrace();
    }
    catch ( ClassNotFoundException e ) {
        e.printStackTrace();
    }
    catch ( InstantiationException e ) {
        e.printStackTrace();
    }
    catch ( IllegalAccessException e ) {
        e.printStackTrace();
    }
}

또는 예외를 던지는 메소드를 호출한 메소드가 자신의 메소드 선언부에 throws 키워드를 사용해서 그 예외들을 선언함으로써, 외부로 예외 처리를 넘겨야 한다.

public void DynamicDataTest() {
    throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {

    URL url = new URL("http://www.jabook.org/DynamicData.class");
    InputStream is = url.openStream();
    FileOutputStream fos = new FileOutputStream("DynamicData.class");
    int i;
    while((i = is.read()) != -1) {
        fos.write(i);
        System.out.println("|");
    }
    fos.close();
    is.close();
    Class c = Class.forName("DynamicData"); 
    Object obj = c.newInstance(); 
    System.out.println(obj);
}

코드출처: http://www.jabook.com/jabook2/bs/bsTreeLoad.do?ba_no=72

11장 자바 Reflection > 11.1 Class 클래스 > 11.1.4 동적 바인딩 클래스 I둘 중 어떤 방법을 사용하건 프로그래머에게 막중한 부담을 준다.

첫번째 방법은 catch 블록에서 예외처리를 하는 내용이 없는데도 checked 예외를 사용함으로서 할 일이 많아진다.
만일 API를 올바르게 사용해도 예외 사항을 피할 수 없고, 예외를 만나더라도 그 API를 사용하는 프로그래머가 어떤 조치를 취해야 한다면, unchecked 예외를 사용하는 것이 더 적합하다.
checked 예외를 unchecked 예외로 바꾸는 한 가지 방법은, 해당 예외를 발생시키는 메소드를 두 개의 메소드로 쪼개는 것이다.
쪼개진 두개의 메소드 중, 하나는 예외 발생 여부를 나타내는 boolean 값을 반환하게 된다.

아래의 코드를,

// checked 예외를 사용한 메소드 호출
try {
    obj.action(args);
}
catch ( TheCheckedException e ) {
    ... // 예외 처리
}

이런 형태로 바꾼다.

// 상태 검사 메소드와 unchecked 예외를 사용한 메소드 호출
if ( obj.actionPermitted(args) ) {
    obj.action(args);
} else {
    ... // 예외 처리
}

변환된 코드가 전보다 깔끔하지 않지만, 유연성은 더 좋다.
그러나 이 메소드가 호출된 객체가 동시적으로 사용되거나(thread 등에서), 외부에서 객체의 상태를 변경해야 한다면 이 코드는 부적합하다.
actionPermitted 메소드와 action 메소드가 호출되는 시점 사이에 그 객체의 상태가 변경될 수 있기 때문이다.


표준 예외를 사용하자

기존에 정의된 예외를 재사용할 때의 장점

1. 프로그래머들이 이미 익숙해진 내용이므로, 배우고 사용하기 쉽다.
2. 생소한 예외를 사용하지 않으므로 코드를 이해하기 쉽다.
3. 적은 수의 예외 클래스를 사용하므로, 메모리 사용도 적게하고 클래스를 메모리로 로딩하는 시간도 줄어든다.
 IllegalArgumentException: null이 아닌 매개 변수 값이 부적합할 때
 IllegalStateException: 객체가 메소드 호출이 가능한 상태가 아닐 때
 NullPointerException: 매개 변수 값이 null일 때
 IndexOutOfBoundsException: index 매개 변수 값이 범위를 벗어날 때
 ConcurrentModificationException: 동시적인 수정이 금지된 객체가 변경되었을 대
 UnsupportedOperationException: 해당 객체에서 메소드를 지원하지 않을 때

재사용할 예외를 선택하는 것은 정확한 과학과는 다르므로, 엄격한 규칙은 없다.


하위 계층의 예외 처리를 신중하게 하자

어떤 메소드에서 자신이 수행하는 작업과 뚜렷한 관계가 없는 예외가 발생한다면 혼란스러울 것이다.
이런 일은 하위 계층의 추상체에서 발생한 예외를 메소드가 자신을 호출한 메소드로 throw할 때 종종 발생한다.

하위 계층의 추상체: 예를 들어, 네트워크나 DB와 같이 외부와의 인터페이스를 담당하는 하위 계층 클래스 등이 해당된다.

이런 문제를 방지하려면, 상위 계층에서 하위 계층의 예외를 반드시 catch 해야 한다.
그리고 하위 계층에서 발생한 예외 대신, 상위 계층에서 알수 있는 예외로 바꿔 던져야 한다.
다음과 같은 이디엄을 예외 변환(exception translation)이라고 한다.

// 예외 변환
try {
    ... // 하위 계층의 추상체를 사용하여 작업하는 코드
} 
catch(LowerLevelException e) {
    throw new HigherLevelException(...);
}

다음은 List 인터페이스의 골격 구현(skeletal implementation)이라고 할 수 있는 AbstractSequentialList의 예외 변환 코드 예이다.

public abstract class AbstractSequentialList extends AbstractList {
    /**
     * Returns the element at the specified position in this list.
     *
     * 

This implementation first gets a list iterator pointing to the * indexed element (with listIterator(index)). Then, it gets * the element using ListIterator.next and returns it. * * @throws IndexOutOfBoundsException {@inheritDoc} */ public E get(int index) { try { return listIterator(index).next(); } catch (NoSuchElementException exc) { throw new IndexOutOfBoundsException("Index: "+index); } } }

예외 변환의 특별한 예로 예외 연쇄(exception chaining)가 있는데, 이것은 고수준(상위 계층) 예외를 유발시킨 저수준(하위 계층) 예외가 디버깅에 도움이 될 경우에 적합하다.
즉, 예외를 유발시킨 근원인 저수준 예외가 고수준 예외로 전달되는 것으로써, 이때 고수준 예외에서는 저수준 예외를 가져오는 접근자 메소드(Throwable.getCause)를 제공한다.

// 예외 연쇄
try {
    ... // 하위 계층의 추상체를 사용하여 작업하는 코드
} 
catch(LowerLevelException cause) {
    throw new HigherLevelException(cause);
}

최상위 예외 클래스는 Throwable로써, 이 클래스의 생성자에는 예외 연쇄를 알수 있는 생성자가 존재한다.

public class Throwable implements Serializable {
    // 생성자
    public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }

    public Throwable getCause() {
        return (cause==this ? null : cause);
    }

    public synchronized Throwable initCause(Throwable cause) {
        if (this.cause != this)
            throw new IllegalStateException("Can't overwrite cause");
        if (cause == this)
            throw new IllegalArgumentException("Self-causation not permitted");
        this.cause = cause;
        return this;
    }
}

따라서 다음과 같이 수퍼 클래스의 생성자를 호출하면 된다.

// 예외 연쇄를 사용하는 고수준 예외 클래스
class HigherLevelException extends Exception { // 우리가 예외 클래스를 정의할 때는 일반적으로 Exception 클래스를 상속 받는다.
    HigherLevelException(Throwable cause) {
        super(cause);
    }
}

대부분의 표준 예외들은 이러한 예외 연쇄를 알고 있는 생성자를 갖고 있으나, 그런 생성자가 없을 경우에는 Throwable.initCause 메소드를 사용하여 근원 예외를 설정할 수 있다.
이렇게 예외 연쇄를 이용하면, 프로그램에서 근원 예외를 사용할 수 있는 것은 물론이고, 근원 예외의 스택 추적정보를 고수준 예외의 것과 통합할 수 있다.
하위 계층에서 발생한 예외를 분별없이 전달하는 것보다는 예외 변환을 사용하는 것이 좋지만 남용해서는 안된다.
하위 계층에서 발생한 예외를 처리하는 가장 좋은 방법은,
상위 계층 메소드의 매개 변수를 하위 계층 메소드로 전달할 때, 유효성을 철저하게 검사하여 예외가 생기지 않도록 하는 것이다.
하위 계층에서 발생하는 예외를 막을 수 없을 때 좋은 방법은,
상위 계층에서 java.util.logging과 같은 로깅 기능을 이용하여, 하위 계층 메소드에서 발생한 예외를 조용히 처리하는 것이다.
이렇게 하면, 클라이언트 코드와 최종 사용자는 문제에서 격리되고 시스템 관리자가 문제를 조사할 수 있다.


메소드가 던지는 모든 예외를 문서화하자

Javadoc의 @throws 태그를 사용하여 항상 checked 예외는 별도로 선언하고, 각 예외가 발생하는 상황을 정확하게 문서화하자.
메소드가 던지는 예외가 많다고 “throws Exception”으로 한다거나, 더 나쁘게는 “throws Throwable”과 같은 식으로, 예외 클래스의 수퍼 클래스로 함축해서 나타내면 안된다.
이렇게 하면 메소드 사용자가 그 메소드가 던질 수 있는 예외에 대해서 제대로 알 수 없게 된다.
Javadoc의 @throws 태그를 사용하여 메소드가 던질 수 있는 unchecked 예외를 문서화하자. 그러나 메소드 선언부의 throws 키워드에는 unchecked 예외를 넣지 말자.
어떤 것이 checked 예외이고, unchecked 예외인지 프로그래머가 아는 것이 중요하다. 두 가지 예외에 따라 해야할 일이 다르기 때문이다.
만일 같은 클래스 내의 여러 메소드에서 동일한 이유로 어떤 한 가지 예외를 던진다면, 그 예외를 메소드에 개별적으로 문서화하기보다, 클래스 문서화 주석에 한 번만 작성하는 것이 좋다.


실패 상황 정보를 상세 메시지에 포함하자

catch 하지 않은 예외로 인해 프로그램 실행이 실패하면, 시스템에서 자동으로 그 예외의 스택 추적정보(stack trace)를 출력한다.
스택 추적정보에는 그 예외를 나타내는 문자열(string representation)이 포함되는데, 이 문자열은 그 예외의 toString 메소드가 호출되어 만들어진 것이다.
일반적으로 문자열은 예외 클래스 이름 다음에 상세 메세지(detail message)가 연결된 형태로 구성된다.
그 예외의 상세 메시지는 향후 분석을 위해 필요한 실패 상황 정보를 담고 있어야 한다.
만일 소프트웨어가 실패했을 때, 프로그래머는 그 상세 메세지를 바탕으로 실패 사항을 쉽게 재현할 수 있을 것이다.
실패 상황 정보를 잡으려면, “예외 발생에 기여한” 모든 매개 변수와 필드의 값이 예외의 상세 메세지에 포함되어야 한다.
적절한 실패 상황 정보가 예외의 상세 메시지에 포함되도록 하는 한 가지 방법은,
해당 예외 클래스의 생성자에 상세 문자열을 인자로 받는 대신 실제 필요한 상황 정보를 요청하여 만드는 것이다.
IndexOutOfBoundsException 예외 클래스의 예를 들자면, IndexOutOfBoundsException(String s)처럼 String 인자를 받고 있는 생성자보다

package java.lang;

/**
 * Thrown to indicate that an index of some sort (such as to an array, to a
 * string, or to a vector) is out of range. 
 * 

* Applications can subclass this class to indicate similar exceptions. * * @author Frank Yellin * @version %I%, %G% * @since JDK1.0 */ public class IndexOutOfBoundsException extends RuntimeException { /** * Constructs an IndexOutOfBoundsException with no * detail message. */ public IndexOutOfBoundsException() { super(); } /** * Constructs an IndexOutOfBoundsException with the * specified detail message. * * @param s the detail message. */ public IndexOutOfBoundsException(String s) { super(s); } }

다음과 같이, 실패 상황정보를 상세 메시지로 전달하는 이디엄을 사용하는 것을 적극 권장한다.

/**
 * IndexOutOfBoundsException 상황 정보 구성
 *
 * @param   lowerBound 인덱스의 최저값
 * @param   upperBound 인덱스의 최고값
 * @param   index 실제 인덱스 값
 */
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
    // 실패 상황정보를 담은 상세 메세지를 만든다.
    super("Lower Bound" + lowerBound + ", Upper Bound" + upperBound + ", Index" + index);

    // 프로그램에서 사용하기 위해 상황 정보를 보존한다.
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index= index;
}

항목 58에서 제안했듯이, 실패 상황 정보를 알려주는 접근자 메소드를 해당 예외에서 제공하는 것이 좋을 수도 있다.
특히 unchecked 예외보다 checked 예외는 그 접근자 메소드를 사용하여 유용하게 장애를 복구할 수 있다.
그러나 unchecked 예외일지라도 일반 원칙(항목 10)에 따라 접근자 메소드를 제공하는 것이 바람직 하다고 생각한다.


실패 원자성을 갖도록 노력하자

일반적으로, 호출된 메소드가 실행에 실패하더라도 객체 상태는 메소드 호출 전과 같아야 한다. 이런 특성을 갖는 메소드를 실패 원자성(failure atomic) 메소드라 한다.
이런 효과를 얻는 방법 몇 가지가 있다.

1. 가장 간단한 방법은 불변 객체로 설계하는 것이다 (항목 15).

객체가 변하지 않으므로 실패 원자성을 고려하지 않아도 된다.
가변 객체를 처리하는 메소드의 경우 실패 원자성을 성취하는 가장 보편적인 방법은, 연산 수행 전에 매개 변수의 유효성을 검사하는 것이다 (항목 38).
이렇게 하면 객체의 변경이 시작되기 전에 미리 예외를 던질 수 있다.
2. 실패 원자성을 성취하는 더 좋은 방법은, 객체를 변경하는 코드에 앞서 실패할 수 있는 코드가 먼저 실행되도록 연산 순서를 조정하는 것이다.
3. 흔하지 않은 방법이지만, 연산 도중에 발생하는 실패를 가로채는 복구 코드(recovery code)를 작성하는 것이다.
이 방법은 주로 영속성(디스크 기반의)을 갖는 데이터 구조에 사용된다.
4. 객체의 임시 복사본을 만들어 연산을 수행하고, 연산 작업이 성공하면 그 객체를 임시 복사본으로 변경하는 것이다.
일반적으로 실패 원자성은 바람직한 것이지만, 복구 불가능한 에러가 발생 했을 때는 실패 원자성을 유지하기 위한 시도조차 할 필요가 없다.
비용이나 복잡도를 현저하게 증가시키는 연산일 경우, 실패 원자성이 바람직하지 않을 수도 있다.

예외를 묵살하지 말자

// 비어있는 catch 블록은 예외를 묵살한다 - 의도가 의심스럽다!
try {
    ...
} 
catch(SomeException e) {
}

unchecked와 checked 예외 모두, catch 블록을 비워 예외를 묵살하지 말자.
우리도 모르는 사이에 계속해서 에러에 직면하는 프로그램을 초래하게 된다.
예외를 무시해도 되는 때는 FileInputStream을 닫을 때이지만, 이런 경우일지라도 예외를 처리하는 것이 좋다.

creative by javacafe

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.