Search

[Swift 공식 문서] 17. Error Handling

Error handling 이란 에러 상황에에 대응하고 이로부터 회복하는 과정을 이르는 말입니다.
Swift 는
throwing
catching
propagating
manipulateing
와 같은 종류의 error handling 기능을 제공합니다.
모든 코드가 항상 온전하게 실행되거나 사용가능한 결과를 생산하는 것은 아닙니다.
Optional 은 값의 부재를 나타내는 데 사용되지만, 어떠한 경우에는 코드의 동작 실패의 원인을 이해하는 것이 용이할 때가 있으며, 코드가 원인에 따라 올바르게 대응하도록 하는 것 또한 필요합니다.
예를들어, 파일 또는 디스크로부터 데이터를 읽어오는 작업을 생각해봅시다.
작업이 실패할 경우의 수는 아래와 같이 다양합니다.
해당 경로에 파일이 존재하지 않는 경우
파일의 읽기 권한이 없는 경우
파일이 호환가능한 형식으로 인코딩되어 있지 않은 경우
3가지 경우 모두 작업의 실패란 점에서는 동일하나,
그 원인이 다르기 때문에 에러를 해결하기 위해서는 다르게 동작해야하며,
해결 불가능한 에러의 경우에는 사용자에게 알려야 할것입니다.

Representing and Throwing Errors

Swift 에서 에러는 Error 프로토콜을 준수하는 타입의 값으로 나타냅니다.
이 텅비어있는 프로토콜이 에러 핸들링에 사용될 수 있는 타입임을 나타내게 됩니다.
Swift 의 열거형은 한 그룹의 연관된 에러를 모델링하는데 굉장히 적합합니다.
associated value 를 사용하면 에러의 본질에 관한 추가적인 정보를 담을 수 있습니다.
enum VendingMachineError: Error { case invalidSelection case insufficientFunds(coinsNeeded: Int) case outOfStock }
Swift
복사
error 를 throw 했다는 것은 동작이 일반적인 흐름에서 벗어나 더이상 실행이 불가능해졌다는 의미이다.
throw 문을 사용해서 error 를 throw 할 수 있으며, 아래 예제 코드에서는 5개 의 추가적인 코인이 필요함을 나타내는 모습입니다.
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Swift
복사

Handling Errors

에러가 던져지면 에러를 핸들링하는 데 책임이있는 코드가 실행된다.
Swift 에는 4가지 방식의 에러 핸들링 방법이 존재한다.
1.
propagte error
2.
do-catch
3.
에러를 optional 값으로 처리
4.
에러가 발생하지 않을 것이라고 정의하기
다음 섹션부터 해당 오류에 대해 하나씩 알아가 보자
함수에서 오류가 발생하면 프로그램의 흐름이 변경되기 때문에 코드에서 오류가 발생할 수 있는 위치를 빠르게 식별할 수 있어야 한다. 코드에서 이러한 오류를 식별하려면 try, try?, try! 키워드를 사용하면 된다. 이 키워드에 대해서는 다음 섹션에서 알아보도록 하겠다.

Propagating Errors Using Throwing Functions

함수, 메서드, 생성자가 에러를 발생시킬 수 있다는 것을 나타내기 위해 throws 키워드를 사용한다.
이렇게 throws 키워드와 함께 선언된 함수를 throwing function 이라고 한다.
함수에 리턴타입이 존재한다면 throws 키워드 다음에 화살표와 함께 작성해주어야한다.
func canThrowErrors() throws -> String func cannotThrowErrors() -> String
Swift
복사
throwing function 은 함수가 호출된 지점의 scope 로 error 를 전파한다.
throwing function 만이 error 를 전파(propagate) 할수 있다. nonthrowing function 내부에서 throw 된 error 는 함수 내부에서 처리되어야 한다.
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)") } }
Swift
복사
vend(itemNamed:) 메서드는 guard 문을 사용해서 필요조건이 충족되지 않았을 경우 해당 필요조건에 적합한 에러를 throw 한다.
throw 문은 즉각적으로 프로그램의 제어권을 넘겨주므로, 필요조건이 충족되지 않았을 경우 어떠한 item 도 vend 되지 않는다.
vend(itemNamed:) 메서드가 에러를 throw 하기 때문에 이 메서드를 호출하는 어떤 코드든 에러를 핸들링해주어야합니다.
에러 핸들링에는 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) }
Swift
복사
위 코드에서 buyFavoriteSnack(person:vendingMachine:) 함수는 함수 내부에서 에러를 발생시킬 수 있는 메서드가 호출되었기 때문에 , throing function 입니다.
vend(itemNamed:) 가 발생시키는 모든 에러는 buyFavoriteSnack(person:vendingMachine:) 로 propagte up (전파) 된다.
vend(itemNamed:) 가 에러를 발생시킬 수 있기 때문에 try 문을 사용한다.
throwing initializer 또한 같은 방식으로 에러를 propagate up 할 수 있다.
struct PurchasedSnack { let name: String init(name: String, vendingMachine: VendingMachine) throws { try vendingMachine.vend(itemNamed: name) self.name = name } }
Swift
복사
위 코드에서 생성자는 initializing 과정에서 오류가 발생할 수 있는 함수를 호출하므로, throwing initializer 이다.

