[Swift] Error Handling

상 원·2022년 8월 7일
0

Swift

목록 보기
31/31
post-thumbnail

오류가 났을 때 반응하고 복구할 수 있도록 하는 행위가 error handling임!
Swift는 요 에러들을 던지고(throw), 받고(catch), 전파하고(propagate), 조작(manipulate)할 수 있도록 일급 지원을 해 준다.

몇몇 작업은 끝까지 실행이 되거나 쓸모있는 결과가 나오지 않을 수도 있다. 그러면 이게 왜 실패했는지 알아내서 코드가 그에 맞게 동작할 수 있도록 짜주면 좋음!

만약에 디스크에 있는 파일을 읽고 데이터를 가공하는 작업을 실행했을 때, 이 작업이 실패할 경우는 엄청 많음. 경로에 파일이 없거나, 접근 권한이 없거나, 인코딩이 똑바로 안 돼 읽지 못하는 경우 등등,, 이런 경우의 에러들을 모두 분석하고 구분해서 해결하고 사용자에게 해결하지 못한 에러들을 알려줘야 한다. 그것이 에러 핸들링이니까..

Representing and Throwing Errors

에러는 Error 프로토콜을 따르는 값 타입으로 나타남. 바로 이 타입으로 에러 핸들링을 할 수 있다!

열거형은 연관된 에러 조건들을 나타내는 데 좋음. 여기에 associated value들을 사용해 오류의 특성에 대한 추가정보를 넣을 수도 있다.


게임 안의 자판기에서 일어날 수 있는 오류들을 모아 놓은 열거형이다.

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

Error protocol 을 준수하는 열거형이다.

에러를 "throw" 한다는 건 예상치 못한 일이 벌어졌다는 것이고, 정상적인 코드 흐름이 불가능해졌다는 것을 의미함. throw 구문을 이용해서 에러를 던져줄 수 있다.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

이 코드로는 5개의 코인이 더 필요하다는 에러를 던져줄 수 있음.

Handling Errors

에러가 던져졌으면 이걸 받아서 해결해줄 코드가 필요함.
4가지 방법이 있는데,

  • 에러를 해당 함수를 호출하는 다른 함수로 전파하는(propagating) 방법이나
  • do-catch 구문을 활용해 에러를 핸들링하거나
  • 에러를 옵셔널 값으로 받거나
  • 오류가 발생하지 않을 거라고 강요하는 방법이 있음.

함수가 에러를 던져줬을 때, 프로그램의 플로우를 바꾸므로 코드에서 에러를 던지는 부분이 어딘지 빠르게 알아내는 게 중요함. 이 부분을 알아내려면 에러가 발생할 수 있는 함수나 메소드, 이니셜라이저 앞에 trytry? 또는 try! 를 써주면 된다.

참고

Swift의 오류 처리는 다른 언어의 예외 처리와 비슷하다.
하지만 다른 언어의 예외 처리와는 다르게 Swift는 call stack을 풀지 않기 때문에 계산 시간이 엄청 줄어들고, 결국 return문의 성능과 비슷해짐. 더 좋다는 것이다~

Propagating Errors Using Throwing Functions

위에서 말했던 에러를 받아 해결하는 네 가지 방법 중 첫번째이다.

함수나 메소드, 이니셜라이저가 에러를 던질 수 있다는 걸 알려주기 위해서는 함수 선언에서 매개변수 뒤에 throws 키워드를 써줘야 한다.
이렇게 선언한 함수를 throwing function이라고 한다.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

이런식으로 에러를 던질 수 있는 함수를 선언할 수 있음.

참고

throwing function만 에러를 전파할 수 있다. 아니면 해당 함수 안에서 에러를 해결해야 함.


이번 예시에서는 VendingMachine 클래스의 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 문을 이용해 과자 구매의 요구사항이 들어맞지 않을 때 에러를 던지고 메소드를 탈출한다. throw 문이 프로그램 제어를 즉시 변경해주므로 모든 요구사항이 충족될 때만 아이템이 나오게 됨!

