티스토리 뷰

iOS

[Swift] 에러처리 (Error Handling)

Peppo 2022. 6. 1. 19:17
728x90

에러 처리 (Error Handling)

 

Swift에서는 런터임 에러가 발생한 경우 Error 처리를 위해 아래의 일급 클래스(first-class)를 제공합니다.

Error처리를 위한 일급 클래스

- Error 발생 (throwing)
-
Error 감지 (cathing)
-
Error 증식 (propagating)
-
Error 조작 (manipulating)

 

완전히 실행되는걸 보장할 수 없을 때, 옵셔널을 사용해 값이 없다는 것을 표시할 수 있지만

어떤 종류의 에러가 발생했는지 확인할 수 없습니다.

 

이럴때 에러 상황을 구분하여 개발자에게 알려준다면 프로그램 실행중 발생할 에러별로 적절하게 대응할 수 있을 겁니다.

NOTE

Swift에서 에러 처리는 Cocoa의 NSError 클래스와 상호 호환되는 에러 핸들링 패턴을 사용합니다.

에러의 표시와 발생  (Representing and Throwing Errors)

Swift에서 에러는 Error 프로토콜을 따르는 타입의 값으로 표현됩니다. 

 

Error 프로토콜을 들어가보면 비어있는데,

이 프로토콜을 따르는 타입이 에러 처리를 위해 사용될 수 있다는 것을 말합니다.

Swift의 열거형(enum)은 이런 관련된 에러를 그룹화하고 추가적인 정보를 제공하기에 적합합니다.

아래는 자판기 동작의 에러 상황을 다음과 같이 표현한 예시 입니다.

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinNeeded: Int)
    case outOfStock
}

 

에러를 발생시킴으로써 예상하지 못한 일이 발생하여 정상적인 실행 흐름을 계속할 수 없음을 나타낼 수 있습니다.

throw 키워드를 사용해서 에러를 던질 수 있습니다.

아래는 자판기에 5개의 추가코인이 필요하다는 에러를 발생 시키는 예시 입니다.

throw VendingMachineError.insufficientFunds(coinNeeded: 5)

에러 처리 (Handling Errors)

에러가 발생하면 특정 코드영역이 해당 에러를 처리하도록 해야합니다.

예를들어, 문제를 해결하거나, 우회할 수 있는 방법을 시도하거나, 사용자에게 실패 상황을 알리는 등의 에러 처리 방법이 될 수 도 있습니다.

 

Swift에서의 에러처리 방법 (4가지)

  • 에러가 발생한 함수에서 리턴값으로 에러를 반환하여 해당 함수를 호출한 코드에서 에러를 처리하도록 하는 방법
  • do-catch 구문을 사용하는 방법
  • 옵셔널 값을 반환하는 방법
  • assert를 사용해 강제로 크래쉬를 발생시키는 방법 ( 이 방법은 좋은것 같진 않다.. )
NOTE

Swift에서의 에러 처리는 다른언어의 예외 처리와 유사하게 do-catch와 throw키워드를 사용하지만,
다른 점은 스택 되감기(Stack Unwinding)와 관련이 없다는것 입니다. 
그렇기 때문에 에러를 반환하는 throw 구문은 일반적인 반환 구문인 return 구문과 비슷한 성능을 보여줍니다.

 

스택 되감기 설명

더보기

스택 되감기

C++ 예외처리에서 예외를 던지는 throw는 try구문 안에 있어야 하며, throw 발생 시 예외를 받아주는 catch구문으로 점프하게 됩니다.

함수 내에서 try, catch 구문이 없고 throw만 발생시키는 함수도 있는데, 이 경우 catch를 찾기 위해 함수 외부를 찾게 됩니다.

 

예외를 던진 함수의 외부에서 catch를 찾기 위해 현재의 스택정보를 정리하고 빠져나가는 스택 되감기(Stack Unwinding)가 발생합니다.

 

참고

 

에러를 발생시키는 함수 사용하기 (Propagating Errors Using Throwing Functions)

 

어떤 함수, 메소드 혹은 초기자가 에러를 발생 시킬 수 있다는 것을 알리기 위해서 throw 키워드를 함수 선언부 뒤에 붙일 수 있습니다. 

throw 키워드로 표시된 함수를 throwing function이라고 부릅니다. 

만약 함수가 리턴 값을 명시했다면 throw 키워드는 리턴 값 표시 기호인 -> 전에 적습니다.

func canThrowErrors() throws -> String {
    return ""
}

func cannotThrowErrors() -> String {
    return ""
}

throwing function은 함수 내부에서 에러를(throw) 만들어 함수가 호출된 곳에 전달합니다.

 

NOTE

오직 throwing function만 에러를 발생시킬 수 있습니다. 
만약 throwing function이 아닌 함수에서 throw가 발생한다면 반드시 해당 함수내에서 throw에 대해 처리되어야 합니다.

 

아래는 예시는

VendingMachine 클래스는 요청한 아이템이 이용가능하지 않거나, 재고가 없거나, 현재 계좌를 초과하는 비용이 발생했을 때를 구분해 VendingMachineError throw를 발생시키는 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(coinNeeded: item.price - coinsDeposited)
        }
        
        coinsDeposited -= item.price
        
        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        
        print("Dispensing \(name)")
    }
}

 

vend(itemNamed:) 메소드의 구현에서 guard 구문을 사용해 snack을 구매하는 과정에서 에러가 발생하면 함수에서 에러를 발생 시키고 함수를 탈출할 수 있도록 합니다. (early exit) 

 

