[Swift] Error Handling

LEEHAKJIN-VV·2022년 5월 24일
0

Study-Swift 5.6

목록 보기
19/22

참고사이트:
English: The swift programming language
Korean: The swift programming language


Error Handling (에러 처리)

Error Handling(에러 처리)는 프로그램에서 에러가 발생했을 때 적절한 처리를 하는 과정이다. Swift는 에러 해결을 위해 throwing, catching, propagating, manipulating이 가능한 first-class(일급 클래스)를 제공한다.

몇몇의 연산 작업은 올바르게 실행되거나 올바른 출력을 항상 보장하지 않는다. 그럴 경우 Optional을 사용하여 값의 부재를 확인할 수 있지만 어떤 종류의 error가 발생했는지는 확인할 수 없다. 코드에서 에러 처리 관련 작업을 하기 위해서 에러의 발생 원인을 이해하는 것은 매우 중요하다.

예를 들어 디스크에 저장된 파일을 읽고 처리하는 작업이 있다고 가정한다. 읽을 경로에 파일이 존재하지 않거나, 읽기 권한이 없거나 또는 파일이 호환되지 않는 형태로 encoded 된 것처럼 이 작업이 실패할 수 있는 이유는 많다. 이러한 에러를 식별하면, 프로그램이 일부 에러를 해결하거나 해결할 수 없는 에러는 사용자에게 전달하여 적절한 조치를 취할 수 있게 한다.

NOTE
Swift의 error handling 패턴은 Cocoa와 Objective-C의 NSError 클래스와 상호 운영된다. 자세한 정보는 다음 문서를 확인한다. Handling Cocoa Errors in Swift.


Representing and Throwing Errors (에러의 표시와 스로잉)

Swift에서 error는 Error 프로토콜을 따르는 타입의 값으로 표현된다. Error 프로토콜은 빈 프로토콜이지만 error handling에 사용될 수 있다는 것을 나타낸다.

Swift의 열거형은 관련된 에러들을 modeling(그룹화) 하는 것에 적합하며, associated values(연관 값)을 이용하여 에러의 추가적인 정보를 나타낼 수 있다. 예를 들어 다음과 같이 게임 안의 자판기가 작업을 수행할 때 발생할 수 있는 여러 개의 error 들을 나타낼 수 있다.

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

error를 throwing 하는 것은 무언가 예기치 못한 상황이 발생하여 정상적인 실행을 할 수 없다는 것을 나타낸다. throw문을 사용하여 error를 throw 한다. 예를 들어 아래 코드는 자판기에 5개의 코인이 추가적으로 필요하다는 error를 throw 한다.

throw VendingMachieError.insufficientFunds(coinsNeeded: 5)

Handling Errors (에러 처리)

error가 던져졌을 때 던져진 코드 영역에서 error를 처리하도록 해야 한다. 예를 들어 error를 해결하거나, 다른 접근 방법을 시도하거나 또는 유저에게 실패를 알리는 것으로 처리할 수 있다.

Swift에서는 4가지 error handling 방법이 있다. error가 발생한 함수에서 해당 함수를 호출한 함수로 propagate(전파) 시키는 방법,do-cathch 문을 사용하는 방법, optional을 사용하는 방법, error가 발생하지 않는다고 명시 하는 방법 이 있다.(try!) 각각의 접근법은 아래 섹션에서 소개한다.

함수가 error를 throw 할 때, 프로그램의 실행 흐름을 변경하므로 error를 throw 할 수 있는 코드를 식별하는 것은 중요하다. 코드를 식별하기 위해서 error를 throw 할 수 있는 함수, 메소드 그리고 이니셜라이저 호출하는 코드 앞에 try, try? try! 키워드를 작성한다.

NOTE
Swift의 error handling은 다른 언어와 유사하고, try, catch 그리고 throws 키워드를 사용한다. 다른 언어의 error handling과 달리 Swift의 error handling는 수행하는 데 비용이 많이 드는 call stack unwinding (콜스택 되돌리기)를 수행하지 않는다. 따라서 Swift의 throw 구문은 일반적인 return 구문과 비슷한 성능을 가진다.