이 메소드가 에러를 전파해주므로, 이 메소드를 호출하는 모든 코드는 에러를 do-catch, try?, try! 중 하나를 사용해 핸들링하거나 에러를 무시하고 continue할 수 있어야 한다.

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 함수는 throwing function인데, vend(itemNamed: ) 함수가 던져주는 모든 에러는 buyFavoriteSnack 함수가 호출된 지점으로 전파돼 올라갈 것이다.
이 함수는 매개변수로 전달된 사람의 최애 간식을 vend 함수를 이용해 사려고 할 것인데, vend 함수가 에러를 뱉어낼 수 있으므로 앞에 try를 붙이는 방식으로 코드를 작성해줌.

Throwing Initializer는 throwing function과 같이 에러를 전파해줄 수 있다.

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

이 구조체의 이니셜라이저는 초기화 단계에서 throwing function을 호출하고, caller에게 에러를 전파해준다.

그니까 try를 적어놓은 건 에러가 발생할 수도 있다~ 라고 적어놓은 거고, 그 이후의 연산을 그냥 수행하는데 에러가 발생하면 이걸 호출한 곳으로 에러를 던져주는 거!

Handling Errors Using Do-Catch

do-catch 구문을 이용해서 코드 조각을 실행하는 방식으로 에러를 핸들링할 수도 있다.
do 안에 있는 코드에 의해 에러가 throw되면, catch 에 의해 어떤 코드로 에러를 핸들링할지 정해질 수 있다.

위와 같은 방식으로 expression에 대한 연산을 수행하고, 에러가 발생하지 않으면 바로 아래의 statement를 실행, 에러가 발생한다면 에러 패턴에 따라 catch문의 statement가 수행되는 방식이다. 패턴이 없는 catch문은 위에서 적용되지 않은 나머지 모든 에러를 받음!

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

여기서 코인은 8개, Alice의 최애 간식인 chips는 10개의 cost를 갖고 있기 때문에 코인이 부족한 오류를 뱉어내므로 insufficientFunds에 해당한다.


catch 문에서 모든 에러를 잡아낼 필요는 없음. 여기서 처리되지 않은 에러는 'surrounding scope'으로 전파돼 올라가서 해결돼야 함.
nonthrowing function에서는 에러를 전파할 수가 없기 때문에 여기 안에 있는 do-catch 문에서 에러를 해결해야 하지만,
throwing function에서는 해당 함수를 호출한 caller가 에러를 해결할 수도 있다.

근데 계속 전파돼 올라가서 top-level 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."

이 예제에서는 nourish 함수가 VendingMachinError 인 에러는 모두 catch해서 처리할 수 있고, 해당되지 않는 에러는 다시 빠져나와서 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

try? 를 사용해서 에러를 옵셔널 값으로 바꿔서 처리할 수도 있다.
이 구문을 사용했을 때 발생하는 에러의 값은 nil로 처리된다. 아래 코드에서 x와 y의 값은 동일함.

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

let x = try? someThrowingFunction()

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

이렇게 에러가 발생하면 nil이 저장되고, 아니라면 함수가 리턴하는 값을 x와 y가 갖게 된다.

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

모든 에러를 같은 방법으로 간결하게 처리하고 싶을 때 try? 를 사용하기도 함.
위와 같은 방식으로 데이터를 받기 위해 몇 번을 접근하는데, 모든 접근이 실패하면 nil을 반환하게 된다.

Disabling Error Propagation

메소드나 함수가 실행 중에 에러를 throw하지 않을 것을 알고 있을 때가 있다.
이때 try! 를 사용해서 에러 전파를 막아버리고 실행 중 에러가 발생하지 않을 거라는 것을 표현할 수 있음. 에러가 실제로 발생해버리면 런타임 에러가 발생한다.

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

이때 이미지는 앱에 포함이 돼있기 때문에 실행 중에 에러가 발생하지 않는다는 것을 알고 있어 try! 를 사용해서 래핑했다.

Specifying Cleanup Actions

defer 키워드를 사용하면 현재 코드 블럭의 실행이 끝나기 직전에 실행된다.
defer은 "연기하다"라는 뜻임! 이 구문을 사용하면 해당 코드블럭이 어떻게 종료되는지에 상관없이 종료 직전에 필요한 cleanup을 할 수 있다.
파일 디스크립터가 닫혔는지, 또는 할당된 메모리가 모두 free됐는지 알아보기 위해 사용할 수도 있음!

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

요 코드는 함수가 종료되기 직전에 열었던 파일을 닫아주는 것을 보장하는 코드임!

참고

error handling이 없는 코드에도 defer문을 사용할 수 있당.

profile
ios developer

0개의 댓글