effective-swift

Item 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라

이번 아이템에서는 자바, 오브젝티브 씨, 스위프트 각각의 예외 종류를 알아보고 각 언어에서 예외(또는 오류)를 어떻게 사용하는게 좋은지 알아봅니다.

Java

자바는 문제 상활을 알리는 타입(Throwable)으로 검사 예외, 런타임 예외, 에러 이렇게 세 가지를 제공하는데, 언제 무엇을 사용해야 하는지 헷갈려 하는 프로그래머들이 종종 있습니다. 언제나 100% 명확한 건 아니지만 이럴 때 참고하면 좋은 멋진 지침들이 있으니 함께 살펴봅시다.

우선 자바의 예외 종류에 대해 알아봅시다.

Java의 예외 및 에러 종류

자바 에러 구조

호출하는 쪽에서 복구하리라 여기지는 상황이라면 검사 예외를 사용하라

검사 예외를 던지면 호출자가 그 예외를 catch로 잡아 처리하거나 더 바깥으로 전파하도록 강제하게 됩니다. 따라서 API 설계자는 호출하는 쪽에서 복구하리라 여기지는 상황이라 판단되면 API 사용자에게 검사 예외를 던져주어 그 상황에서 회복해내라고 요구하면 됩니다.

검사 예외는 일반적으로 복구할 수 있는 조건일 때 발생합니다. 따라서 호출자가 예외 상황에서 벗어나는 데 필요한 정보를 알려주는 메서드를 함께 제공하는 것이 중요합니다. 예컨대 쇼핑몰에서 물건을 구입하려는 데 카드 잔고가 부족하여 검사 예외가 발생했다고 해봅시다. 그렇다면 이 예외는 잔고가 얼마나 부족한지를 알려주는 접근자 메서드를 제공해야 합니다.

프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하라

비검사 throwable은 두 가지로, 바로 런타임 예외와 에러입니다. 둘 다 동작 측면에서는 다르지 않습니다. 이 둘은 프로그램에서 잡을 필요가 없거나 혹은 통상적으로는 잡지 말아야 합니다. 프로그램에서 비검사 예외나 에러를 던졌다는 것은 복구가 불가능하거나 더 실행해봐야 득보다는 실이 많다는 뜻이기 때문입니다.

이 두가지 비검사 throwable 중에서 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용해야 합니다. 런타임 예외의 대부분은 전제조건을 만족하지 못했을 때 발생합니다. 전제조건 위배란 단순히 클라이언트가 해당 API의 명세에 기록된 제약을 지키지 못했다는 뜻입니다. 예컨대 배열의 인덱스는 0에서 ‘배열 크기 -1’ 사이어야 합니다. ArrayIndexOutOfBoundsException이 발생했다는 건 이 전제조건이 지켜지지 않았다는 뜻입니다.

Error 클래스는 사용하지 말자

위에서 말씀드렸든 Error 는 보통 JVM이 자원 부족, 불변식 깨짐 등 더이상 수행을 계속할 수 없는 상황을 나타낼 때 사용합니다. 따라서 JVM 관련 상황에서만 사용되므로 Error 클래스를 상속해 하위 클래스를 만드는 일은 자제해야 합니다. 다시 말해 우리가 구현하는 비검사 throwable은 모두 런타임 예외(RuntimeException)의 하위 클래스여야 합니다.

Objective-C

Objective-C의 예외 및 에러 종류

Objective-C 에서는 에러가 크게 NSError와 NSException이 있습니다.

NSException *exception = [[NSException alloc] initWithName:NSRangeException reason:@"" userInfo:nil];

=> Objective-c 에서는 NSRangeException이 에러 이름(스트링 타입)으로서 존재합니다.
=> Swift에서는 런타임 예외가 이름, 즉 객체로서 있지 않습니다. 신기하죠?

NSException도 자바의 비검사 예외와 마찬가지로 @try - @catch 문으로 해결할 수 있습니다. 하지만 프로그래머의 실수를 이렇게 해결하는 건 좋지 않은 방법입니다.

