[Swift5] Error Handling
- 다음은 Swift 5.6 Doc의 Error Handling 공부 내용을 정리했음을 밝힙니다.
Error Handling
에러 핸들링은 프로그램 에러 조건을 조정하는 과정이다. 스위프트는 런타임 도중 복구 가능한 에러를 스로잉하고, 캐칭하고, 프로파게이팅하고, 조작할 수 있도록 지원한다.
특정 연산이 언제나 성공한다고 보장할 수 없을 때가 있다. 옵셔널을 통해 특정 값이 널 값인지 체크할 수 있다.
가령 디스크에 존재하는 파일에서 데이터를 읽고 쓰는 작업을 할 때 파일이 경로 상에 없거나 읽기 권한이 없거나 읽을 수 있는 포맷으로 인코딩되어 있지 않을 수 있다.
에러 표현 및 스로잉
스위프트에서 에러는 에러 프로토콜 중 어느 한 타입의 값으로 표현된다. 스위프트는 열거 타입을 통해 연관된 에러 조건을 그룹으로 만들어 사용하고 있다.
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
에러 스로잉을 통해 예상치 못한 일이 발생했고 실행 중 정상 흐름이 지속하지 못함을 알 수 있다. 즉 스로잉 문으로 에러를 던져버릴 수 있는 것이다.
에러 핸들링
에러 스로잉이 일어나면 스로잉 문을 둘러싼 코드가 에러 핸들링을 담당한다.
스위프트에서 에러를 핸들링하는 방법은 네 가지다.
- 특정 함수에서 에러가 일어났을 때 이 함수를 호출한 코드 블록으로 에러를 프로파게이트한다.
do-catch
문으로 에러를 핸들링한다.- 옵셔널 값으로 에러를 취급한다.
- 에러가 일어나지 않는다고
assert
한다.
함수가 에러를 스로잉할 때 프로그램 실행 흐름이 바뀌기 때문에 에러를 스로잉하는 코드 위치를 정하는 게 중요하다. try
또는 try?
, try!
를 사용하자.
함수를 스로잉해서 에러 프로파게이트
함수, 메소드, 이니셜라이저가 에러를 스로잉한지 알려주려면 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)")
}
}
vend
함수는 파라미터로 받은 물품 이름을 통해 물품의 개수, 가격 등을 확인해 에러를 스로잉한다. VendingMachineError
는 커스텀한 에러 열거형임을 기억하자. 에러가 감지되면 곧바로 종료하도록 유도된다.
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)
}
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
try
키워드를 붙인 까닭은 vend
에서 에러가 발생할 수 있기 때문이다.
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
문 다음에 패턴을 쓴다. 패턴이 없다면 모든 종류의 에러와 매치되서 error
라는 이름의 지역 상수로 에러가 연결된다.
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."
try
문으로 코드 실행 중 에러가 발생하면 catch
를 통해 에러 종류에 따라 내부 코드 블록을 실행할 수 있다. 에러가 던져지면 실행 흐름이 곧바로 그에 해당하는 catch
절로 이어진다. 패턴 매칭이 안 되며 마지막 절의 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."
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.")
}
}
nourish
함수에서 vend
는 에러를 스로잉할 수 있는 함수다. 하지만 여기에서 스로잉되는 에러가 아닌 에러가 일어난다면 do
이후catch
코드 블록에서 다뤄짐을 볼 수 있다.
에러를 옵셔널 값으로 변환하기
try?
키워드를 통해 옵셔널 값으로 에러를 변환할 수 있다. 옵셔널로 에럭 변환된다면 이 값은 널 값이다.
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
someThrowingFunction
의 리턴 값이 정수이기 때문에 try?
로 핸들링된 에러 옵셔널은 정수 옵셔널이다.
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
fetchDate
함수는 디스크 또는 서버에서 데이터를 패치하며, 차례대로 성공하면 그대로 데이터를 리턴하고 그렇지 않으면 널 값을 리턴한다.
에러 프로파게이션을 비활성화하기
스로잉 함수/메소드가 런타임 도중 에러를 스로잉하지 않을 때가 있다. 이때 try!
키워드를 통해 에러 프로파게이션 기능을 꺼서, 런타임 도중 어떤 에러도 스로잉하지 않음을 표현할 수 있다. 이때 에러가 일어나면 런타임 에러가 일어난다.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
메모리 정리
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
하는 기능이다.
Author And Source
이 문제에 관하여([Swift5] Error Handling), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@j_aion/Swift5-Error-Handling저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)