에러 처리(error handling)
는 프로그램에서 발생한 에러를 받고 대응하는 과정이다. Swift는 런타임 에러를 발생시키고(throwing), 잡고(catching), 전파하고(propragating), 조작하는(manipulating) 일급 클래스(first-class)
를 지원한다.
몇몇 연산자는 항상 완벽하게 실행되거나 유용한 결과물을 제공하는 것을 보장하지 않는다. 옵셔널은 값의 부재를 나타내는 데 사용한다. 하지만 연산이 실패했을 때 무엇이 실패를 유도했는지 이해하고, 코드가 거기에 따라 대응하도록 하는 게 유용할 때가 있다.
디스크에서 파일을 읽고 데이터를 쓰는 작업을 고려해 보자. 존재하지 않는 파일에 경로를 설정하거나, 읽기 권한이 없거나, 적절한 포맷으로 인코딩되어 있지 않는 등, 이 작업이 실패할 경우의 수는 많다. 이렇게 서로 다른 상황을 구별하는 것은 프로그램이 특정한 에러를 해결하고, 해결할 수 없는 에러는 유저와 소통할 수 있게 해 준다.
Swift의 에러 처리는 Cocoa와 Objective-C의
ESError
를 사용하는 에러 처리 패턴과 상호 운용한다.
Swift에서 에러는 Error
프로토콜을 준수하는 타입의 값으로 표현된다. 이 빈 프로토콜은 에러를 처리하는 데 이 타입을 사용할 수 있음을 나타낸다.
Swift의 열거형은 특별히 이런 관련된 에러를 그룹화(Grouping)하고 추가적인 정보를 제공하기에 적합하다. 예를 들어, 게임 안에서 판매 기기 동작의 에러 상황을 다음과 같이 표현할 수 있다.
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
에러를 발생시킴으로써 예상치 못한 것이 발생하거나 일반적인 실행 흐름이 계속될 수 없다는 것을 알릴 수 있다. throw
문 을 사용해 에러를 발생시킨다. 아래 코드는 판매 기기에 5개의 코인이 더 필요하다는 것을 알리는 에러를 발생시킨다.
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
에러가 발생했을 때, 특정 코드 영역은 에러를 처리해야 할 책임이 있다. 문제를 수정하거나, 다른 대안을 시도하거나, 사용자에게 실패를 알리거나 해야 한다.
Swift에는 에러를 처리하는 네 가지 방법이 있다.
do-catch
문을 사용.함수가 에러를 발생시킬 때, 이는 프로그램의 흐름을 바꾼다. 때문에 에러를 발생시킬 수 있는 코드의 위치를 빠르게 파악하는 것이 중요하다. 이를 위해 try
키워드를 사용한다. 에러를 발생시킬 수 있는 함수, 메소드, 이니셜라이저를 호출하는 코드 이전에 작성한다.
Swfit의 에러 처리는 다른 언어의 에러 처리와 유사하다. Objective-C를 포함해 다른 언어의 exception 처리와 다른 점은 Swift에서 에러 처리는 많은 계산이 필요할 수 있는 콜스택(call stack) 되돌리기(unwinding)와 관련이 없다는 것이다. 그렇기 때문에 에러를 반환하는
throw
구문은 일반적인 반환 구문인return
구문과 비슷한 성능을 보여준다.
함수, 메소드, 이니셜라이저가 에러를 발생시킬 수 있음을 알리기 위해, 매개 변수 선언 뒤에 throws
키워드를 작성한다. throws
가 표시되어 있는 함수를 throwing function
이라고 부른다. 만약 함수가 반환 타입을 지정했다면, 매개 변수 선언과 반환 화살표(->) 사이에 키워드를 적는다.
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
throwing function
은 그 함수를 호출한 곳으로 에러를 전파한다.
throwing function
만이 에러를 전파할 수 있다.throwing function
이 아닌 함수는 함수 안에서 에러를 처리해야 한다.
아래 예시는 VendingMachine
클래스를 정의한다. 이 클래스는 VendingMachineError
를 발생시킬 수 있는 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(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)
}
이 예시에서 buyFavoriteSnack(person:vendingMachine:)
함수는 사용자가 가장 좋아하는 스낵을 찾고, vend(itemNamed:)
를 호출하여 구매를 시도한다. vend(itemNamed:)
가 에러를 발생시킬 수 있기 때문에 try
키워드와 함께 호출한다.
throwing initializer
는 throwing function
과 동일한 방법으로 에러를 전파한다. PurchasedSnack
구조체의 이니셜라이저는 초기화 과정에서 throwing function
을 호출한다. 그리고 이 구조체를 호출한 곳으로 에러를 전파하여 이를 처리한다.
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
코드 블록에서 에러를 처리하기 위해 do-catch
문을 사용한다. 만약 do
구문에서 에러가 발생한다면 에러의 종류를 catch 구문으로 구분해 처리할 수 있다. 어떤 것이 에러를 처리할 수 있는지 결정한다.
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
} catch {
statements
}
어떤 에러를 이 구문이 처리할 수 있는지 알리기 위해 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(person:vendingMachine:)
함수는 에러를 발생시킬 수 있기 때문에 try
표현으로 호출되었다. 에러가 발생하면 실행은 즉시 catch
구문으로 옮겨져 전파를 계속할 것인지 결정한다. 만약 매치되는 패턴이 없다면 에러는 마지막 catch
구문에 잡히고, 지역 상수 error
에 바인딩 된다. 에러가 발생하지 않는다면 do
구문이 계속 실행된다.
catch
구문은 do
구문에서 발생할 수 있는 모든 가능한 에러를 처리할 필요는 없다. 만약 에러를 처리할 수 있는 catch
구문이 없다면 에러는 주변 범위(surrounding scope)로 에러를 전파한다. 전파된 에러는 반드시 특정 주변 범위에서 처리되어야 한다. 에러를 발생 시키지 않는 함수에서는 관련된 do-catch
구문에서 그 에러를 반드시 처리해야 하고, 에러를 발생 시키는 함수에서는 에러를 do-catch
구문에서 처리하거나 함수를 호출한 곳에서 반드시 에러를 처리해야 한다. 만약 에러를 처리하지 않고 최상위 범위까지 전파하면 런타임 에러가 발생한다.
예를 들어, 위의 예시는 VendingMachineError
가 아닌 모든 오류가 호출한 함수에 의해 포착되도록 작성될 수 있다.
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError {
print("Invalid selection, out of stock, or not enough money.")
}
}
do {
try nourish(with: "Beet-Flavored Chips")
} catch {
print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Invalid selection, out of stock, or not enough money."
nourish(with:)
함수에서, 만약 vend(itemNamed:)
가 VendingMachineError
열거형의 케이스 중 하나인 에러를 발생시킨다면, nourish(with:)
은 메시지를 출력함으로써 에러를 처리한다. 그렇지 않으면 이 함수를 호출한 곳으로 에러를 전파한다.
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
값은 함수의 반환 값이 된다. x
와 y
는 someThrowingFunction()
가 반환하는 타입의 옵셔널이다. 이 경우에는 함수가 Int
를 반환하기 때문에 x
와 y
는 Int?
를 반환한다.
try?
를 사용하면 모든 에러를 같은 방식으로 처리하고 싶을 때 간결한 에러 처리 코드를 작성할 수 있다. 예를 들어, 아래의 코드는 데이터를 가져오기 위한 몇 가지 접근법을 보여 준다. 접근이 실패하면 nil
을 반환한다.
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
함수나 메소드가 에러를 발생시키지 않을 것이라 확신하는 경우, try!
키워드를 사용한다. 에러 전파를 비활성화하고, 에러가 발생하지 않도록 runtime assertion
으로 호출을 래핑할 수 있다. 실제 에러가 발생할 시 런타임 에러가 나타난다.
다음 코드는 주어진 경로의 이미지 리소스를 로드하거나 경로가 없을 시 에러를 발생시키는 loadImage(atPath:)
함수를 사용한다. 이 경우 이미지는 애플리케이션과 함께 제공되므로 런타임 시 에러가 발생하지 않을 것이라 확신할 수 있다. 때문에 에러 전파를 비활성화하는 것이 적절하다.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
코드 실행이 현재 코드 블록을 떠나기 직전에 defer
문을 사용하여 일련의 구문을 실행할 수 있다. 이 구문은 어떻게 현재 코드 블록을 떠나는지(에러 발생, return
, break
등)와 상관없이 실행되어야 하는 모든 필요한 정리를 하도록 한다. 파일 디스크립터를 닫고 수동으로 메모리를 비우기 위해 defer
문을 사용할 수 있다.
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.
}
}
위의 코드는 open(_:)
함수 이후 close(_:)
함수의 호출을 보장하기 위해 defer
문을 사용한다.
코드에 에러 처리가 없어도
defer
문을 사용할 수 있다.