@try {
    NSArray *array = [[NSArray alloc] initWithObjects:@"aaa", nil];
    NSString *result = [array objectAtIndex:5];
} @catch (NSException *exception) {
    NSLog(@"%@ is exception name", exception.name);
    NSLog(@"%@ is exception reason", exception.reason);
      
} @finally {
    NSLog(@"finally");
}

참고한 곳: https://stackoverflow.com/questions/11100951/nsexception-and-nserror-custom-exception-error, https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Introduction/introObjectiveC.html#//apple_ref/doc/uid/TP30001163

Swift

Swift의 에러 및 예외 종류

Swift의 에러 및 예외는 크게 Error 와 Runtime Exception으로 나눌 수 있겠습니다. 사실 런타임 예외는 이름으로서, 즉 객체로서 존재하지도 않지만 Runtime Exception이라고 칭하겠습니다.

NOTE: Ounchecked 빌드란? (by Lena)

처리 가능한 예외 상황이라 여겨지면 Error(enum type)을 사용하세요

위에서 말씀드린 것처럼 처리 가능한 에러들로 무조건 에러 처리를 해야 합니다(처리 안하면 컴파일 에러 발생). 따라서 API 설계자는 처리 가능한 에러라면 Error를 사용해 사용자가 에러를 처리하도록 해야 합니다.

처리 불가능한 프로그래밍 오류(자바에서의 미확인 예외)라면 사용할 예외 및 예외가 없습니다. 옵셔널로 예외 상황을 예방하세요.

위에서 언급했듯 스위프트의 경우 런타임 예외 상황을 처리할 수 있는 객체, 즉 예외 및 에러가 존재하지 않습니다. 따라서 nil 런타임 에러나 array index 관련 런타임 에러를 처리할 수 없고 크래시가 발생할 수 밖에 없습니다. 따라서 옵셔널을 활용해 런타임 에러, 즉 크래시가 발생하지 않도록 예방하십시오.

스크린샷 2021-05-27 오후 7 02 22

Optional 사용하기

extension Collection {
    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

=> 이처럼 Optional를 잘 활용하면 런타임 에러 상황을 예방할 수 있습니다.

참고한곳 : https://stackoverflow.com/questions/25329186/safe-bounds-checked-array-lookup-in-swift-through-optional-bindings/30593673#30593673, https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html

번외: 비동기 상황에서의 에러 던지기

클로저에서 throw 사용하기

enum BadLuckError: Error {
    case unlucky
}

func incNum(num: Int, completion: (Int) throws -> Void) rethrows {
    // "Network call"
    try completion(num + 1)
}

func callingFunction(num: Int, closure: (Int, (Int) throws -> Void) throws -> Void ) throws {
    try closure(num, { result in
        print (result)
        if result == 13 {
             throw BadLuckError.unlucky
        }
    })
}

do {
    try callingFunction(num: 12, closure: incNum)
} catch {
    print ("DONE")
}

@escaping closure

completionHandler(data)
failureHandler(error)

Result Type

enum Result<Success, Failure> where Failure: Error {
  case success(Success)
  case failure(Failure)
}
struct Tutorial {
  let title: String
  let author: String
}
enum TutorialError: Error {
  case rejected
}
func feedback(for tutorial: Tutorial) -> Result<String, 
                                                TutorialError> {
  Bool.random() ? .success("published") : .failure(.rejected)
}
func edit(_ tutorial: Tutorial) {
    let result = feedback(for: tutorial)
    switch result {
    case let .success(data):
        print("\(tutorial.title) by \(tutorial.author) was \(data) on the website.")
        
    case let .failure(error):
        print("\(tutorial.title) by \(tutorial.author) was \(error).")
    }
}
let tutorial = Tutorial(title: "What’s new in Swift 5.1", author: "Cosmin Pupăză")
edit(tutorial)