Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ Item 69 ] 예외는 진짜 예외 상황에만 사용하라 #67

Open
ruthetum opened this issue Jun 10, 2022 · 0 comments
Open

[ Item 69 ] 예외는 진짜 예외 상황에만 사용하라 #67

ruthetum opened this issue Jun 10, 2022 · 0 comments
Assignees
Labels
10장 예외

Comments

@ruthetum
Copy link
Member

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

배열 순회

① 예외를 잘못 사용한 예 👎

public void bad() {
    try { 
        int i = 0; 
        while(true) 
            range[i++].climb(); 
    } catch (ArrayIndexOutOfBoundsException e) { 
    } 
}

② 표준적인 관용구 👍

public void good() {
    for (Mountain m : range) {
        m.climb();
    }
}

①번 코드의 경우 일단 코드가 직관적이지 않다.

원 목적은 배열의 원소를 순회하는 것인데 무한 루프를 돌다가 배열의 끝에 도착하면 ArrayIndexOutOfBoundsException을 발생시켜 순회를 종료하고 있다.

②번 코드와 같이 작성했다면 대부분의 사람들은 바로 이해했을 것이다.

예외를 써서 루프를 종료한 이유?

잘못된 추론을 근거로 성능을 높여보고자 한 것이다.

JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데, 일반적인 반복문도 배열 경계에 도달하면 종료한다.

따라서 이 검사를 반복문에도 명시하면 같은 일이 중복되므로 하나를 생략한 것이다.

잘못한 추론한 측면 3가지

  1. 예외는 예외 상황에 쓸 용도로 설계되었으므로 JVM 구현자 입장에서는 명확한 검사만큼 빠르게 만들어야 할 동기가 약하다. (최적화에 신경쓰지 않았을 가능성이 크다.)

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

  3. 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않는다. JVM이 알아서 최적화해서 없애준다.

실제 실행 결과

// ns (System.nanoTime())로 비교
// ms (System.currentTimeMillis())로 비교 시 둘 다 0ms로 측정
public void sample() {
    int count[] = new int[100];

    long startTime = System.nanoTime();
    try {
        int i = 0;
        while (true)
            count[i++]++;
    } catch (ArrayIndexOutOfBoundsException e) {
    }
    long endTime = System.nanoTime();
    System.out.println("try-catch : " + (endTime - startTime) + "ns"); // try-catch : 68600ns

    startTime = System.nanoTime();
    for (int i = 0; i < count.length; i++)
        count[i]++;
    endTime = System.nanoTime();
    System.out.println("loop : " + (endTime - startTime) + "ns"); // loop : 3400ns
}

실제 실행결과를 확인해봤을 때 일반적인 반복문을 사용할 때보다 try-catch를 사용했을 때 훨씬 느린 것을 확인할 수 있다.

추가적으로 만약 반복문에 버그가 숨어있다면 흐름 제어를 위해 사용된 예외가 이 버그를 숨겨 디버깅을 어렵게 할 수 있다

또한 반복문의 몸체에서 사용한 메소드(ex. climb())가 내부에서 관련 없는 배열을 사용하다가 ArrayIndexOutOfBoundsException가 발생하는 경우, 해당 예외를 정상적인 반복문 종료로 오해하고 종료될 수 있다.

예외를 사용하는 원칙

예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다.

잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할일이 없게 해야한다.

특정 상태에서만 호출할 수 있는 '상태 의존적' 메서드를 제공하는 클래스는 '상태 검사' 메서드도 함께 제공해야 한다.

Iterator

Iterator 인터페이스의 nexthasNext가 각각 상태 의존적 메서드와 상태 검사 메서드에 해당한다.

for-each도 내부적으로 hasNext를 사용한다.

Good 👍

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { 
    Foo foo = i.next();
    ...
}

Bad 👎

try { 
    Iterator<Foo> i = collection. iterator() ; 
    while(true) { 
        Foo foo = i. next( ) ; 
    } 
} catch (NoSuchElementException e) { 
}

상태 검사 메서드 대신 사용할 수 있는 선택지

상태 검사 메서드 대신 빈 옵셔널이나 null 같은 특정 값을 반환할 수도 있다.

상태 검사 메서드, 옵셔널, 특정 값 중 하나를 선택하는 방법

  1. 외부 동기화 없이 여러 스레드가 동시에 접근하거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다.

  2. 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.

  3. 그 외의 모든 경우에는 상태 검사 메서드 방식이 조금 더 낫다.

정리

예외는 예외 상황에서 쏠 의도로 설계되었다.

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

@ruthetum ruthetum added the 10장 예외 label Jun 10, 2022
@ruthetum ruthetum self-assigned this Jun 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
10장 예외
Projects
None yet
Development

No branches or pull requests

1 participant