Swift/Swift 가이드

[Swift] 공식 문서 - 에러 핸들링(Error Handling)

유정주 2022. 7. 28. 18:34
반응형

새로 배운 점

  • Swift는 런타임 에러를 처리하기 위해 throwing, catching, propagting, manipulating을 지원하는 일급 클래스(first-class)를 제공합니다.
  • Swift에서 에러는 Error 프로토콜을 준수하는 타입의 값으로 표현됩니다.
  • Swift에서 에러 처리를 많은 계산이 필요할 수 있는 Call Stack Unwinding과 관련이 없다는 것입니다.
  • 에러를 반환하는 throw문은 일반적인 return과 비슷한 성능을 보여줍니다.
  • throws 키워드가 붙은 함수는 throwing function 이라고 부릅니다.
  • 만약 catch절이 패턴을 가지고 있지 않다면, 이 catch절은 모든 에러와 일치하고 해당 에러를 error라는 로컬 상수로 바인딩합니다.
  • defer문을 사용하면 현재 코드 블록 실행을 종료하기 전에 실행되는 일련의 문장들을 실행시킬 수 있습니다.
  • defer에 의해 연기된 작업은 소스 코드에 작성된 순서의 역순으로 실행됩니다.

 

Error Handling

에러 핸들링(Error Handling)은 프로그램에서 에러 조건들에 대해 대응하고 회복하는 프로세스입니다. 

Swift는 런타임 에러를 처리하기 위해 throwing, catching, propagting, manipulating을 지원하는

일급 클래스(first-class)를 제공합니다.

 

어떠한 명령은 실패하거나 무의미한 결과를 생성합니다.

옵셔널은 값이 존재하지 않을 수 있는데, 명령이 실패할 때는 어떠한 원인으로

실패했는지 이해하는 것이 유용하며, 이를 통해 적절한 처리를 할 수 있습니다.

 

예를 들어, 디스크에서 파일을 읽어 데이터를 처리하는 경우

파일이 존재하지 않거나 읽기 권한이 없거나 파일의 데이터가 인코딩되지 않을 때

작업이 실패할 수 있습니다.

이러한 에러들을 식별하여 사용자에게 제공해주면 프로그램 실행 중에

각 에러를 적절하게 대응하도록 도와줄 수 있습니다.

Swift의 에러 핸들링은 Cocoa와 Objective-C의 NSError 클래스와 상호호환되는 에러 핸들링 패턴을 사용합니다.

 

Representing and Throwing Errors

Swift에서 에러는 Error 프로토콜을 준수하는 타입의 값으로 표현됩니다.

이 빈 프로토콜은 해당 타입이 에러 핸들링에 사용된다는 것을 가리킵니다.

 

Swift의 열거형은 특히 에러 조건과 관련된 그룹을 모델링하는데 적합하며,

그 값들로 에러에 특성에 대한 부가적인 정보를 커뮤니케이션할 수 있도록 합니다.

예를 들어, 아래 코드처럼 자판기 동작의 에러 조건들을 나타낼 수 있습니다.

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

에러 throwing은 예기치 못한 무언가가 발생했고

일반적인 실행 흐름을 계속할 수 없다는 것을 가리킵니다.

throw문을 사용하여 에러를 발생시킬 수 있습니다.

다음 코드는 자판기에서 5개의 코인이 더 필요하다는 것을 에러로 발생시킵니다.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

 

Handling Errors

에러가 던져졌을 때 주변의 코드는 에러를 해결하거나 대안 방법을 시도하거나 유저에게 실패를 알리는 등

반드시 에러를 처리하기 위한 행동을 해야합니다.

Swift에서는 에러를 처리하기 위한 4가지 방법이 있습니다.

어떤 함수에서 발생한 에러를 함수를 호출한 코드로 전파(propagate)하거나

do-catch문을 사용하여 에러를 처리하거나, 에러를 옵셔널 값으로 처리하거나, 에러가 발생하지 않았다고 처리할 수 있습니다.

각 방법들은 아래에서 하나씩 살펴보겠습니다.

 

함수가 에러를 발생시킬 때, 프로그램의 흐름을 발생시키며 

