프로그램 실행시 에러가 발생하면 그 상황에 대해 적절한 처리가 필요하다. 이 과정을 에러 처리라고 부른다. Swift에서는 런타임 에러가 발생한 경우 처리를 위해 에러의 발생(throwing), 감지(catching), 증식(propagating), 조작(manipulating)을 지원하는 first-class를 제공한다.
어떤 명령은 항상 완전히 실행되는 것이 보장되지 않는 경우가 있다. 그런 경우에 옵셔널을 사용해 에러가 발생해 값이 없다는 것을 표시할 수 있지만, 어떤 종류의 에러가 발생했는지 확인할 수는 없다. 이럴 때는 구체적으로 발생한 에러를 확인할 수 있어야 코드를 작성하는 살람이 각 에러의 경우에 따른 적절한 처리를 할 수 있다.
Swift에서 에러는 Error 프로토콜을 따르는 타입의 값으로 표현된다. 비어있는 이 프로토콜은 프로토콜을 따르는 타입이 에러 처리를 위해 사용 될 수 있다는 것을 말한다.
Swift의 열거형은 특별히 이런 관련된 에러를 그룹화하고 추가적인 정보를 제공하기에 적합하다.
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
에러를 발생시킴으로써 무언가 기대하지 않았던 동작이 발생했고 작업을 계속 수행할 수 없다는 것을 알려줄 수 있다. 에러를 발생시키기 위해 throw 구문을 사용할 수 있다.
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
위 코드는 판매 기기에서 5개의 코인이 더 필요하다라는 에러를 발생시킨다.
에러가 발생하면 특정 코드영역이 해당 에러를 처리하도록 해야한다.
에러가 발생한 함수에서 리턴값으로 에러를 반환해 해당 함수를 호출한 코드에서 에러를 처리
do-catch 구문을 사용
옵셔널 값을 반환
assert를 사용해 강제로 크래쉬를 발생
어떤 람수, 메소드 혹은 초기자가 에러를 발생 시킬 수 있다는 것을 알리기 위해서 throw 키워드를 함수 선언부의 파라미터 위에 붙일 수 있다. throw 키워드로 표시된 함수를 throwing function이라고 부른다. 만약 함수가 리턴값을 명시한다면 throw 키워드는 리턴값 표시 기호인 -> 전에 적는다.
func canThrowError() throw -> String
func cannotThrowError() -> String
throwing function은 함수 내부에서 에러를 만들어 함수가 호출된 곳에 전달한다.
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 구문을 사용해 snack
을 구매하는 과정에서 에러가 발생하면 함수에서 에러를 발생시키고 빠르게 함수를 탈출할 수 있도록 한다. 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: )
함수가 실행되는 곳에까지 전해진다.
buyFavoriteSnack(person: , vendingMachine: )
함수가 주어진 favoriteSnacks[person]
이 뭔지 확인하고 vend(itemNamed: )
메소드를 호출해 구매 시도를 한다. 왜냐하면 vend(itemNamed: )
메소드는 에러를 발생 시킬 수 있기 때문에 메소드 호출 앞에 try
키워드를 사용한다. 에러 발생 초기자는 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-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
구문이 없다면 에러는 마지막 catch
구문에 걸리게 되서 지역 에러 상수인 error
로 처리할 수 있다. 만약 아무런 에러도 발생하지 않는다면 do
구문이 실행된다. catch
구문에서 발생 가능한 모든 에러에 대해 반드시 종류 별로 처리할 필요는 없다. 만약 에러를 처리하는 적절한 catch
구문이 없다면 그 코드에 둘러 쌓인 곳에 에러가 발생한다. 하지만 발생 되는 에러는 반드시 관련된 특정 코드 영역에서 처리돼야 한다. 에러를 발생 시키지 않는 함수에서는 관련된 do-catch
구문에서 그 에러를 반드시 처리해야 하고, 에러를 발생 시키는 함수에서는 에러를 do-catch
구문에서 처리하거나 함수를 호출한 곳에서 반드시 에러를 처리해야 한다. 만약 에러가 발생한 곳에서 에러에 대해 아무런 처리도 하지 않으면 런타임 에러가 발생하게 된다. 예를 들어, 위 코드는 모든 VendingMachineError
에 대해 기술하는 것 대신 다음과 같이 처리 할 수 있다.
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError { // 모든 VendingMachineError 구분을 위해 is를 사용
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)")
// 여기에서 처럼 catch를 그냥 if-else에서 else 같이 사용 가능
}
// Prints "Invalid selection, out of stock, or not enough money."
nourish(with: )
함수에서 만약 vend(itemNamed: )
초기자에서 VendingMachineError
열거형 중 한가지의 에러가 발생한 경우, nourish(with: )
함수가 에러를 처리해 메시지를 출력한다. 반면 nourish(with: )
함수는 그것을 호출한 곳에 에러를 발생 시킨다. 발생한 에러는 그리고 나서 일반적인 catch
구문에 의해 처리 된다.
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()
의 타입이 어떤 것이든 상관없이 옵셔널이 된다. 이 함수에서는 integer
을 리턴하기 때문에 x
와 y
는 옵셔널 integer
이다. 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: )
함수를 사용해 주어진 경로에서 이미지 리소스를 불러오거나 이미지를 불러오는데 실패한 경우 에러를 발생 시킨다. 이 경우에는 앱이 배포될때 이미지가 포함되 배포되기 때문에 런타임에는 아무 에러도 발생되지 않을 것이라 확신할 수 있어 try!
를 사용하는 것이 적절하다.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
defer
구문을 이용해 함수가 종료 된 후 파일 스트림을 닫거나, 사용했던 자원을 해지 하는 등의 일을 할 수 있다. defer
가 여러개가 있는 경우 가장 마지막 줄부터 실행 된다. 즉 bottom-up 순으로 실행 된다.
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
// block이 끝나기 직전에 실행, 주로 자원 해제나 정지에 사용
}
while let line = try file.readline() {
// Work with the file.
}
// close(file) is called here, at the end of the scope.
}
}
위 예제는 defer
구문을 이용해 open(:)
함수와 짝을 이루는 close(:)
함수를 실행한다.
열거형에 에러를 그룹화 한다는 개념이 신박하네요. 에러를 저런 식으로 처리하는 걸 생각을 해본 적이 없어서 그런지... 자바에도 가능한지 한번 적용시켜봐야겠네요 ㅋㅋㅋ 감사합니다.