Propagating Errors Using Throwing Functions (Throwing 함수를 사용한 에러 전파)

함수, 메소드 또는 이니셜라이저가 error를 throw 할 수 있는 것을 나타내기 위해 함수의 파라미터 뒤에 throws 키워드를 작성한다. throws 키워드가 포함된 함수를 throwing function이라고 한다. 만약 함수가 특정 타입을 명시한다면 화살표 앞에 throws 키워드를 작성한다.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

throwing function은 함수 내부에서 발생한 error를 함수를 호출한 곳에 throw 한다.

NOTE
오직 throwing function만 error를 전파할 수 있다. nonthrowing function에 던져진 error들은 던져진 함수 내부에서 처리되어야 한다.

아래 예제를 살펴보면, VendingMachine 클래스는 요청된 아이템이 없거나 재고가 없거나 또는 현재 보유하고 있는 양보다 초과하는 비용이 있는 경우 적절한 VendingMachineError를 throw 하는 vend(itemNamed:)를 가진다.

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문을 사용하여 snack을 구매하는 과정에서 어떠한 요구사항이 만족하지 않으면 적절한 error를 throw 하고 빠르게 탈출한다. throw문은 error를 메소드를 호출한 코드로 즉시 제어의 흐름을 변경하므로 모든 조건들이 만족해야 아이템은 팔리게 된다.

vend(itemNamed:) 메소드는 발생한 모든 error를 전파하기 때문에 이 메소드를 호출한 모든 코드들은 do-cathc문, try?또는 try!을 사용하여 오류를 처리하거나 error을 다시 전파해야 한다. 에를 들어 아래 예제에서 buyFavoriteSnack(person:vendingMachine:) 메소드는 throwing function이며, vend(itemNamed:)에서 throw된 error들은 buyFavoriteSnack(person:vendingMachine:) 가 호출된 위치까지 다시 전파될 것이다.

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

이 예제에서 the buyFavoriteSnack(person: vendingMachine:) 함수는 사람이 좋아하는 snack을 찾고 vend(itemNamed:) 메소드를 호출하여 snack을 구매한다. vend(itemNamed:) 메소드는 error를 throw 할 수 있기 때문에, try 키워드를 앞에 작성하여 호출된다.

또한 throwing initializers도 throwing function과 같은 방식으로 error를 전파할 수 있다. 예를 들어 PurchasedSnack 구조체의 이니셜라이저는 error를 발생시키는 함수이다. 이 이니셜라이저가 실행될 때 발생한 오류는 이니셜라이저를 호출한 코드에게 전파된다.

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-catch문의 일반적인 형태이다.

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

catch 뒤에 처리할 error의 종류를 작성하고, 어떻게 처리하는지 명시할 수 있다. 만약 catch문 뒤에 error 종류를 작성하지 않으면 발생하는 모든 error를 지역 상수인 error로 바인딩 한다. 패턴 매칭에 관한 자세한 정보는 다음 문서를 확인한다. Patterns

예를 들어 아래 코드는 열거형 VendingMachineError에서 발생하는 에러 3가지를 처리하는 코드이다.

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

위 예제에서buyFavoriteSnack(person:vendingMachine:) 함수는 error를 throw 할 수 있기 때문에 try 구문 내에서 호출된다. 만약 코드 실행 도중 error가 발생하면 catch 절로 이동하여 이를 전파할 것인지 아니면 이 구문 내에서 처리할 것인지 결정한다. 만약 발생한 error의 종류를 처리할 catch 구문이 없다면 마지막 catch절로 이동하여 지역 상수인 error에 바인딩 된다. 아무런 error도 thrown(전달되지) 않는다면 do구문의 나머지 코드 부분이 실행된다.

do 절에서 catch절로 throw된 error를 반드시 해결할 필요는 없다. 던져진 error를 이 코드를 호출한 곳으로 전파시킬 수 있기 때문이다. do-catch 문을 포함하고 있는 nonthrowing function은 반드시 에러를 처리해야 한다. throwing function의 경우, do-catch절에서 error를 처리하거나 이 함수를 호출한 코드에서 처리해야 한다. 만약 error가 어떤 코드에서도 처리되지 않으면 runtime error가 발생한다.

