본문 바로가기

Swift/꼬꼬무

Error Handling

에러 처리 (Error handling) 는 프로그램의 에러 조건에서 응답하고 복구하는 프로세스 입니다.

Swift는 런타임에 복구 가능한 에러를 처리 위한 최고 수준의 지원한다고 합니다.

경우에 따라서 실행을 완료하지 못하는 작업이 발생할 수도 있습니다.

예를 들면 옵셔널은 값이 없음을 나타내는데 사용되지만 작업이 실패할 경우 어떤 원인으로 값이 생성되지 못했는지 확인 후 처리하도록 할 수 있습니다.

 

Note
Swift에서 에러 처리는 Cocoa와 Objective-C에 NSError 를 사용하는 에러 처리 패턴과 상호 운용됩니다.

 

Representing and Throwing Errors (에러 표현과 던지기)

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

이 빈 프로토콜은 에러를 처리하는 것에 대해 사용될 수 있음을 나타냅니다.

열거형을 이용하면 에러 케이스 그룹을 만들거나 관련값으로 각 케이스별 추가 정보도 전달하는데 용이합니다.

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

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

예를 들어 아래의 코드는 자동 판매기에 5개의 코인이 더 필요하다고 에러를 발생 시킵니다:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

 

에러 처리 (Handling Errors)

만약 에러가 발생한다면!

에러 처리를 담당하는 코드가 있어야 합니다.

문제를 수정한다던지, 다른 방법을 시도한다던지, 사용자에게 에러를 알려주는 등의 방법으로 에러 처리를 해야 합니다.

 

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

  1. 함수를 호출한 코드로 에러를 반환
  2. do - catch 구문을 사용
  3. 옵셔널 값으로 에러 처리
  4. 에러가 발생하지 않을 것이라고 주장! ??

 

함수에서 에러가 발생하면 프로그램의 동작에 문제가 생기니 빨리 에러 발생을 알아야 합니다.

 

코드에서 이러한 위치를 식별하려면 에러가 발생할 수 있는 함수, 메서드, 또는 초기화 구문을 호출하는 코드 이전에 try 또는 try? 또는 try! 키워드를 작성합니다.

 

이제부터! 자세히 한번 알아보겠습니다.

 

Note
Swift에서 에러 처리는 try, catch 그리고 throw 키워드를 사용하는 다른 언어에서 에러 처리와 유사합니다. Objective-C를 포함하여 많은 언어에서의 예외 처리와 달리 Swift에서 에러 처리는 계산 비용이 많이 드는 프로세스 인 호출 스택 해제가 포함되지 않습니다. 따라서 throw 구문의 성능 특성은 return 구문의 성능 특성과 비슷합니다.

 

 

던지기 함수를 이용한 에러 전파 (Propagating Errors Using Throwing Functions)

에러가 발생할 수 있는 함수, 메서드, 또는 초기화 구문을 나타내기 위해 함수의 선언 중 파라미터 뒤에 throws 키워드로 표시된 throwing function을 추가해서 에러를 전달합니다.

 

함수에 반환 타입이 지정되어 있으면 throws 키워드는 반환 화살표 (->) 전에 작성합니다.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

 

던지기 함수는 내부에서 발생한 에러를 호출된 범위로 전파합니다.

Note
던지기 함수는 에러를 전파만 할 수 있습니다. 던지기 선언이 되지 않은 함수 내에서 발생된 모든 에러는 함수 내에서 처리되어야 합니다.

 

아래 예제에서 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 구문을 통해 요구조건을 충족하는지 확인하고 그렇지 않은 경우 즉시 적절한 에러를 발생시킵니다.

 

vend(itemNamed:) 메서드는 발생하는 에러를 전파하기 때문에 이 메서드를 호출하는 코드는 do-catch 구문, try?또는 try! 를 사용하여 에러를 처리하거나 계속 전파해야 합니다.

 

 

예를 들어 아래 예제에서 buyFavoriteSnack(person:vendingMachine:) 은 던지기 함수이며 vend(itemNamed:) 메서드에서 발생한 에러는 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)
}

 

vend(itemNamed:) 메서드는 에러를 발생할 수 있으므로 try 키워드를 앞에 두어 호출됩니다.

던지기 초기화 구문은 던지기 함수와 같은 방법으로 에러를 전파할 수 있습니다.

 

 

예를 들어 아래의 리스트에서 PurchasedSnack 구조체의 초기화 구문은 초기화 프로세스 부분으로 던지기 함수를 호출하고 발생하는 모든 에러를 호출자에게 전파하여 처리합니다.

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

 

