에러 처리(Error Handling)는 프로그램의 에러 조건에서 응답하고 복구하는 프로세스이다. Swift는 런타임 에러가 발생하면 에러를 처리하기 위해 에러의 발생(throwing), 감지(catching), 전파(propagating), 조작(manipulating)을 지원하는 최고 수준의 지원을 제공한다.
일부 작업은 항상 실행을 완료하거나 유용한 출력을 생성하는 것이 보장되지 않는다. 옵셔널을 사용해 에러가 발생해 값이 없다고 nil로 표시할 수 있다. 하지만 어떤 종류의 에러가 발생했는지는 알 수 없다. 이런 경우 구체적으로 어떤 에러가 발생했는지 확인할 수 있어야 경우에 따른 적절한 처리를 할 수 있다.
예를 들어 디스크의 파일에서 데이터를 읽고 처리하는 작업을 생각해 보자
지정된 위치에 파일이 없거나 파일에 읽기 권한이 없거나 적절한 형식으로 인코딩 되지 않는 것을 포함해 작업이 실패할 수 있는 여러 경우가 있다. 이러한 모든 상황을 구분하면 프로그램에서 에러를 해결하고 해결할 수 없는 에러를 전달할 수 있다.
Swift에서 에러 처리는 Cocoa와 Objective-C에 NSError를 사용하는 에러 처리 패턴과 상호 운용된다.
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 Errors
에러가 발생할 때 특정 코드의 부분이 에러 처리를 담당해야 한다. 문제를 수정하거나 다른 방법을 시도하거나 사용자에게 알리는 방법으로 에러를 처리해야 한다.
Swift에는 에러를 처리하는 4가지 방법이 있다.
- 에러 전파(error propagation) - 함수에서 해당 함수를 호출하는 코드로 에러 전파
- do-catch 구문 사용
- 옵셔널 값 사용
- 에러가 발생하지 않을 것이라고 정의하는 방법 (assert를 사용해 강제로 크래쉬를 발생시키는 방법)
함수에서 에러가 발생하면 프로그램의 흐름이 변경되므로 코드에서 에러가 발생할 수 있는 위치를 신속하게 알 수 있어야 한다. try, trt?, try! 키워드를 사용해 코드에서 이러한 오류를 식별할 수 있다.
Swift에서 에러 처리는 계산 비용이 많이 드는 프로세스인 호출 스택 해제가 포함되지 않는다. 따라서 throw 구문의 성능 특성은 return 구문의 성능 특성과 비슷하다.
Propagating Errors Using Throwing Functions
에러가 발생할 수 있는 함수, 메서드, 또는 생성자를 나타내기 위해 선언 중 파라미터 뒤에 throws 키워드를 작성한다. throws로 표시된 함수는 던지기 함수(throwing function)라고 부른다. 함수에 반환 타입이 지정되어 있으면 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 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)")
}
}
위 코드는 VendingMachine 클래스에서 발생 가능한 오류를 처리하기 위한 vend(itemNamed:) 메서드를 가진다.
guard 구문을 사용해 스낵을 구매하는 과정에서 에러가 발생하면 함수에서 에러를 발생시키고 빠르게 함수를 탈출할 수 있도록 한다.
vend(itemNamed:) 메서드는 발생하는 에러를 전파하기 때문에 이 메서드를 호출하는 함수는 do-catch 구문, try? 또는 try! 를 사용해 에러를 처리하거나 계속 전파해야 한다.
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:) 메서드에서 발생한 에러는 buyFavoriteSnack(person:vendingMachine:) 함수가 호출된 지점까지 전파된다.
vend(itemNamed:) 메서드는 에러가 발생할 수 있으므로 try 키워드를 앞에 두어 호출된다.
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
던지기 생성자는 함수와 같은 방법으로 에러를 전파할 수 있다. PurchasedSnack 구조체의 생성자는 초기화 프로세스 부분으로 던지기 함수를 호출하고 발생하는 모든 에러를 호출자에게 전파하여 처리한다.
Handling Errors Using Do-Catch
에러가 do 절에서 발생하면 catch 절과 비교하여 에러를 처리할 수 있다.
do {
try <#expression#>
<#statements#>
} catch <#pattern 1#> {
<#statements#>
} catch <#pattern 2#> where <#condition#> {
<#statements#>
} catch <#pattern 3#>, <#pattern 4#> where <#condition#> {
<#statements#>
} catch {
<#statements#>
}
catch 뒤에 처리할 수 있는 에러를 적고 어떻게 처리할 것인지 명시할 수 있다. 만약 catch 절이 패턴을 가지고 있지 않다면 발생하는 모든 에러를 error라는 지역 상수로 에러를 바인드 한다.
VendingMachineError 열거형의 3가지 에러 종류에 대해 처리하는 코드
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 절이 없다면, 에러는 마지막 catch 절에 의해 포착되고 지역 error 상수에 바인딩된다. 에러가 발생하지 않으면 do 구문 나머지가 실행된다.
catch 절은 do 절에서 발생할 수 있는 모든 에러를 처리할 필요는 없다. catch 절이 에러를 처리하지 않으면 에러는 주변에 전파된다. 그러나 전파된 에러는 일부 주변 범위에서 언젠가는 처리되어야 한다. 던지지 않는 함수에서는 do-catch 구문에서 에러를 처리해야 한다. 던지는 함수에서는 do-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."
위 코드는 함수 내에서 VendingMachineError를 처리할 수 있도록 정의한다. 하지만 그 외의 에러는 처리하지 않고 전파만 한다. 그렇기 때문에 실제로 사용할 때는 do-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.")
}
}
여러 개의 오류를 동시에 처리하는 방법은 catch 뒤에 오류의 종류를 나타낼 때 쉼표로 구분한다. 위 코드처럼 작성하면 세 가지 오류를 동시에 처리할 수 있다.
Converting Errors to Optional Value
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의 타입이 어떻든 상관없이 옵셔널 값을 가진다.
try?를 사용하면 모든 에러를 같은 방식으로 처리할 수 있다.
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
Disabling Error Propagation
throwing 함수 또는 메서드가 실제로 런타임 에러를 발생시키지 않을 것이라는 확신이 드는 경우 try! 를 사용할 수 있다. 이 경우 에러 전파를 비활성화한다. 에러가 발생하면 런타임 에러가 발생한다.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
예를 들어 주어진 경로의 이미지를 로드하거나 이미지를 로드할 수 없을 때는 에러를 발생하는 loadImage(atPath:) 함수가 있다. 이 경우 이미지는 이미지 앱과 함께 제공되고 런타임에 에러가 발생하지 않으므로 에러 전파를 비활성화한다.
Specifying Cleanup Actions
코드의 현재 블록이 종료되기 직전에 어떠한 작업을 수행하려면 defer 구문을 사용해 특정 코드를 실행할 수 있다. 이 구문을 사용하면 에러가 발생되거나 return 또는 break 같이 현재 코드 블록을 떠나는 방식에 상관없이 수행해야 하는 작업을 수행할 수 있다.
defer 구문은 현재 범위가 종료될 때까지 실행을 연기한다. defer 키워드와 나중에 실행될 구문으로 구성되어 있다. 지연된 구문은 return 또는 break 구문과 같이 구문의 밖으로 제어를 이동하거나 에러를 발생시키는 코드를 포함할 수 없다. 지연된 동작은 소스 코드에 작성된 순서와 반대로 실행된다. 즉 첫 번째 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.
}
}
위 예제는 open(_:) 함수에 close(_:)에 대한 호출이 있는지 확인하기 위해 defer 구문을 사용한다.
에러 처리 코드가 포함되어 있지 않아도 defer 구문을 사용할 수 있다.
'Swift 공식 문서' 카테고리의 다른 글
Type Casting (0) | 2024.01.15 |
---|---|
Concurrency (0) | 2024.01.11 |
Optional Chaining (1) | 2024.01.05 |
Deinitialization (1) | 2024.01.04 |
Initialization (1) | 2024.01.03 |