[Swift] 공식문서 17 - Error Handling(에러 처리)
Error Handling — The Swift Programming Language (Swift 5.5)
Error Handling Error handling is the process of responding to and recovering from error conditions in your program. Swift provides first-class support for throwing, catching, propagating, and manipulating recoverable errors at runtime. Some operations aren
docs.swift.org
1차 수정 (2022 / 02 / 27) : 의역, 오타 및 스타일 수정
Error Handling
에러 처리(Error Handling) 은 사용자 프로그램의 에러 상태에 대응하고 복구하는 과정입니다. Swift는 throwing, catching, propagating, 런타임에 복구 가능한 에러를 실행하는 1급(first)-클래스를 지원합니다.
몇몇 실행문들은 실행을 완성하거나 출력을 한다는 것이 보장되지 않습니다. 옵셔널이 값이 없을 때를 위해 사용되지만, 실행문이 실패할 때 "무엇이 실패를 만드는지 이해하는 것"은 유용합니다. 이에 따라 코드가 알맞게 응답할 수 있습니다.
예를 들어, 디스크의 파일에서 데이터를 읽고 처리하는 일을 생각해보겠습니다. 이 일이 실패할 경우가 매우 많습니다. 파일이 특정경로에 존재하지 않는다거나, 파일에 읽기 권한이 없거나, 파일이 호환되는 형식으로 인코딩되어 있지 않는 경우들이 있습니다. 이런 특정 상황들을 구별하는 것은 프로그램에게 오류를 해결하고 해결할 수 없는 오류를 사용자에게 전달할 수 있도록 해줍니다.
Swift의 에러 처리는 Cocoa 와 Objective-C의 NSError 클래스를 사용하는 에러 처리 패턴과 상호운용(interoperates) 됩니다.
Representing and Throwing Errors (에러 발생과 스로잉)
Swift에서 에러는 Error 프로토콜에 맞춰 타입의 값을 나타냅니다. 이 빈 프로토콜은 타입이 에러처리를 위해 사용될 수 있다는 것을 나타냅니다.
Swift 열거형은 연관된 에러 상태를 모델링하기 위해 특별히 최적화되어 있습니다. 관련된 값을 사용하여 오류의 특성에 대한 추가 정보를 전달할 수 있습니다.
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
예제는 자판기에서 어떻게 에러가 실행되는지 보여줍니다.
에러 스로잉은 사용자에게 뭔가 예상치 못한 일이 일어났고 정상적인 실행이 지속될 수 없음을 알려줍니다. throw 를 작성하여 에러를 스로잉합니다.
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Handling Erros(에러 처리)
에러가 스로잉되면, 둘러싸고있는 코드들이 반드시 에러처리를 해주어야 합니다. 예를 들어 문제를 바로잡거나, 다른방식을 시도하거나, 사용자에게 실패를 알립니다.
Swift에 에러를 처리하는 4가지 방식이 있습니다.
- 함수로부터 해당 함수를 호출하는 코드로 에러를 전달합니다
- do-catch문을 사용하여 에러를 처리합니다.
- 옵셔널 값으로 에러를 처리합니다.
- 에러가 발생하지 않는다고 주장(assert)합니다.
(my opinion : assert는 단언하다, 주장하다 의 뜻을 가지는데 확신을 가지고 주장한다는 의미로 생각하면 될 것 같습니다. assertrion 은 코드에서 반드시 통과해야 하는 문을 나타냅니다)
함수가 에러를 스로잉하면, 프로그램의 흐름(flow)를 바꿉니다. 따라서 에러를 던지는 코드가 어디에 있는지 빠르게 알아차리는 것이 매우 중요합니다. 코드에서 어디인지 알기위해, try키워드를 사용합니다. 에러를 스로잉할 수 있는 함수, 메소드, 이니셜라이져 앞에 try를 작성합니다. (try?, try! 도 사용 가능합니다)
Swift의 에러 처리는 다른 언어에서 사용하는 예외처리 방식과 비슷합니다. try,catch, throw를 사용하는 방식. Objective-C를 포함한 많은 언어들의 예외처리와 달리, Swift의 에러처리는 콜 스택을 풀지(unwinding) 하지 않으며 이 과정은 많은 계산 비용이 듭니다. 이와 같이 throw문의 수행 특성은 return 문과 비교됩니다.
Propagating Errors Using Throwing Functions(스로잉 함수를 사용한 에러 전달)
함수, 메소드, 이니셜라이져가 에러를 스로우 할 수 있다는 것을 나타내기 위해, 함수의 선언에서 파라미터 () 뒤에 throws 키워드를 작성합니다. throws를 사용한 함수를 "스로잉 함수" 라고 부릅니다. 만약 함수가 특정 리턴 타입을 가지면 -> 전에 throws를 작성합니다.
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
스로잉 함수는 함수가 호출되는 범위 안에서 에러를 전달합니다.
스로잉 함수만 에러를 전달할 수 있습니다. 스로잉 함수가 아닌 함수는 함수 안에서만 에러를 다룹니다.
아래 예제에서 VendingMachine 클래스는 에러를 던지는 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문을 사용해 메소드를 일찍 빠져나옵니다. 또 구매하는 스낵에 대한 요구를 만족시키지 못하면 적절한 에러를 스로잉합니다. thorw문은 프로그램 제어를 즉시 바꾸기 때문에, 모든 요구를 만족하는 경우에만 품목이 제공됩니다.
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를 사용해줍니다.
스로잉 이니셜라이져(throwing initializers)는 스로잉 함수와 같은 방식으로 에러를 전달합니다.
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
예제에서 이니셜라이져 과정 중 스로잉 함수를 사용합니다. 모든 오류를 호출자에게 전달함으로써 처리합니다.
Handling Errors Using Do-Catch (Do-Catch 문을 사용한 에러 처리)
코드 블럭에서 do-catch문을 사용해서 에러를 처리할 수 있습니다. do 문 안에서 에러가 스로잉되면, catch문에서 해당 에러를 처리할 문을 찾습니다.
catch 뒤에 패턴(pattern)을 작성함으로써 어떤 에러를 처리할 지 설정합니다. 캐치 절에 패턴이 없으면, 캐치 절은 모든 에러를 로컬 상수 error로 바인딩합니다. (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 절이 없으면 주변으로 에러를 전달합니다. 주변으로 전달된 에러는 반드시 처리되어야 합니다. 쓰로잉 함수가 아닌 함수에서 do-catch문은 오류를 처리해야 합니다. 스로잉 함수에도 do-catch문이나 호출자가 에러를 반드시 처리해야 합니다. 만약 에러가 처리되지 않고 최상위 범위(scope) 로 전달되면 런타임 에러가 발생합니다.
위의 예제에서 VendingMachineError 가 아닌 모든 오류가 호출 함수에서 catch되도록 작성되었습니다.
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:) 함수는 VendingMachineError를 한 번에 적었습니다. 3 가지 중 하나에 해당하는 에러가 스로잉 되면 catch 절이 에러를 처리합니다. 다른 에러들은 주변 범위로 전달됩니다.
Converting Errors to Optional Values (옵셔널 값으로 에러 전환)
try? 를 사용해 에러를 옵셔널 값으로 바꿀 수 있습니다. 만약 에러가 try? 표현식에서 발생한다면, 표현식의 값은 nil 이 됩니다.
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가 리턴하는 값에 관계없이 x,y 는 옵셔널이 됩니다.
try?를 사용해 모든 에러를 같은 방식으로 처리하고 싶을 때 간결한 오류 처리코드를 작성하게 해줍니다. 아래 예제는 데이터에 접근합니다. 접근이 실패하는 모든 경우에 대해 nil을 리턴합니다.
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
Disabling Error Propagation (에러 전달 미사용)
종종 스로잉 함수 혹은 메소드가 런타임에 에러를 스로잉하지 않습니다. 이 때 에러 전달을 하지 못하도록 표현식 앞에 try!를 사용합니다. 오류가 발생하지 않도록 런타임 assertion으로 호출을 래핑합니다. 실제로 에러가 발생하면 런타임에러가 발생합니다.
(런타임 중에 에러가 발생해도 모를 경우가 있다. 이럴 때 에러를 래핑해서 최상위 범위(scope)로 올려 런타임 에러를 발생시킨다)
예제의 loadImage(atPath:) 함수는 이미지를 로드하고, 실패할 경우 에러를 쓰로잉합니다. try! 표현식을 사용해 에러 발생시 런타임 에러를 발생시킨다.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
Specifying Cleanup Actions (정리 작업 지정하기)
defer 문을 사용해 코드 실행이 현재 코드 블럭을 떠나기 바로 전에 실행문을 실행시킵니다. 이 문을 사용하면 "어떻게 현재 코드 블럭을 벗어나는 지에 상관없이" 수행해야 하는 정리를 실행할 수 있습니다. (오류 발생 혹은 리턴, break 하는 실행문 때문에 남겨집니다) 예를 들어 파일 설명자가 닫히고 수동으로 할당된 메모리가 해제되면 defer문을 사용할 수 있습니다.
defer 문은 현재 스코프가 끝날 때까지 실행을 지연시킵니다. defer 키워드를 사용해 작성합니다. defer 문은 문 밖으로 나가는 코드를 포함할 수 없습니다. break, return, 에러 쓰로잉 문 등을 포함할 수 없습니다. defer 문은 소스코드에 작성한 순서 반대로 실행됩니다. 이 말은 제일 먼저 작성된 defer 문이 가장 마지막에 실행된다는 말입니다. (마지막으로 작성된 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.
}
}
예제는 defer 문을 사용해 open(_:) 함수를 사용할 때 close(_:) 함수가 짝지어 오도록 보장합니다.
(파일을 열면 파일을 닫아주어야 합니다. defer문을 사용해 close(file) 을 open(filename) 과 같이 작성할 수 있습니다)
오류 처리 코드가 없는 경우에도 defer 문을 사용합니다.
throws 를 통해 에러 전달
do-catch를 통해 전달된 에러를 처리
혹은 다른 곳으로 에러 계속 전달
에러를 처리하지않으면 최상위 스코프로 올라가게 되고 이에 따라 런타임 에러 발생
어디서 에러가 발생하는 지 알고 싶을때 try!를 사용해 일부러 런타임 에러 발생 가능
defer문을 사용해 코드 블럭 안에서 마지막에 실행하고 싶은 문을 가독성 있게 작성가능
Error Handling 끗!
😙