Do-Catch 사용하여 에러 처리 (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 절이 패턴을 가지고 있지 않다면 이 절은 모든 에러와 일치하고 error 라는 이름을 가진 지역 상수로 에러를 바인드 합니다.

 

 

아래 예제 코드는 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:) 함수는 에러를 발생할 수 있으므로 try 표현식으로 호출됩니다.

 

에러 발생! -> catch 절로 전달 되어 일치하는 패턴으로 처리

일치하는 패턴이 없음! -> 마지막 catch절에 의해서 에러 메세지 출력

에러 발생하지 않음! -> do 구문의 나머지 코드 실행

 

catch 절은 do 절에서 발생할 수 있는 모든 에러를 처리할 필요는 없습니다.

던지지 않는 함수에서는 do-catch 구문에서 에러를 처리해야 합니다.

던지는 함수에서는 do-catch 구문이나 호출자가 에러를 처리해야 합니다.

 

에러가 처리되지 않고 범위의 최상위로 전파되면 런타임 에러를 발생합니다.

 

예를 들어 위의 예제는 VendingMachineError 가 아닌 모든 에러가 호출 함수에서 포착되도록 작성할 수 있습니다

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가지 VendingMachineError 중 하나가 발생하면 catch 문에 걸려서 에러 메시지를 출력합니다.

 

catch 문에 걸리지 않은 에러는 함수를 호출한 곳으로 전파됩니다.

 

에러를 옵셔널 값을 변환 (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
}

someThrowingFunction() 이 에러를 발생하면 x 와 y 의 값은 nil 이고 그렇지 않으면 실제 반환된 값이 됩니다.

x 와 y 는 someThrowingFunction() 이 반환하는 타입의 옵셔널이 됩니다.

try? 를 사용하면 하나의 방식으로 에러를 좀 더 쉽게 처리할 수 있습니다.

케이스 별로 catch를 만들지 않고 에러가 발생하면 nil을 반환하게 한다.

 

 

아래 예제처럼 데이터를 가져오는데 성공하면 데이터를 반환하고 아니면 nil을 반환하게 하는 등으로 사용할 수 있습니다.

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

 

에러 전파 비활성화 (Disabling Error Propagation)

throwing function이나 메서드가 런타임 에러를 발생시키지 않는다고 확신하는 경우에는, try! 구문을 사용하여 에러 전파를 비활성화 할 수 있습니다.

try! 구문을 사용했는데 에러가 발생하면 런타임 에러가 발생합니다.

만약에 앱에서 특정 경로의 이미지를 불러오는 함수를 사용하는 경우 특정 경로의 이미지 파일은 앱에 포함되어 있기 때문에 불러오기에 실패할 가능성이 없습니다.

 

이런 경우 에러 전파를 비활성화 해서 사용할 수 있습니다.

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

 

정리 작업 지정 (Specifying Cleanup Actions)

코드의 현재 블럭이 종료되기 직전에 어떤 작업업을 수행하고 싶은 경우 defer 구문으로 구현할 수 있습니다.

defer 구문은 에러의 발생으로 인한 종료나, return 또는 break 같은 구문에 의해 종료와 상관없이 코드를 실행시킬 수 있습니다.

 

예를 들어 defer 구문을 사용하여 파일 설명자가 닫히고 수동으로 할당된 메모리가 해제되도록 할 수 있습니다.

 

defer 구문은 defer 키워드와 나중에 실행될 구문으로 구성되어 있습니다.

defer 구문 안의 코드는 현재 실행중인 범위가 종료될때까지 실행을 연기합니다.

나중에 실행될 구문 안에는 break 또는 return 구문 처럼 현재 실행중인 구문 밖으로 이동하거나 에러를 발생시키는 코드는 작성할 수 없습니다.

 

defer 구문은 작성된 순서와 반대로 실행되는데, 첫번째 defer 구문은 마지막에 실행되고 그 다음 defer 구문은 마지막에서 두번째로 실행됩니다.

defer1 → defer2 → defer3 의 순서로 작성된 경우 실제로는 defer3 → defer2 → defer1 의 순서로 동작

 

소스 코드의 마지막 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.
    }
}

 

위의 예제는 open(_:) 함수가 종료되기 직전에 close(_:) 함수를 호출하기 위해 defer 구문을 사용합니다.

Note
에러 처리 코드가 없는 경우에도 defer를 사용할 수 있습니다.