vend(itemNamed:) 메소드는 에러를 발생(throw)시키기 때문에, 이 메소드를 호출하는 메소드엔 do-catch 구문, try?, try! 등을 사용해줘야합니다.

예를들어, 아래의 buyFavoriteSnack(person:vendingMachine:) 메소드 또한 throwing function이며, 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)
    }

 

이 예제에서는 buyFavoriteSnack(person:vendingMachine:) 함수가 주어진 사람의 가장 좋아하는 스낵이 뭔지 확인하고 vend(itemNamed:) 메소드를 호출해 구매 시도를 합니다. 

buyFavoriteSnack(person:vendingMachine:) 내부의 vend(itemNamed:) 메소드는 에러를 발생 시킬 수 있기 때문에 메소드 호출 앞에 try 키워드를 사용합니다. 

 

에러 발생 초기자는 throwing function과 같은 방법으로 에러를 발생 시킬 수 있습니다. 

아래 예제의 구조체 PurchasedSnack의 초기자(inti)는 초기화 단계의 일부분으로 에러를 발생시킬 수 있는 메소드 입니다.

그리고 초기자가 실행될 때 발생한 에러는 이 초기자를 호출한 곳에 전달 됩니다.

 

struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}

 

Do-Catch를 이용한 에러 처리 (Handling Error Using Do-Catch)

 

만약 에러가 do 구문 안에서 발생한다면 발생하는 에러의 종류를 catch 구문으로 구분해 처리할 수 있습니다. 

 

do-catch 구문 형태

do {
    try expression
    statements
} catch pattern 1 { 
    statements
} catch pattern 2 where condition {
    statements
} catch { // 나머지 error를 걸러내는곳
    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:) 메소드에서 에러가 발생하자마자 catch 구문에 전달해 적절한 처리를 할 수 있게 하기 위해 try 안에서 호출됩니다. 

만약 발생한 에러를 처리하는 catch 구문이 없다면 가장하단의 catch구문에서 지역 에러 상수인 error로 처리할 수 있습니다. 

아무런 에러도 발생하지 않는다면 do 구문이 실행됩니다. 

catch 구문에서 발생가능한 모든 에러에 대해 꼭 종류별로 처리할 필요는 없습니다. 

만약 에러를 처리하는 적절한 catch 구문이 없다면 그 코드에 둘러 쌓인 곳에 에러가 발생합니다.

에러가 발생한 곳에서 아무런 처리도 하지않으면 런타임에러가 발생하게 됩니다.

예를들어, 위에서 VendinMachineError에 대해 모든 에러를 처리하는것 대신 아래와같이 표현할 수 도 있습니다.

 

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 { // 나머지 모든 error를 이곳에서 처리
    print("Unexpected non-vending-machine-related error: \(error)")
}

 

nourish(with:) 메소드에서 만약 vend(itemNamed:)초기자에서 VendingMachineError 열거형 중 한가지의 에러가 발생한 경우, nourish(with:) 메소드가 에러를 처리해 메시지를 출력합니다. 

반면, nourish(with:) 메소드는 그것을 호출한 곳에 에러를 발생 시킵니다. 발생한 에러는 catch구문에 의해 처리 됩니다.

 

 

에러를 옵셔널 값으로 변환하기(Converting Errors to Optional Values)

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() 타입이 어떤타입이던옵셔널이 됩니다. 

여기선 옵셔널Int 타입이 되겠네요.

 

모든 에러를 같은 방법으로 처리하고자할 때 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!를 사용할 수 있습니다.

만약 에러가 발생할 경우엔 런타임 에러가 발생하게 됩니다. 

 

아래 코드는 loadImage(atPath:) 함수를 사용해 주어진 경로에 이미지가 무조건 있기 때문에 런타임에 아무 에러도 발생하지 않을거라는걸 확신할 수 있기 때문에 try!를 사용하는것이 적절합니다.

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

 


정리 작업 지정 (Specifying Cleanup Actions)

defer 구문을 이용해 현재 코드 블럭이 종료되기 직전 수행해야 할 일을 지정할 수 있습니다. 

defer 구문은 return, break 또는 예외로 인해 강제로 벗어나는 경우에도 동작 하며, 

열었던 파일이나 소켓을 닫거나, 수동으로 직접 할당한 메모리를 정리하는 등의 코드를 넣는데 사용됩니다.

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) 은 가장 마지막에 불려집니다.
    }
}

(defer는 에러처리 이외에 다른 구문에서도 사용할 수 있습니다.)

 

 

defer가 여러개 있을 경우엔 가장 마지막 줄부터 실행 됩니다. (역순으로)

아래는 defer 실행 순서 예제 입니다.

func test() {
    defer {
        print("defer 1")
    }
    
    defer {
        print("defer 2")
    }
    
    defer {
        print("defer 3")
    }
}

test()

//defer 3
//defer 2
//defer 1

 

728x90

'iOS' 카테고리의 다른 글

[Swift] 동시성 - Concurrency  (0) 2022.06.05
[iOS] Swift Memory - Copy On Write (COW)  (0) 2022.06.03
[iOS] 앱 업데이트  (0) 2022.05.29
[iOS] ViewController LifeCycle (생명주기)  (0) 2022.05.27
[iOS] Reactive Programming 이란?  (0) 2022.05.25