에러를 발생시킬 수 있는 코드의 위치를 빠르게 식별하는 것이 중요합니다.

코드에서 위치를 식별하기 위해서는 try나 try?, try! 키워드를 함수나 메서드, 이니셜라이저를 호출하는 코드 전에 사용합니다.

이 키워드도 아래에서 살펴보겠습니다.

Swift에서의 에러 처리는 다른 언어의 try-catch, throw를 사용하는 exception 처리와 비슷합니다.
Objective-C를 포함하여 다른 언어의 exception 처리와 다른 점은
Swift에서 에러 처리를 많은 계산이 필요할 수 있는 Call Stack Unwinding과 관련이 없다는 것입니다.
그렇기 때문에 에러를 반환하는 throw문은 일반적인 return과 비슷한 성능을 보여줍니다.

 

Propagating Erros Using Throwing Functions

함수, 메서드, 이니셜라이저가 에러를 발생시킬 수 있다는 것을 알리기 위해

함수 선언부의 파라미터 뒤에 throws 키워드를 붙여주면 됩니다.

throws 키워드가 붙은 함수는 throwing function 이라고 부릅니다.

만약 함수의 리턴 타입이 있다면 throw 키워드는 리턴 화살표 이전에 작성합니다.

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

throwing 함수는 함수 내부에서 에러를 던져서 함수가 호출된 곳으로 전달합니다.

오직 throwing 함수만이 에러를 전달할 수 있습니다.
non-throwing 함수 내에서 발생된 에러는 반드시 그 함수 내부에서 처리 돼야 합니다.

아래 예제 코드에서 VendingMachine 클래스는

요청된 아이템을 사용할 수 없거나, 재고가 없거나, 현재 가진 것을 초과하는 비용이 발생했을 때,

적절한 VendingMachineErorr를 던지는 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문을 사용하여 스낵을 구입하는 조건에 만족하지 못한 경우

적절한 에러를 던져서 메서드를 일찍 종료하도록 합니다.

throw 문은 즉시 프로그램 컨트롤을 전달하기 때문에

모든 요구사항들을 만족할 때만 그 아이템을 구매할 수 있습니다.

 

vend(itemNamed:)는 이 메서드가 던진 에러들을 전달하기 때문에 이 메서드를 호출한

어떤 코드는 반드시 에러를 처리하거나 이를 계속해서 전달해야 합니다.

예를 들어, 아래 코드의 buyFavoriteSnack(person:vendingMachine:) 또한 에러를 발생시키는데

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

위 코드에서 buyFavoriteSnack 함수는 주어진 사람의 가장 좋앙하는 스택이 무엇인지 확인하고

이를 vend(itemNamed:)에 전달해 호출하여 구매를 시도합니다.

vend 메서드는 에러를 발생시킬 수 있기 때문에 try 키워드와 함께 호출하고 있습니다.

 

throwing 이니셜라이저도 throwing 함수와 동일한 방법으로 에러를 전달할 수 있습니다.

예를 들어, 아래 코드에서 PurchasedSnack 구조체는 초기화 단계에서 throwing 함수를 호출하고,

발생된 에러는 이 이니셜라이저를 호출한 곳으로 전달됩니다.

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
}

catch문 뒤에서 처리할 수 있는 에러를 적는 패턴으로 작성합니다.

만약 catch절이 패턴을 가지고 있지 않다면, 이 catch절은 모든 에러와 일치하고

해당 에러를 error라는 로컬 상수로 바인딩합니다.

 

다음 코드는 VendingMachineError 열거형의 모든 케이스에 대해 매치됩니다.

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 함수는 에러를 발생시킬 수 있기 때문에 try 표현식 내에서 호출됩니다.

만약 에러가 발생되면 흐름은 즉시 catch절로 이동하여 전파를 허용할지 결정합니다.

만약 어떠한 패턴도 매칭되지 않으면 에러는 마지막 catch 절에서 잡히고 로컬 error 상수로 바인딩됩니다.

만약 어떠한 에러도 발생되지 않으면 do문에 남아있는 문장들이 실행됩니다.

 

catch절은 do절에서 발생될 수 있는 모든 에러를 처리하지 않아도 됩니다.

