Error Handling이란?

Error Handling은 프로그램에서 에러 상태로부터 응답하고 회복하는 과정이다.
Swift는 런타임에 회복가능한 오류를 throwing, catching, propagating, and manipulating하는 first-class를 제공한다.

Error Handling은 왜 필요한가?

  • 모든 코드는 100% 원하는대로 동작한다고 확신할 수 없다.
  • 에러가 발생했을 때 오류의 원인을 쉽게 파악할 수 있다.

Swift는 옵셔널 타입이 있어 값이 없음을 나타낼 수 있다. 하지만 작업이 실패할 경우를 옵셔널로 나타내는 것보다 왜 이런 에러가 발생했는지 등으로 나타내는 것이 더 적합할 때가 있다. 이를 Error Handling을 통해 나타낼 수 있고 더 나아가 코드의 동작이 실패했을 때 그에 따라 적절히 동작하기 위해 존재한다.

디스크에 파일이 들어있는데, 그 파일의 데이터를 읽고 처리하는 작업이 있다고 생각해보자. 이 작업에 대해서 굉장히 많은 작업 오류가 발생할 수 있다.
지정된 경로에 파일이 존재하지 않다거나, 데이터를 읽을 수 있는 권한이 없거나, 데이터가 호환되지 않는 형식으로 인코드되거나...

이러한 발생할 수 있는 다양한 상황을 구분하면, 프로그램은 "아 이거 나 처리 못하겠는데?", "아 이 에러 좀 처리해줘", "나 지금 문제 생겨서 다음 동작 못 할 것 같아!" 와 같이 사용자에게 이야기할 수 있다.

Swift의 Error Handling은 Cocoa와 Objective-C의 NSError 클래스를 사용하는 Error Handling과 같이 동작한다.


그래서 Error Handling 어떻게 하는데?

Swift에서의 Error는 Error 프로토콜을 준수하는 타입의 형태로 나타낼 수 있다. Swift의 열거형은 에러 조건들과 관련된 그룹으로 모델링하기에 적절하며, 에러의 특성에 대한 정보를 담아 전달할 수 있다.
자판기가 작동할 때 오류조건을 다음과 같이 나타낼 수 있다.

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

에러는 throw 구문을 사용하여 나타낼 수 있다. 에러를 "던지다", "발생시키다"의 의미인 것 같다.
다음 코드는 "자판기에 5개의 동전이 더 필요해!" 라는 에러를 발생시킨다.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

에러(주사위)는 던져졌다... 그렇다면 에러를 처리할 친구들이 있어야한다. 에러를 해결하는 친구도 있을 것이고, 다른 방식으로 접근하여 원하던 동작을 하려는 친구도 있을 것이며, 사용자에게 에러가 발생했음을 알리는 친구들도 있을 것이다.

Swift에서 에러를 처리하는 방법으로는 4가지 방법이 있다.

  • 다른 함수를 이용하여 처리하거나, 전파시키거나
  • do-catch문을 사용하여 에러를 처리하거나
  • 에러를 그냥 옵셔널로 처리해버리거나
  • 에러가 절대 발생하지 않을 것이라고 확정지어버리거나..?

함수가 던진(throw) 에러는 프로그램의 실행 흐름을 바꿀 수 있기때문에 어디서 에러를 던질 수 있는지 알아야한다. 이를 알기 위해서 코드 앞에 try, try?, try! 키워드를 사용한다.


함수를 이용해 에러를 처리 또는 전파(?)

함수가 에러를 발생시킬 수 있음을 나타내기 위해 throws 키워드를 함수의 파라미터 뒤에 작성한다. 만약 함수가 리턴 값을 가지고 있을 경우 리턴 화살표 -> 뒤에 작성한다.

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

만약 throws 키워드를 붙이지 않았다면 함수 내부에서 에러를 처리해야한다.

