Study/object

[엘레강트 오브젝트] 4-2장 체크 예외(checked exception)만 던지세요

에디개발자 2021. 10. 6. 00:31
반응형

나를 닮았다고 한다...

이 글은 엘레강트 오브젝트 새로운 관점에서 바라본 객체지향 도서를 보며 스터디한 글입니다.

책에서 주장하는 내용을 정리하였으며 예제들은 모두 코틀린 코드로 변환하여 작성하였습니다.

 

목차

  1. Checked Exception vs Unchecked Exception
  2. 꼭 필요한 경우에만 예외를 잡자
  3. 항상 예외를 체이닝하자
  4. 단 한번만 복구하자
  5. 관전-지향 프로그래밍을 사용하자
  6. 하나의 예외 타입만으로도 충분하다

 


1. Checked Exception vs Unchecked Exception

두 Exception의 차이는 이 글을 참조해주세요.

 

unchecked exception

fun length(file: File): Int {
    if (!file.exists())
        throw IllegalArgumentException("file doesn't exist")
    return content(file).length
}

fun use() {
    val file = File("/path/file")
    length(file)  // 예외에 대한 처리를 하지 않을 수 있음
}

unchecked exception은 의미 그대로 코드에서 exception을 체크하지 않아도 되는 exception입니다. 이 경우 개발자는 발생하는 exception에 대해서 무방비해지고 잠재적으로 500 에러를 발생시키는 코드가 탄생하게 됩니다.

 

checked exception

이 코드는 Java로 작성합니다. Kotlin에서는 throws 키워드가 존재하지 않습니다.
// check exception
public byte[] content(File file) throws IOException {
    byte[] array = new byte[1000];
    new FileInputStream(file).read(array);
    return array;
}

// 방법1
public int length(File file) {
    try {
        return content(file).length;
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 방법2
public int length(File file) throws IOException {
    return content(file).length;
}

위처럼 checked exception으로 처리했을 경우 개발자는 반드시 exception에 대한 처리를 해야만 합니다. 개발자는 안전하지 않은 메서드를 사용하고 있다는 사실을 알 수 있습니다. 

 

2. 꼭 필요한 경우에만 예외를 잡자

public int length(File file) {
    try {
        return content(file).length;
    } catch (IOException e) {
        return 0;
    }
}

checked exception을 사용한 코드입니다. 이 코드는 에러가 발생하지 않는 안전한 코드입니다. 이로써 프로그램에 함정카드를 심어놓았습니다. 이 코드에서 exception이 발생할 경우 결과값을 0을 반환하고 있습니다. 코드에서는 exception을 처리하라고 개발자에게 명시했지만 개발자는 무시하고 0 ( 개발한 개발자만이 알 수 있는 숫자 ) 을 반환해버립니다. 이 코드에서 exception이 발생한 경우 정상처리되면 안되지만 강제로 정상처리하여 개발자에게 함정카드 발동이라는 나쁜 선물을 선사하게 됩니다.

반환값으로 0, -1 을 개발자 임의로 반환한다면 null을 반환하는 행위와 동일합니다.

이와 같이 처리할바엔 exception을 상위호출에게 throws 해버리는 것이 훨씬 효율적입니다.

 

3. 항상 예외를 체이닝하자

public int length(File file) throws Exception {
    try {
        return content(file).length;
    } catch (IOException e) {
        throw new Exception("계산이 불가능하다.");
    }
}

먼저 체이닝하지 않는 코드를 살펴보겠습니다. 이 코드는 이전에 작성한 코드에서 exception 처리를 새로운 Exception을 생성하여 throw 시키고 있습니다. 이 경우 length() 메서드를 사용하는 곳에서 Exception을 처리할 수 있지만 근본적으로 어디에서 Exception이 발생했는지 찾을 수 없습니다. 

Exception은 length() 메서드에서 발생하고 있다. 하지만 근본적인 exception은 content() 메서드에서 발생하고 있다. 개발자는 근본적인 exception을 length() 메서드로 착각한다.

위 코드를 체이닝 방식으로 수정해보겠습니다.

public int length(File file) throws Exception {
    try {
        return content(file).length;
    } catch (IOException e) {
        throw new Exception("계산이 불가능하다.", e);  // 체이닝 방식
    }
}

근본적으로 Exception이 발생한 IOException의 데이터를 새로운 Exception을 생성할 때 넣어주는 방식으로 작성할 수 있습니다. 

 

4. 단 한번만 복구하자

복구는 가장 상위에서 단 한번만 복구하는 것이 가장 좋습니다. 하위에서 발생한 exception은 그 메서드를 사용하고 있는 상위에서 exception을 잡고 체이닝하고 던지고, 다시 상위에서 exception을 잡고 체이닝하고 던지는 방법이 가장 좋습니다.

 

항상 예외를 잡고, 체이닝하고, 던지고를 반복한 뒤 가장 최상위에서 단 한번만 복구합니다.

 

5. 관점-지향 프로그래밍을 사용하자

exception을 발생시키는 경우 가끔식 실패한 케이스에 대해서 재시도를 해야할 필요가 있습니다. 

public String content() throws IOException {
    int attempt = 0;
    while (true) {
        try {
            return http();
        } catch (IOException ex) {
            if (attempt >= 2) {
                throw ex;
            }
        }
    }
}

이 코드는 http()메서를 2번 재시도하고 3번째에 예외를 발생시키는 코드입니다. 하지만 코드가 매우 장황해집니다. 이럴 경우 관점-지향 프로그래밍 AOP 방법을 사용하여 풀 수 있습니다. 

 

위 코드를 AOP 방법으로 수정해보겠습니다.

@RetryOnFailure(attempts = 3)
public String content() throws IOException {
    return http();
}

컴파일러는 컴파일 시점에 @RetryOnFailure Annotation을 발견한 후 content() 메서드를 실패, 재시도 코드로 둘러쌉니다. 이렇게 실패, 재시도 코드를 분리하면 중복 코드 또한 제거할 수 있습니다. 

 

6. 하나의 예외 타입만으로도 충분하다

'절대 복구하지 않기' '항상 체이닝하기'와 같은 방법으로 예외를 처리한다면 예외 타입은 하나의 타입만으로도 충분합니다. 예외는 이전에 발생한 예외를 담을 수 있는 예외라면 항상 체이닝하여 가장 최상위까지 예외를 던져 처리할 수 있기 때문입니다. 

 

반응형