Swift - Error Handling (예외 처리)
이번 포스트에서는 Swift에서 예외를 정의하고 발생하는 방법에 대해 알아보도록 하겠습니다. Swift에서는 예외(Exception)를 Error라고 표현합니다. Swift에서 에러는 Error Protocol을 따르는 타입의 값으로 표현할 수 있습니다. Enum을 사용하여 예외를 정의할 수 있습니다.
enum AppError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
예외를 발생시킬 때는 아래처럼 사용할 수 있습니다.
throw AppError.insufficientFunds(coinsNeeded: 4)
예외/에러 핸들링 (Exception/Error Handling)
Swift에서는 에러(예외)를 핸들링하는 방법을 4가지로 구현할 수 있습니다.
1. 에러가 발생한 함수에서 리턴값으로 에러를 반환해 해당 함수를 호출한 코드에서 에러를 처리
2. do-catch 구문을 사용하는 방법
3. Optional 값을 반환하는 방법
4. assert를 사용해 강제로 크래쉬를 발생시키는 방법
에러를 반환하는 함수를 생성하려면 throws라는 키워드를 사용하면 됩니다. 아래 코드는 에러를 발생시킬 수 있는 함수와 그렇지 않은 함수를 정의한 것입니다.
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
예외 함수를 사용한 예제 코드를 살펴보도록 하겠습니다.
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 AppError.invalidSelection
}
guard item.count > 0 else {
throw AppError.outOfStock
}
guard item.price <= coinsDeposited else {
throw AppError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
print("Dispensing \(name)")
}
}
vend 함수는 에러를 발생시킬 수 있는 함수입니다. name을 Formal Parameter로 받아서 해당 이름의 아이템이 존재하지 않을 경우 guard문 내의 에러 발생 코드를 실행하게 됩니다. 또한 선택한 아이템의 재고가 0보다 크지 않을 경우 즉, 재고가 없을 경우 재고 없음(Out Of Stock)이라는 에러를 발생시키게 됩니다. 이렇게 에러를 발생시킬 수 있습니다.
따라서 이 함수는 에러를 발생시키기 때문에 이 함수를 호출하는 함수는 반드시 do-catch, try?, try! 등의 구문을 사용해 에러를 처리해야 합니다.
Do-Catch를 사용하여 에러 처리
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
} catch {
statements
}
do 안에는 예외(에러)가 발생할 수 있는 코드 실행 혹은 함수 호출을 진행합니다. 도중에 에러가 발생할 경우 catch의 Parameter로 주어지는 각 예외에 대하여 예외처리를 할 수 있도록 구성할 수 있습니다. 만약 특정 에러를 Parameter로 구성할 수 없다면 error라는 개체로 넘어오기 때문에 아래와 같은 형식으로 error라는 것을 사용하여 예외를 처리할 수 있습니다.
print("Unexpected error: \(error).")
try?를 사용하여 에러를 Optional 값으로 변환하기
try?라는 구문을 사용하여 에러를 Optional 값으로 변환할 수 있습니다.
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
위 코드를 보면 try?는 아래의 do catch로 구현된 코드와 동일한 의미를 가지고 있습니다. try?에서 에러가 발생한다면, x, y의 값은 nil이 됩니다. x, y는 try?의 형태로 정의되고 있으므로 nil이 올 수 있는 Optional 값이라고 할 수 있습니다. 그리고 someThrowingFunction이 Int를 반환하는 함수이므로 x, y는 Int? 타입 (옵셔널) 변수라고 할 수 있겠습니다.
에러가 발생하지 않음을 확신하기 (try!)
함수나 메소드에서 에러가 발생하지 않는다는 것을 보장할 수 있는 상황에서 개발자는 try!를 사용하여 에러가 발생하지 않음을 보장할 수 있습니다. 예를 들어 특정 경로에 존재하는 이미지를 가져올 경우 에러가 발생할 수 있기 때문에 try?를 사용하는 것이 적절하지만 "./Resource/a.jpg"와 같은 경로는 앱에 사전에 포함된 이미지를 불러오는 것이므로 에러가 발생하지 않음이 분명하기 때문에 try? 보다는 try!를 사용하는 것이 적절합니다.
Specifying Cleanup Actions
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.
}
}
Error 처리 예외의 경우에도 defer 구문을 사용할 수 있습니다. 위 예제는 open을 하고 마지막에 종료직전에 close를 호출하는 모습입니다.