-
Swift Language ) Error HandlingSwift/Swift Language 2021. 11. 23. 16:08
Error handling 은 프로그램 내 error conditions 에 대해 반응하고 recover 하는 과정이다. Swift 는 runtime 시 throwing, catching, propagating, manipulating recoverable erros 에 대해 first-class support 를 제공한다. 어떤 operations 는 실행을 완전히 마칠 수 없을 수 있고, 유용한 output 을 내지 못할 수도 있다. Optionals 는 값이 없음을 나타낼 때 사용된다. An operation 이 fail 될 때, 어떤 것이 failure 을 발생시켰는지 알면 그에 따라 반응할 수 있다.
예를 들어, disk 내에 있는 file 내 data 를 읽고 처리하는 과정에 대해 생각해보자. 해당 task 는 여러 이유로 fail 할 수 있다. 지정한 path 에 file 이 존재하지 않을 수 있고 file 에 대한 read permission 이 없거나, compatible format 으로 encoded 되지 않았을 수도 있다. 이러한 다른 상황들을 구분한다면 프로그램이 errors 에 대해 해결하고 어떤 에러가 났는지 user 에게 알려줄 수도 있다.
Representing and Throwing Errors
Swift 에서, errors 는 Error protocol 을 conform 하는 types 의 values 로 표현된다. Error handling 에 type 이 사용될 수 있다. Swift 의 Enumerations 는 error conditions 를 모델링하기 좋은 구조를 가지고있다. Associated values 를 이용하면 error 의 추가적인 정보에 대해 더 다루기 쉽다. 아래 예제는 game 내의 자판기에서 발생할 수 있는 error 에 대해 나타낸 것이다.
enum VendingMachineError: Error { case invalidSection case insufficientFunds(coinsNeeded: Int) case outOfStock }
Error 를 Throwing 하면 어떤 예상치 못한 것이 발생하거나 정상적인 exeucution 을 계속 진행할 수 없을 때를 알 수 있다. Throw statement 를 사용해서 error 를 throw 하면 된다. 아래 코드는 5 개의 coins 이 더 필요하다는 것을 알리는 error 를 throw 한다.
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Handling Errors
Error 가 thrown 됐을 때, 감싸고 있는 코드는 반드시 해당 error 에 대응해야한다 (문제를 해결하거나, 대체할 방법을 시도하거나, 사용자에게 failure 을 알리는 방식 등으로 ).
Swift 에는 errors 를 handle 하는 네가지 방법이 있다. Error 가 발생하는 코드를 호출하는 function 에 error 를 propagate 하거나, do-catch statement 를 이용해서 처리하거나, optional value 로 처리하거나, or assert that the error will not occur . 각 방법을 아래 section 에 기술하였다.
A function 이 error 를 throw 할 때, program 의 흐름을 바꿀 수 있으므로 error 를 throw 하는 곳의 위치를 빠르게 파악하는게 중요하다. 어떤 곳에서 에러가 발생하는지 파악하기 위해 try (or try? try!) keyword 를, error 를 throw 하는 function(or method, initializer) 이 오기 전에 사용한다.
Propagating Errors Using Throwing Functions
Function, method, or initializer 가 error 를 throw 할 수 있다는 것을 나타내기 위해 throws keyword 를 function 을 선언할 때 parameters 뒤에 써준다. throws 로 marked 된 함수는 'throwing function' 이라 불린다. 만약 함수가 return type 을 지정할 때는 return arrow (->) 이전에 throws keyword 를 쓴다.
func canThrowErrors() throws -> String func cannotThrowErrors() -> String
throwing function 은 내부에서 발생한 에러를 자신이 호출된 곳으로 propagate 한다.
throwing functions 만 error 를 propagate 할 수 있다. nonthrowing function 에서 thrown 된 error 는 반드시 해당 function 내에서 처리되어야 한다.
아래 예제에서, VendingMachine class 는 vend(itemNamed:) method 를 갖고, 이 method 는 적절한 VendingMachineError 를 throw 한다 (item 이 사용 불가능한 경우, 재고가 없는 경우, 현재 가지고 있는 비용보다 더 비쌀 경우에 따라) .
class VendingMachine { var inventory : [String: Item] = ["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.invalidSection } 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:) method 에서, Item 을 구매하는 데 요구되는 어떤 조건이라도 만족하지 못할 시 바로 method 를 끝내고 적절한 errors 를 throw 하기 위해서 guard statement 를 사용한다. throw statement 는 바로 program 의 control 순서를 바꾸기 때문에, item 은 모든 조건이 충족될 때에만 자판기에서 뽑혀 나오게된다.
vend(itemNamed:) method 에서 error 를 throw 하기때문에, 이 method 를 호출하는 code 는 반드시 error 를 처리하거나 (do-statement, or try?, try!) propagate 시켜주어야한다. 예를 들어 아래 buyFavoriteSnack(person:vendingMachine:) 은 throwing function 이고, 따라서 vend(itemNamed:) 에서 throw 하는 error 는 buyFavoriteSnack(person:vendingMachine:) 을 호출하는 곳까지 propagate 된다.
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:) 에서 person 의 favorite snack 을 찾아보고, vend(itemNamed:) method 를 이용해서 구입을 try 한다. vend(itemNamed:) method 는 error 를 throw 할 수 있기 때문에, 앞부분에 try keyword 가 사용되었다.
Throwing initializers 도 throwing functions 와 같은 방식으로 errors 를 propagate 할 수 있다. 예를 들어, 아래 PurchasedSnack 의 initializer 는 intialization process 중에 throwing function 을 호출하고, initializer 를 호출하는 곳에 error 를 propagate 하는 방식으로 처리한다.
struct PurchasedSnack { let name: String init(name: String, vendingMachine: VendingMachine) throws { try vendingMachine.vend(itemNamed: name) self.name = name } }
Handling Errors Using Do-Catch
Errors 를 handle 하기 위해 do-catch statement 를 사용한다. do clause 내에서 error 가 thrown 되면, 이에 대응하는 catch clause 와 매치된다. 아래는 일반적인 do-catch statement 형태이다.
You use a do-catch statement to handle errors by running a block of code. If an error is thrown by the code in the do clause, it’s matched against the catch clauses to determine which one of them can handle the error.
do { try expression statements } catch pattern 1 { statements } catch pattern 2 where condition { statements } catch pattern 3, pattern 4 where condition { statements } catch { statements }
Clause 가 처리할 수 있는 errors 를 나타내기 위해 catch 뒤에 pattern 을 작성한다. 만약 catch clause 가 pattern 을 갖지 않으면, 이 clause 는 any error 에 대응된다. 아래 예시에서는 3가지 VendingMachineError enumeration 에 대응하는 코드이다.
You write a pattern after catch to indicate what errors that clause can handle. If a catch clause doesn’t have a pattern, the clause matches any error and binds the error to a local constant named error. For more information about pattern matching, see Patterns.
var vendingMachine = VendingMachine() vendingMachine.coinsDeposited = 8 do { try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine) print("Success ! Yum.") } catch VendingMachineError.invalidSection { 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:) function 은 error 를 throw 할 수 있기때문에 try expression 에서 호출된다. 만약 error 가 thrown 될 시, 바로 catch clauses 가 실행되고, propagation 이 계속 될지에 대해 결정한다. 어떠한 pattern 과도 matched 되지 않으면, error 는 마지막 catch clause 에서 잡히게되고 local error constant 와 bind 되게된다. 어떠한 error 도 thrown 되지 않으면 do statement 의 나머지 statements 가 실행된다. catch clauses 에서 do clause 가 throw 할 수도 있는 모든 error 를 처리할 필요는 없다. 만약 어떠한 catch clauses 도 error 을 handle 하지 않으면, error 는 주변 scope 로 propage 된다. 이때 propagated 된 error 는 반드시 주변 scope 에서 처리되어야한다. nonthrowing function 에서는 감싸고 있는 do-catch statement 가 반드시 에러를 처리해야한다. 만약 error 가 top-level scope 까지 handle 되지 않은 상태로 propagate 될 시, runtime error 가 발생한다.
예를 들어, 아래 코드는 VendingMachineError 가 아닌 경우 function 을 호출하는 곳에서 처리하는 과정이다.
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)") }
nourish(with:) function 에서, 만약 vend(itemNamed:) 가 VendingMachineError enumeration 에 있는 error 를 throw 하게된다면 message 를 출력하는 방식으로 error 를 처리한다. 하지만 그렇지 않은 경우 nourish(with:) 은 함수를 호출하는 곳에 error 를 propagete 한다. 그 error 는 general catch clause 에서 caught 된다.
여러개의 관련된 errors 를 처리하는 또다른 방법은 catch 뒤에 그 list 를 , (comma) 로 구분하여 적는 것이다.
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:) function 에서는 catch 할 vending machine errors 와 대응하는 text 를 적었다. 만약 어떤 listed errors 가 thrown 될 경우, message 를 출력하는 방식으로 error 를 처리한다. 다른 errors 는 surrounding scope 로 propagate 된다.
Converting Errors to Optional Values
try? 를 사용하여 error 를 optional value 로 변환하는 방식으로 처리할 수 있다. error 가 try? expression 을 처리하는 과정에서 thrown 될 경우, try expression 의 값은 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 이 된다. 그렇지 않은 경우, x 와 y 는 function 이 반환하는 값을 갖는다. 따라서, x 와 y 는 someThrowingFunction() 이 반환하는 type 의 optional 이다. 위 경우에서는 integer 를 return 하므로 optional integer 타입을 갖는다.
try? 를 사용하면 전과 같은 방식으로 더 간결하게 error 를 처리할 수 있다. 아래는 data 를 가져오기 위해 여러 방식을 사용하는 코드이다. 이 경우 모든 방법이 실패할 경우 nil 을 반환한다.
Using try? lets you write concise error handling code when you want to handle all errors in the same way. For example, the following code uses several approaches to fetch data, or returns nil if all of the approaches fail.
func fetchData() -> Data? { if let data = try? fetchDataFromDisk() { return data } if let data = try? fetchDataFromServer() { return data } return nil }
Disabling Error Propagation
가끔씩은 throwing function 이 runtime 시 error 를 throw 하지 않는다는 것을 알 때도 있다. 이런 경우, try! 를 expression 앞에 사용해서 error propagation 을 불활성화시키고 wrap the call in a runtime assertion that no error will be thrown. 만약 에러가 thrown 된다면 runtime error 을 맞이하게 된다.
예를들어, 아래 코드는 loadImage(atPath:) function 을 사용한다. 이 함수는 주어진 path 에 있는 image resource 를 부르고, 만약 불가능한 경우 error 를 throw 한다. 이 경우, image 가 application 과 함께 존재하기 때문에 runtime 시에 어떠한 에러도 thrown 되지 않는다. 따라서, 이 경우 error propagation 을 불활성화 시키는 것이 적절하다.
Sometimes you know a throwing function or method won’t, in fact, throw an error at runtime. On those occasions, you can write try! before the expression to disable error propagation and wrap the call in a runtime assertion that no error will be thrown. If an error actually is thrown, you’ll get a runtime error.
For example, the following code uses a loadImage(atPath:) function, which loads the image resource at a given path or throws an error if the image can’t be loaded. In this case, because the image is shipped with the application, no error will be thrown at runtime, so it’s appropriate to disable error propagation.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
Specifying Cleanup Actions
현재 code 블록이 끝나기 바로 전에 특정 statements 를 실행시키기 위해 'defer' statement 를 이용할 수 있다. 이 statement 는 어떠한 상태로 코드가 종료되든간에 (에러로 종료되거나 return or break ) 실행되어야하는 꼭 필요한 뒤처리를 할 수 있게 만든다. 예를 들어, file desciptors 가 닫히고 할당되었던 메모리가 해제가 보장되도록 할 때 사용할 수 있다. defer statement 는 현재 범위가 종료될 때까지 실행을 미룬다 (defer) . defer stateement 는 'defer' keyword 와 나중에 시행될 statements 로 구성된다. 여기 쓰여질 statements 는 break, return 등의 control 순서를 바꾸거나 error 를 throw 하는 코드를 포함하지 않을 수 있다. Deferred actions 는 source code 에 쓰여진 순서와 반대로 시행된다. 즉, 첫번째 defer statement 는 가장 나중에, 두번째 defer statement 는 두번째로 나중에, 마지막 defer stament 가 가장 먼저 시행된다.
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(_:) function 과 대응하는 close(_:) function 을 반드시 시행하기 위해 defer statement 가 사용되었다.
출처: https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html
'Swift > Swift Language' 카테고리의 다른 글
Swift Language ) Properties (0) 2022.01.17 Assertions & Preconditions (0) 2021.11.24 Swift Language ) Advanced Operators (0) 2021.11.11 Structures And Classes (Swift) (0) 2021.11.01 Swift Language ) Closure (0) 2021.10.06