아래의 예에서 VendingMachine class의 vend(itemNamed:) 함수는 VendingMachineError를 발생시킬 수 있다.
(요청한 아이템을 사용할 수 없거나, 재고가 없거나, 돈이 없거나)

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:) 함수는 guard 구문을 사용하여 특정 조건이 맞지 않을 경우 에러를 발생시키고 함수를 빠르게 종료할 수 있다. vend(itemNamed:) 함수를 호출하는 친구는 vend(itemNamed:)가 던지는 에러를 처리하거나 전파시켜야한다.

전파라는 단어가 처음 나왔다.
아래 예시를 보면 전파시킨다는 의미를 알 수 있다.

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

buyFavoriteSnack(person:vendingMachine:) 함수 역시 에러를 발생시킬 수 있는 함수이다. 앞의 vend(itemNamed:) 함수가 던졌던 에러를 받아 또 다른 함수에게 던지는(전파시키는) 것이다.
마찬가지로 buyFavoriteSnack(person:vendingMachine:) 함수를 호출한 친구 역시 에러를 처리하거나 또 다른 누군가에게 전파시켜야한다. 만약 에러가 계속 전파되어 제일 처음 호출한 친구에게까지 전파되고 이 친구조차 에러를 해결하지 않는다면 런타임 에러가 발생하게 된다.

함수뿐만 아니라 이니셜라이저 또한 에러를 던질 수 있다.

PurchasedSnack 구조체를 초기화하면서 에러가 발생했을 경우 PurchasedSnack를 초기화한 친구에게 전파시켜 에러를 처리(또는 전파)할 수 있도록 한다.

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

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
}

어떤 에러를 처리할 수 있는 친구인지 나타내기 위해 catch 구문 뒤에 패턴을 작성한다.
코드에서 마지막의 catch는 패턴이 없는데, 이는 위에서 다 걸려져서 나온 패턴이며, switch 구문의 default와 비슷한 것 같다.

앞서 나온 함수들을 이용하여 에러 처리 하면 다음과 같이 표현할 수 있다.

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

buyFavoriteSnack(person:vendingMachine:) 함수는 에러를 발생시킬 수 있는 친구이기 때문에 앞에 try 키워드를 작성해야 호출할 수 있다. 또한 do 블럭 내부에 있기 때문에 이 함수에서 에러가 발생한다면 catch 블럭에서 해당하는 에러 패턴에 맞게 처리할 수 있다. 에러가 발생하지 않는다면 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 이후에 작성된 에러들 중 하나라도 잡힌다면 에러 처리를 할 수 있다.


에러를 옵셔널로 처리

try? 키워드를 사용하여 에러를 발생시킬 수 있는 함수가 만약에 에러가 나왔다면 그냥 nil로 처리해버리는 것이다.

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

let x = try? someThrowingFunction()

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

위의 코드에서 x와 y는 같은 동작을 한다. someThrowingFunction() 함수에서 에러가 발생한다면 x와 y의 값은 nil이 된다. 에러가 발생하지 않는다면 함수가 반환하는 값인 Int타입의 값이 될 것이라 생각했지만, nil이 될 수 있으므로 Int? 타입의 값이 된다!!!

물론 try? 키워드를 사용하여 옵셔널 바인딩을 하여 에러를 처리하거나 nil을 반환할 수 있다.

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

꼭 에러를 발생시켜야하나?

에러를 발생시킬 수 있는 함수이지만 에러를 발생시키지 않고 싶을 때에는 try! 키워드를 사용한다. try! 키워드가 붙은 친구는 에러 전파를 하지않거나 에러가 발생했다는 것을 막을 수 있다. 하지만 이는 옵셔널 강제 추출과 비슷하게 위험한 방법이며, 실제로 에러가 발생했을 때에는 런타임 에러가 발생할 수 있다. 따라서 에러가 절대 발생하지 않을 것이라 확정 지을 경우 사용한다.

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
profile
iOS 개발자가 되고싶어요

0개의 댓글