ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Error Handling
    iOS/Swift 공식문서 2022. 2. 22. 20:39

    Error Handling

    프로그램에서 에러 발생 조건에 응답하여 회복하는 과정을 말한다. Swift는 error를 throw, catch, propagate, manipulate하기 위한 first-class support를 제공한다.


    일부 operation의 경우 항상 완전한 실행을 하거나 유용한 결과를 만든다는 보장이 없다. 값이 없는 상태를 나타내기 위해 optional을 사용하지만, operation이 실패할 경우, 그 원인을 파악하여 코드가 이에 맞게 응답을 할 수 있도록 하는 것이 유용하다.


    note

    Swift에서의 에러 처리는 Cocoa와 Objective-C에서의 NSError 클래스를 사용하는 에러 처리 패턴과 상호작용한다.


    Representing and Throwing Errors

    Swift에서 에러는 Error 프로토콜을 따르는 타입의 값으로 나타낸다. Error 프로토콜은 해당 타입이 에러 처리에 사용될 수 있음을 나타내준다.


    열거형은 서로 관련된 에러 그룹을 모델링하고, 에러에 대한 추가적인 정보를 전달하기에 적합하다. 예를 들어 게임 안에서 자판기를 작동시킬 때 에러 조건들을 다음과 같이 나타낼 수 있다.

    enum VendingMachineError: Error {
        case invalidSelection
        case insufficientFunds(coinsNeeded: Int)
        case outOfStock
    }

    에러를 던지는 것은 예상치 못한 상황이 발생하여 정상적인 실행 플로우를 계속할 수 없음을 나타내준다. 다른 언어처럼 throw 키워드를 사용한다.

    throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

    Handling Errors

    에러를 던지는 경우, 주변의 코드가 에러 처리를 담당해야 한다. Swift에서 에러를 처리하는 방법은 4가지가 있다. 첫번째로 에러를 함수에서 함수를 호출하는 코드로 propagate 하는 방법, do-catch 문을 사용하는 방법, 에러를 optional 값으로 처리하는 방법, 에러가 발생하지 않을 것이라고 assert를 하는 방법이 있다.


    note

    Swift의 에러 처리는 try, catch, throw 키워드를 사용한다는 점에서 다른 언어와 비슷하나, 다른 언어와는 달리 많은 계산이 필요할 수 있는 call stack 되돌리기(unwinding)를 하지 않는다. 그래서 throw 문의 성능은 return 문의 성능과 비슷하다.


    Propagating Errors Using Throwing Functions

    함수, 메서드, initializer가 에러를 던질 수 있게 하려면, 함수 선언에서 매개변수 뒤에 throws 키워드를 적어야 한다. 이 키워드가 있는 함수를 throwing function이라 한다.

    func canThrowErrors() throws -> String
    func cannotThrowErrors() -> String

    함수에서 전파되는 에러는 함수가 호출된 스코프 안으로 던져진다.


    note

    throwing function만 에러를 전파할 수 있다. non-throwing function에서 던져진 에러는 그 함수 안에서 처리되어야 한다.


    아래와 같이 VendingMachine 클래스를 정의하자. vend(itemNamed:) 메서드의 경우 요청된 아이템이 존재하지 않는 경우, 재고가 없는 경우, 혹은 현재 넣은 비용보다 가격이 비쌀 경우 에러를 던진다. guard를 통해 필요한 조건을 만족하지 못할 경우 throw 문이 실행된다.

    struct Item {
        var price: Int
        var count: Int
    }
    
    class VendingMachine {
        var inventory = [
            "Candy Bar": Item(price: 12, count: 7),
            "Chips": Item(price: 10, count: 4),
            "Pretzels": Item(price: 7, count: 11)
        ]
        var coinsDeposited = 0
    
        func vend(itemNamed name: String) throws {
            guard let item = inventory[name] else {
                throw VendingMachineError.invalidSelection
            }
    
            guard item.count > 0 else {
                throw VendingMachineError.outOfStock
            }
    
            guard item.price <= coinsDeposited else {
                throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
            }
    
            coinsDeposited -= item.price
    
            var newItem = item
            newItem.count -= 1
            inventory[name] = newItem
    
            print("Dispensing \(name)")
        }
    }

    vend(itemNamed:)에서는 던지는 모든 에러를 전파하기 때문에, 해당 메서드를 호출하는 코드는 에러를 처리하거나, 계속 에러를 전파해야 한다. 아래의 buyFavoriteSnack 함수의 경우 에러를 또 다시 전파한다.

    let favoriteSnacks = [
        "Alice": "Chips",
        "Bob": "Licorice",
        "Eve": "Pretzels",
    ]
    func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
        let snackName = favoriteSnacks[person] ?? "Candy Bar"
        try vendingMachine.vend(itemNamed: snackName)
    }

    initializer도 같은 방식으로 에러를 전파할 수 있다.

    struct PurchasedSnack {
        let name: String
        init(name: String, vendingMachine: VendingMachine) throws {
            try vendingMachine.vend(itemNamed: name)
            self.name = name
        }
    }

    Handling Errors Using Do-Catch

    에러를 직접 처리하기 위해서는 do-catch 문을 사용해야 한다. 에러가 do 구문 안에서 던져진다면, 에러를 처리할 수 있는 알맞은 catch 구문에 매치된다. do-catch 문의 일반적인 형태는 다음과 같다.

    do {
        try expression
        statements
    } catch pattern 1 {
        statements
    } catch pattern 2 where condition {
        statements
    } catch pattern 3, pattern 4 where condition {
        statements
    } catch {
        statements
    }

    do 구문 안에 에러가 발생할 수 있는 expression 앞에 try 키워드를 작성한다. catch 뒤에는 패턴을 적어서 해당 구문이 처리할 수 있는 에러가 무엇인지 알려준다. catch 뒤에 패턴이 없다면, 모든 에러를 지역 상수인 error로 바인딩한다.


    아래와 같이 buyFavoriteSnack 함수를 호출하여 에러를 처리하는 코드를 작성할 수 있다.

    var vendingMachine = VendingMachine()
    vendingMachine.coinsDeposited = 8
    do {
        try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
        print("Success! Yum.")
    } catch VendingMachineError.invalidSelection {
        print("Invalid Selection.")
    } catch VendingMachineError.outOfStock {
        print("Out of Stock.")
    } catch VendingMachineError.insufficientFunds(let coinsNeeded) {
        print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
    } catch {
        print("Unexpected error: \(error).")
    }
    // Prints "Insufficient funds. Please insert an additional 2 coins."

    catch 구문이 발생할 수 있는 모든 에러를 처리해야 하는 것은 아니다. catch 구문 중 그 어느것도 에러를 처리하지 못한다면, 상위 스코프로 에러를 전파한다. 따라서 해당 에러가 상위 스코프에서 처리되어야 한다. 물론 이는 throwing function에만 해당된다. non-throwing function의 경우에는 에러가 발생하면 반드시 그 함수에서 처리해야 한다. 스코프가 전파되다가 top-level scope에서도 처리되지 않는다면 runtime error가 발생한다.


    다음 코드의 nourish와 같이 일부 에러만 처리하고 나머지 에러는 전파할 수 있다.

    func nourish(with item: String) throws {
        do {
            try vendingMachine.vend(itemNamed: item)
        } catch is VendingMachineError {
            print("Couldn't buy that from the vending machine.")
        }
    }
    
    do {
        try nourish(with: "Beet-Flavored Chips")
    } catch {
        print("Unexpected non-vending-machine-related error: \(error)")
    }
    // Prints "Couldn't buy that from the vending machine."

    여러 에러를 똑같이 처리해야 한다면 catch 구문에 여러 에러들을 괄호로 구분하여 적으면 된다.

    func eat(item: String) throws {
        do {
            try vendingMachine.vend(itemNamed: item)
        } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
            print("Invalid selection, out of stock, or not enough money.")
        }
    }

    Converting Errors to Optional Values

    에러를 optional 값으로 변환할 때는 try?문을 사용할 수 있다. try? expression을 평가하다가 에러가 발생하면, 해당 expression의 값은 nil이 된다.

    func someThrowingFunction() throws -> Int {
        // ...
    }
    
    let x = try? someThrowingFunction()
    
    let y: Int?
    do {
        y = try someThrowingFunction()
    } catch {
        y = nil
    }

    다음과 같이 optional binding과 같이 활용할 수 있다.

    func fetchData() -> Data? {
        if let data = try? fetchDataFromDisk() { return data }
        if let data = try? fetchDataFromServer() { return data }
        return nil
    }

    Disabling Error Propagation

    throwing function, method가 실제로는 에러를 던지지 않는다는 것을 안다면, expression 앞에 try!를 적고, 에러가 던져지지 않을 것이라는 runtime assertion을 적을 수 있다. 에러가 실제로 던져진다면, runtime error가 발생한다.

    let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

    Specifying Cleanup Actions

    defer 문을 작성하면 현재 코드 블럭을 떠나기 직전에 일련의 코드들을 실행하도록 할 수 있다. 어떠한 이유로 코드 블럭을 떠나더라도 실행되어야 하는 클린업 코드를 작성할 때 유용하다.


    defer문은 현재 실행이 연기되다가, 현재 스코프의 실행이 끝날 때 실행된다. 여러 defer문이 있는 경우 마지막 defer문부터 bottom-up으로 실행된다.


    앞에서도 설명했듯이, defer문은 다음과 같이 열려있는 파일을 에러가 발생하더라도 닫게 해야만 할 때와 같이 반드시 수행해야 하는 클린업 코드를 작성할 때 유용하다. defer문은 에러 처리를 하지 않더라도 사용이 가능하다.

    func processFile(filename: String) throws {
        if exists(filename) {
            let file = open(filename)
            defer {
                close(file)
            }
            while let line = try file.readline() {
                // Work with the file.
            }
            // close(file) is called here, at the end of the scope.
        }
    }

    Reference

    https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html

    'iOS > Swift 공식문서' 카테고리의 다른 글

    Type Casting  (0) 2022.02.23
    Concurrency  (0) 2022.02.23
    Optional Chaining  (0) 2022.02.22
    Deinitialization  (0) 2022.02.21
    Initialization  (0) 2022.02.21

    댓글

Designed by Tistory.