예를 들어 위 예제를 아래 코드와 같이 다시 작성할 수 있다.

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

만약 vend(itemNamed:) 메소드가 열거형 VendingMachineError의 error 중 하나를 nourish(with:) 함수에게 throw 한다면, 이 함수는 메시지 출력을 통해 던져진 error를 처리한다. nourish(with:) 함수 내에서 처리하지 않으면 이 함수를 호출한 위치로 error를 전파한다.

error를 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?구문을 사용하여 error를 optional 값으로 변환할 수 있다. try? 표현식 내에서 error가 던져졌다면 표현식의 값은 nil이 된다. 예를 들어 다음 코드의 x와 y는 같은 값을 가지고 같은 동작을 한다.

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

let x = try? someThrowingFunction()

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

만약 someThrowingFunction 함수가 error를 throw 하면, x와 y의 값은 nil이 된다. error가 throw 되지 않으면 함수가 반환하는 값으로 할당된다. someThrowingFunction 함수의 반환 타입이 무엇이든지 x와 y는 optional 타입임을 기억하자. 함수의 반환 타입이 Int이기 때문에 x와 y는 optioanl Int 타입이 된다.

모든 error를 같은 방법으로 처리하기를 원할 때 try? 구문을 사용한다. 이 구문을 사용하면 간결하게 코드를 작성할 수 있다. 예를 들어 아래 예제는 데이터를 가져오는 2 가지 접근법을 사용하는데 이 접근들이 모두 실패하면 함수는 nil을 반환하고 접근이 성공하면 가져온 데이터를 반환한다.

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

Disabling Error Propagation (에러 전파 중지)

함수나 메소드에서 error가 발생하지 않을 것이라고 확신하는 경우가 있다. 이때는 try!를 사용한다. 만약 error가 실제로 thrown 되면 runtime error가 발생한다.

예를 들어 다음 코드는 주어진 경로에서 이미지를 읽어오는 loadImage(atPath:) 함수를 사용한다. 이 함수는 주어진 경로에서 이미지를 읽어오지 못하면 error를 발생시킨다. 이 경우에는 앱이 배포될 때 내부에 이미지가 포함되어 배포되기 때문에 error가 발생하지 않을 것이라고 확신할 수 있다. 그래서 try!을 사용하는 것이 적합하다.

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

Specifying Cleanup Actions ( 정리 작업 명시)

defer 구문을 사용하여 error가 발생했거나, return이나 break 때문에 현재 코드 블록을 벗어나야 하는 경우 그 이유에 상관없이 반드시 실행되어야 하는 코드 블록을 실행할 수 있다. 에를 들어 defer 구문을 사용하여 파일 스트림을 종료하거나, 파일 작업에 할당된 메모리를 수동으로 해제할 수 있다.

defer 구문은 현재 실행 중인 코드 범위가 종료될 때까지 실행을 연기한다. 이 구문은 defer 키워드와 나중에 실행되어야 할 코드로 구성된다. 연기된 구문 내에서는 error throwing, return이나 break와 같이 구문을 벗어날 수 있는 어떠한 코드도 작성될 수 없다. 그리고 defer 구문은 소스코드의 역순으로 실행된다. 이를 확인하기 위해 아래 코드를 살펴보자.

defer {
    code a
    code b
    code c
}

위 코드에서는 a->b->c 순으로 실행되는 것이 아닌 역순인 c->b->a 순으로 코드가 실행된다.

다음으로 difer 구문을 이용하여 file을 닫는 작업을 구현한다.

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

위 예제에서 difer 구문은 파일을 닫고 자원을 해지하는 작업을 수행하는close(file)을 구문 내에 작성하여 이 코드가 반드시 실행되는 것을 보장한다.

NOTE
difer 구문을 error 처리 이외에도 사용할 수 있다.

0개의 댓글