만약 에러를 처리하는 catch 절이 없다면 에러는 주변 지역으로 전파됩니다.

그러나 전파된 에러는 반드시 주변 지역 무언가에 의해 처리 돼야 합니다.

 

non-throwing 함수에서는 do-catch문에서 반드시 에러를 처리해야 합니다.

throwing 함수에서는 do-catch문이나 이 함수를 호출한 측에서 에러를 처리해야 합니다.

만약 에러를 처리하지 않고 top-level까지 에러가 전파된다면 런타임 에러가 발생합니다.

 

아래 코드는 VendingMachineError를 제외한 모든 에러가 호출된 함수에 의해 catch 됩니다.

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(with:)에서 만약 vend(itemNamed:)가 VendingMachineError 열거형 중의 한 케이스를 발생시킨다면

nourish(with:)는 메시지를 출력하는 것으로 에러를 처리합니다.

다른 에러는 nourish(with)를 호출한 쪽으로 에러를 전파합니다.

그러면 이 에러는 일반 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.")
    }
}

eat(item:)은 catch하는 에러를 나열하고 있습니다.

만약 리스트된 3개의 에러 중 하나가 발생하면 메시지를 출력하면서 에러를 처리합니다.

다른 에러는 주변 지역으로 전파됩니다.

 

Converting Errors to Optional Values

try?를 사용하여 이를 옵셔널 값으로 바꿔 에러를 처리할 수 있습니다.

만약 try? 표현식을 평가하는 동안 에러가 발생되면 그 표현식의 값은 nil이 됩니다.

예를 들어, 아래 예제에서 x와 y는 값과 동작이 같습니다.

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

만약 someThrowing()이 에러를 발생시키면 x와 y는 nil이 됩니다.

만약 에러를 발생시키지 않는다면 x와 y의 값은 함수가 리턴하는 값이 됩니다.

someThrowingFuction()의 리턴 타입이 무엇이든 x와 y는 옵셔널입니다.

위 코드는 Int를 반환하므로 Int?형이 됩니다.

 

모든 에러를 동일한 방식으로 처리하고 싶을 때 try?를 사용하면

코드를 간결하게 작성할 수 있습니다.

예를 들어, 다음 코드는 데이터를 가져오는데 모든 방법이 실패하면 nil을 리턴합니다.

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

 

Disabling Error Propagation

throwing 함수나 메서드가 런타임 에러를 발생시키지 않는다는 것을 알고 있을 때가 있습니다.

이런 상황에서 표현식 전에 try!를 사용하면 에러 전파를 비활성화할 수 있습니다.

하지만 에러가 발생하면 런타임 에러가 발생합니다.

 

예를 들어, 다음 코드는 loadImage(atPath:)를 사용하는데

이 함수는 주어진 경로에서 리소스를 읽지 못하거나 로드되지 못하면 에러를 발생시킵니다.

이 경우 앱에 이미지가 존재하기 때문에 런타임에 에러가 발생하지 않을 것이고

따라서 에러 전파를 비활성화하는게 적절합니다.

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

 

Specifying Cleanup Actions

defer문을 사용하면 현재 코드 블록 실행을 종료하기 전에 실행되는 일련의 문장들을 실행시킬 수 있습니다.

이 문장은 코드 실행이 현재 코드 블록을 떠나는 이유와 관계 없이 수행되어야 하는

cleanup을 수행할 수 있습니다.

예를 들어 defer문을 사용해 파일 descriptor를 닫고, 수동으로 할당된 메모리를 해제되도록 할 수 있습니다.

 

defer문은 현재 범위가 종료될 때까지 실행을 연기합니다.

이 문장은 defer 키워드와 나중에 실행될 문장들로 구성됩니다.

이렇게 연기된 코드들은 break나 return문과 같이

문장의 외부로 컨트롤을 전달하는 코드나 에러를 던지는 코드가 포함될 수 없습니다.

이렇게 연기된 작업은 소스 코드에 작성된 순서의 역순으로 실행됩니다.

즉, 첫 번째 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문은 에러 처리 코드 이외에도 사용될 수 있습니다.

 

참고

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

 

 

반응형