Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Error Handling 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
에러 핸들링Error handling은 에러 상황에 반응하고 에러를 복구하는 Process를 말한다. Swift는 복구할 수 있는 에러Recoverable error를 런타임 동안 throwing, catching, propagating, manipulating 할 수 있는 방법을 제공한다.
항상 완벽한 실행이나 유용한 아웃풋을 낸다고 보장할 수 없는 오퍼레이션들이 있기 마련이다. 이런 오퍼레이션이 실패했을 때, 그 실패의 원인을 알아야 코드에서 적절한 대응을 할 수 있다. 예를 들어 디스크 파일의 데이터를 읽고 처리하는 오퍼레이션을 생각해보자. 이 경우 오퍼레이션이 실패하는 원인은 다양하다. (파일이 특정 경로에 없을 수도 있고, 읽기 권한이 없을 수도 있고 등등) 각각의 실패 원인을 구분할 수 있다면 각각의 에러에 적절하게 대처할 수 있을 것이다.
<note> Cocoa와 Objective-C에서의 NSError를 이용한 에러 핸들링 패턴과 Swift의 에러 핸들링은 상호운용interoperate된다.
Representing and Throwing Errors
Swift에서 에러는 Error 프로토콜을 따르는 타입의 값으로 표현되며 이것을 에러 핸들링에 이용한다. (참고로 Error 프로토콜은 empty 프로토콜이다.)
Swift의 ENUM은 서로 관련있는 에러 조건error conditions들의 그룹을 모델링하기에 적합하며, associated value를 이용하면 특정 에러에 대한 추가 정보를 제공하는 것도 가능하다.
다음 예제를 보자. 자판기에서 발생할 수 있는 에러 조건들을 표현하고 있다.
// 아래와 같이 발생할 수 있는 에러를 정의할 수 있다
enum VendingMachineError: Error {
case InvalidSelection
case InsufficientFunds(coinsNeeded: Int)
case OutOfStock
}
// throw 키워드를 사용하여 에러를 던질 수 있다
// associated value를 사용하면 추가정보를 제공할 수 있다
throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)
Handling Errors
에러가 던져졌을 때 그 에러에 반응(핸들링)하는 코드는 반드시 필요하다. 예를 들어 유저에게 에러 상황을 알려주거나 다른 시도를 해보거나 하는 등의 코드가 있어야 한다.
Swift에서 에러 핸들링 방법은 4가지가 있다. 일단 간단히 살펴보면 다음과 같다.
propagate error. 함수 내에서 발생한 에러를, 그 함수를 호출한 코드로 전파
do-catch문을 사용해 에러 핸들링
에러를 optional value로 변환하여 핸들링
에러가 발생하지 않을 것이라는 assert를 걸기
함수가 에러를 던지게 되면 프로그램의 플로우가 바뀌게 된다. 따라서 에러가 던져질 수 있는 코드 구간을 식별할 수 있게 하는 것은 매우 중요하다. 에러를 던질 수 있는 함수, 메서드, 이니셜라이저의 앞에 try 키워드(혹은 try? 나 try!)를 쓰도록 하자. 이 키워드에 대해서는 밑에서 자세히 설명한다.
<note> try, catch, throw 키워드를 사용하는 다른 언어들의 exception 핸들링과 Swift의 에러 핸들링은 비슷하다. 그러나 Unlike exception handling in many languages—including Objective-C—error handling in Swift does not involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a throw statement are comparable to those of a return statement. (매끄럽게 해석하기가 힘들어서... 원문을 읽어주세요)
<1.Propagating Errors Using Throwing Functions>
throwing function이란? throws 키워드가 붙은 함수를 말한다. 에러를 던질 수 있는 함수, 메서드, 이니셜라이저에 throws 키워드를 붙임. throws 키워드의 위치는 파라미터 정의 뒤에 -> 가 나오기 전 위치이다.
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
throwing 함수는, 함수 내부에서 발생해서 던져진 에러를, 자신을 호출한 scope로 전파한다. (throwing 함수가 아닐 경우에는 외부로 에러를 전파할 수 없기 때문에, 함수 내부에서만 에러를 핸들링 해야한다)
자판기 예제를 하나 보자. VendingMachine 클래스는 vend(itemNamed:) 메서드를 가지고 있고, 이 메서드는 에러를 던질 수 있다.
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"캔디바" : Item(price: 12, count: 7),
"감자칩" : Item(price: 10, count: 4),
"프레즐" : Item(price: 7, count: 11)
]
var coinsDeposited = 0
// throws 키워드가 있으므로, 이것을 호출한 곳으로 에러 전파 가능
// 호출한 곳에서 에러 핸들링 가능
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("\(snack)을 제공해줌")
}
}
let favoriteSnacks = [
"Alice": "감자칩",
"Bob": "젤리",
"Eve": "프레즐"
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "캔디바"
try vendingMachine.vend(itemNamed: snackName)
// 에러가 발생할 수 있어서 try 키워드를 붙여 호출하고 있다
// <참고>
// 이 함수 자체도 throwing 함수기 때문에, 위 vend 메서드에서 에러가 발생한다면
// 이 함수를 호출한 곳으로 에러가 전파될 것이다
}
struct PurchasedSnack {
let name: String
// 이니셜라이저에 throws 키워드를 붙인 케이스
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name) // 에러가 던져질 수 있는 메서드
self.name = name
}
}
<2.Handling Errors Using Do-Catch>
do-catch 문을 사용해서 에러를 핸들링할 수 있다. do문안에서 에러가 던져지면 그 에러를 핸들링할 수 있는 catch문에 매치가 된다.
위에서 보이는 것처럼 catch문마다 어떤 에러를 핸들링할지 결정할 수 있다. 만약 catch문에 아무 패턴도 명시하지 않는다면 어떤 에러라도 매칭되며 그 에러는 error라는 이름의 로컬 상수에 바인딩된다.
catch문은 모든 에러를 핸들링할 필요는 없다. 만약 어떤 catch문도 핸들링하지 않는 에러가 던져지면, 에러는 surrounding scope(그 에러를 둘러싸고 있는 scope)로 전파된다. 주의할 점은, do-catch문이건 throwing 함수내부에서건 에러의 핸들링 자체는 반드시 되어야 한다는 것이다.
다음의 자판기 예제를 보자. VendingMachineError ENUM의 세 가지 에러 케이스에 대해 핸들링을 하고 있다. 그러나 그 외의 모든 에러들도, 그 에러의 surrounding scope에서 핸들링은 되어야 한다.
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack("Alice", vendingMachine: vendingMachine) // 에러 발생 시 여기서 멈추고 catch문으로
} catch VendingMachineError.InvalidSelection {
print("그런 제품은 없습니다.")
} catch VendingMachineError.OutOfStock {
print("품절되었습니다.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
print("돈이 부족합니다. 코인을 \(coinsNeeded)개 더 넣어주세요.")
}
참고로 어싱크로 돌 때 에러 핸들링을 하려면 이 방식으로는 되지 않는다. (어싱크로 도는 동안 이미 do-catch 코드를 지나가 버린다.) 이럴 때는 기존 Objective-C에서 했던 방식을 활용하는 것이 좋다. (ex: completionHandler(result, NSError) 같은 것)
<3.Converting Errors to Optional Values>
에러를 optional value로 변환converting하여 핸들링하려면 try? 키워드를 사용한다. try? expression을 수행하던 중 에러가 던져진다면 expression의 값은 nil이 된다.
예제를 보자.
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
// 위를 풀어쓰면 이러하다
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
// x와 y는 둘 다 optional Int이고 똑같은 동작을 한다
// <강조> x도 optional Int
// 보통 이런 식으로 많이 쓰인다
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() {
return data
}
if let data = try? fetchDataFromServer() {
return data
}
return nil
}
<4.Disabling Error Propagation>
throwing 함수가 런타임 중에 에러를 던지지 않을 거라는 확신이 있다면, 그리고 에러를 던지면 안 되는 경우라면, try! 키워드를 써서 에러 전파를 막고 assertion을 걸도록 하자. 이 경우 에러가 발생하면 런타임 에러가 발생한다.
let photo = try! loadImage("./Resources/John Appleseed.jpg")
Specifying Cleanup Actions
defer문 : 현재의 코드 블럭을 벗어나기 딱 직전에 실행시키고 싶은 코드가 있을 때 사용. 보통 cleanup을 위해 사용됨. (ex: file close)
에러가 던져져서 코드 블럭을 벗어나든, return이나 break에 의해 코드 블럭을 벗어나든, 상관없이 defer문은 실행됨
defer문은 current scope가 끝나기 전까지exited 실행을 미룬다 (*defer의 뜻 : 연기하다, 미루다)
defer문에는 컨트롤을 밖으로 보내는transfer 코드를 포함시킬 수 없다 (break, return, 에러던지기 등)
defer문의 실행순서는 역순임(맨 먼저 정의된 defer문이 맨 나중에 실행됨)
어떻게 쓰이는지 예제를 하나 보자.
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
// open된 파일은 항상 close 되는 것을 보장할 수 있다
}
while let line = try file.readline() {
// work with the file
}
// end of the scope.
// close(file)가 불린다
}
}
<note> 물론 에러 핸들링 코드가 없어도 defer문은 사용할 수 있다.
<추가설명>
func testDefer() {
print("statement 1")
defer {
print("defer 1")
}
print("statement 2")
}
//statement 1
//statement 2
//defer 1
func testDefer() {
print("statement 1")
defer {
print("defer 1")
}
print("statement 2")
defer {
print("defer 2")
}
defer {
print("defer 3")
}
}
//statement 1
//statement 2
//defer 3
//defer 2
//defer 1
func testDefer() {
print("statement 1")
return
defer {
print("defer 1")
}
}
//statement 1
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Nested Types (0) | 2017.03.23 |
---|---|
Type Casting (0) | 2017.03.23 |
Optional Chaining (0) | 2017.03.18 |
Automatic Reference Counting (ARC) (1) | 2017.03.14 |
Deinitialization (0) | 2017.03.13 |