[Swift] 17. Error Handling

도윤·2021년 8월 9일
0

Swift

목록 보기
15/21

애플 공식 문서를 참조하여 작성했습니다!


Error Handling

오류 처리는 프로그램의 오류 상태에 응답하고 복구하는 프로세스이다. swift는 런타임에 복구 가능한 오류를 throwing,catching,propagating,manipulating하는 일급 클래스를 제공한다.

일부 작업은 항상 실행을 완료하거나 유용한 출력을 생성하지 못할 수 있습니다. Optionals은 값의 부재를 나타내기 위해 사용되지만, 작업이 실패했을 때 실패의 원인이 무엇인지 이해하는 것이 유용하여 코드가 적절하게 응답할 수 있습니다.


Represnting and Throwing Errors

Swift에서 오류는 Error 프로토콜을 준수하는 타입 값으로 표현된다. Swift의 열거형은 오류의 원인들을 나누고 해당 오류들의 특성에 대한 추가 정보를 전달하는 모델을 만드는게 적합하다.

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

Throwing Error는 예기치 못한 상황이 발생하여 현재 상황을 진행할 수 없을 때 그 상황을 알리는 에러이다. throw선언을 통해 아래처럼 사용하면 된다.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

Handling Errors

에러가 발생했을 때 코드들은 문제를 파악하고 대체 접근을 하고 유저들에게 문제를 알린다. swift에는 에러를 다루는 4가지의 방법이 있다. Propagate the error(오류를 전파),do-catch 구문 사용, 오류를 옵셔널 값으로 처리, 오류가 발생하지 않을 것이라고 정의하는 방법.

오류가 발생하면 프로그램의 흐름이 변경되기 때문에 코드에서 오류가 발생할 수 있는 위치를 빠르게 식별할 수 있어야 한다. 코드에서 이러한 오류를 식별하려면 try,try!,try? 키워드를 사용하면 된다.

Propagating Errors Using Throwing Functions

함수,메서드,생성자에서 오류가 발생할 수 있음을 알리기 위해 throw키워드를 함수 선언 시 매개변수 뒤에 작성하면 된다. throws가 작성된 함수를 Throwing Function이라고 하며, 만약 반환 타입이 있다면 반한 값의 타입 앞에 throws를 써주면 된다.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

VendingMachine 클래스에서 발생할 수 있는 오류를 처리하기 위한 vend(itemNamed name:)메서드이다.gaurd문과 throw를 사용하여 오류를 처리한다. gaurd문의 코드가 오류가 발생하면 else문을 실행하여 throw VendingMachinError 오류를 발생되고 메서드가 종료된다. 즉 세개의 gaurd문이 있어서 세 조건문이 모두 오류가 발생하지 않아야 다음 코드가 수행된다. 이 메서드를 호출하면 오류가 발생할 수도 있기 때문에 do-catch,try,try?,try!를 사용하여 오류를 처리하거나 전파해야 한다.

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)")}
}

Throwing 생성자는 함수에서 오류를 발생하는 것과 같은 방식으로 오류를 전파할 수 있다. 예를 들어 위의 코드에서 PurchaedSnack구조체에서 생성자는 초기화 프로세스의 일부로 throwing 함수를 호출하게 되는데 만약 오류가 발생한다면 해당 오류를 호출자에게 전파하여 처리할 수 있게 한다.

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

Handling Errors Using Do-Cath

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
}
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절에서 전파를 진행할지 말아얄지를 결정한다. 만약 어떠한 패턴도 맞는게 없다면 최종 catch절에서 에러는 local error 상수에 바인딩된다. 오류가 발생하지 않으면 do문에 있는 나머지 문이 실행된다. catch절에서 do절에서 throw한 코드에서 발생한 모든 에러를 다룰 순 없다. 만약 catch절 중 어떠한 것도 에러를 다룰 수 없다면 에러는 주변 scope에 전파된다. 그러나 전파된 에러는 반드시 처리되어야 한다.

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.

VendingMachinError를 처리하도록 정의되어 있다. 하지만 그 외의 오류는 처리하지 않고 전파만 하기 때문에 실제로 사용할 때 do-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.")
    }
}

여러 개의 오류를 동일하게 처리하는 방법은 catch 뒤에 오류의 종류를 나타낼 때 쉼표로 구분하는 것이다. 위의 코드처럼 만들게 도미ㅕㄴ 세가지 오류에 대해 동일한 처리를 할 수 있다.

Converting Errors to Optional Values

optional value를 다룰 때 try?를 사용하면 된다. try? 코드를 평가하는 동안 오류가 발생하게 되면 해당 코드의 값은 nil이 된다.

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

someThrowingFunction에서 에러가 발생한다면 x,y값은 nil이 된다. 그렇지 않으면 함수가 반환 값을 가지게 된다. 여기서 오류가 발생하지 않더라도 x,y는 Int?타입이다.

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

try?를 사용하면 모든 오류를 동일한 방법으로 처리하고자 할 때 간결하게 코드를 작성할 수 있다. 위의 코드처럼 작성하게 되면 함쉐서 발생하는 모든 방법을 동일하게 처리할 수 잇고 실패하면 nil값이 반환된다.

Disabling Error Propagation

만약 throwing함수나 메서드가 런타임에 오류를 발생하지 않는 상황임을 확신한다면 try!를 사용하면 된다. 하지만 이 경우 오류가 발생하게 되면 런타임 오류가 발생한다.

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

위의 코드는 이미지 경로를 지정하는 코드이고 여기서 사용된 loadImage 함수는 오류를 발생할 가능성이 있다. 하지만 개발자가 오류가 절대 발생하지 않을 것이라도 확신한다면위 처럼 try!를 사용하면 된다.


Specifying Cleanup Actions

코드 실행이 현재 코드 블록을 떠나기 직전에 defer문을 사용하여 특정 코드를 실행할 수 있다.
이 문을 사용하면 오류가 발생했거나 반환 또는 중단과 같은 명령으로 인해 실행이 현재 코드 블록을 떠나는 방식에 관계없이 필요한 모든 정리를 수행할 수 있습니다.

예를 들어 오류가 발생하여 종료하거나 Return, break와 같은 명령문에 의해 종료되는 경우 defer문을 사용하여 파일 디스크럽터가 닫히고 메모리 할당을 수동으로 해제할 수 있다.

defer문은 현재 범위를 종료할 때까지 실행을 연기합니다. 이 구문은 defer키워드와 나중에 실행할 코드로 구성된다.

즉, 첫 번째 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.
   }
}

defer문을 사용하여 open(:),close(:)함수가 함께 호출되도록 한다. defer문은 오류 처리 코드를 포함하지 않아도 사용할 수 있다.

0개의 댓글