Handling Errors Using Do-Catch

do-catch 문을 사용해서 에러를 핸들링하는 코드 블럭을 작성한다.
do 클로져 내부에서 에러가 throw 되면, 그에 해당하는 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 }
Swift
복사
catch 문 뒤에 어떠한 에러를 핸들링할 것인지에 대한 패턴을 작성한다.
catch 클로져에 대응되는 패턴이 없을 경우, error 라는 지역상수에 에러를 바인딩한다.
패턴에 대한 자세한 정보는 Patterns 참고
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."
Swift
복사
위 코드에서 buyFavoriteSnack(person:vendingMachine:) 함수는 오류를 발생시킬 가능성이 있기 때문에 try 표현 내에서 호출된다.
에러가 throw 되면, 실행은 즉각적으로 catch 클로져로 옮겨간다.
일치하는 패턴의 catch 클로져가 실행되며, 일치하는 패턴이 존재하지 않는 경우 지역상수 error 와 바인딩된 마지막 catch 클로져가 실행된다. 만약 error 가 throw 되지 않았다면, 남아있는 do 문 내부의 코드가 실행된다.
catch 클로져는 do 클로져가 throw 할 수 있는 모든 에러를 다룰 필요는 없다.
만약 catch 클로져가 어떠한 에러도 처리하지 않는다면, 에러는 주변의 scope 로 전파되어 처리되어야한다.
nonthrowing function 내부에서, do-catch 문이 모든 에러를 핸들링한다.
하지만
throwing function 에서는 do catch 문 과 호출한 코드(caller) 둘다 에러를 핸들링해야한다.
만약 에러가 처리되지 않고 위 레벨의 scope 로 전파된다면 runtime error 가 발생한다.
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."
Swift
복사
위 코드에서 함수는 VendingMachineError 를 처리할 수 있도록 정의 되었다.
하지만, 그 외의 오류는 처리하지 않고 전파만 하기 때문에 실제로 이 함수를 호출할 때에는
do 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.") } }
Swift
복사
VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock
위 코드의 함수에서는 위에 명시된 3개의 에러에대해서 일괄적으로 처리해주고 있다.
마찬가지로 이 3가지 에러가 아닌 다른 에러는 전파되기만 할 뿐이므로
함수를 호출한 부분에서 do catch 문을 따로 작성해주어서 에러에 대해 처리를 해주어야한다.

Converting Errors to Optional Values

try? 문을 사용해서 에러를 optional 값으로 바꾸어 처리할 수 있다.
→ 만약 에러가 try? 표현 내부에서 발생한다면, 표현의 값은 nil 이된다.
에러를 optional 값으로 변환하기
func someThrowingFunction() throws -> Int { // ... } let x = try? someThrowingFunction() let y: Int? do { y = try someThrowingFunction() } catch { y = nil }
Swift
복사
만약 someThrowingFunction() 가 에러를 발생시킨다면, xy 값은 nil 이된다.
에러가 발생하지 않았다면, 함수의 리턴값이 상수 x y 의 값이 될 것이다.
someThrowingFunction() 의 리턴 타입과는 상관없이 xy 가 optional 해진다.
위 코드에서는 리턴타입이 Int 형이므로 리턴 타입은 optional 한 Int 타입(Int?)이 될것이다.
try? 는 모든 에러를 같은 방식으로 처리하고자 할 때 사용하면 간결한 표현이 가능하다.
아래 코드 처럼 작성하면 모든 오류에 대해서 동일한 방법으로 처리할 수 있다.
func fetchData() -> Data? { if let data = try? fetchDataFromDisk() { return data } if let data = try? fetchDataFromServer() { return data } return nil }
Swift
복사

Disabling Error Propagation

throwing 함수나 메서드가 런타임에 오류를 발생시키지 않을 것이라고 확신이 들면 try!를 사용하면 된다. → 에러 전파 비활성화
하지만 이경우 에러가 발생하게되면 런타임에러가 발생한다.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
Swift
복사
위 코드의 경우 이미지가 애플리케이션 내부에 탑재되어 있기 때문에 런타임 도중 에러가 발생하지 않을 것이다.
따라서 이경우 try! 문을 사용해서 에러 전파를 비활성화 해주면된다.

Specifying Cleanup Actions

defer 문을 사용하면 현재 코드블록을 떠나는 방식에 관계없이 무조건 수행되어야 하는 작업을 수행토록 할 수 있다.
현재 범위가 종료될 때까지 실행을 연기한다.
이를 활용하면 상황에 관계없이 반드시 실행되어야하는 코드를 실행시킬 수 있다.
func processFile(filename: String) throws { if exists(filename) { let file = open(filename) //실행연기 defer { close(file) } // 에러가 발생되어 함수가 종료되어도 defer 문이 실행된다. while let line = try file.readline() { // Work with the file. } // close(file) is called here, at the end of the scope. } }
Swift
복사
위 코드에서는 파일을 닫는 close 함수를 defer 문에 작성해 줌으로써 파일 디스크럽터를 닫고 메모리 할당을 수동으로 종료하는 모습이다.
defer 문들 간의 동작 순서는 소스코드에 적힌 순서의 역순이다.
첫번째 defer 문이 제일 마지막에 실행되며
마지막 defer 문이 제일 처음 실행된다.