소소한개발팁
article thumbnail
반응형

Error Handling 

- Swift에서는 런타임에 에러가 발생한 경우 그것의 처리를 위해 에러의 발생(throwing), 감지(catching), 증식(propagating), 조작(manipulating)을 지원하는 일급 클래스(first-class)를 제공합니다.

 

- 어떤 명령은 항상 완전히 실행되는 것이 보장되지 않는 경우가 있습니다. 그런 경우에 옵셔널을 사용해 에러가 발생해 값이 없다는 것을 표시할 수 있지만, 어떤 종류의 에러가 발생했는지 확인할 수는 없습니다. 이럴 때는 구제적으로 발생한 에러를 확인할 수 있어야 코드를 작성하는 사람이 각 에러의 경우에 따른 적절한 처리를 할 수 있습니다.

 

- 예를 들어, 디스크에서 파일을 읽어 데이터를 처리하는 일을 한다고 할 때 이 작업이 실패할 경우의 수는 여러 가지가 존재합니다. 파일이 특정 경로에 존재하지 않거나, 읽기 권한이 없거나 혹은 파일의 데이터가 식별할 수 있는 포맷으로 적절히 인코딩 되지 않은 경우 등 말이죠. 이런 종류별 에러 상황을 식별해 사용자에게 제공해 주면 프로그램 실행 중 발생항 각 에러 별로 사용자가 적절히 대응할 수 있도록 도울 수 있습니다.

 

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

 

 

Representing and Throwing Errors 

- Swift에서 에러는 Error 프로토콜을 따르는 타입의 값으로 표현됩니다. 비어있는(Empty) 이 프로토콜은 프로토콜을 따르는 타입이 에러 처리를 위해 사용될 수 있다는 것을 가리킵니다.

 

- Swift의 열거형은 특별히 이런 관련된 에러를 그룹화(Grouping)하고 추가적인 정보를 제공하기에 적합합니다. 예를 들어, 게임 안에서 판매기기 동작의 에러 상황을 다음과 같이 표현할 수 있습니다.

 

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

 

- 에러를 발생시킴으로써 무언가 기대하지 않았던 동작이 발생했고 작업을 계속 수행할 수 없다는 것을 알려줄 수 있습니다. 에러를 발생시키기 위해 throw 구문을 사용할 수 있습니다. 예를 들어, 다음 코드는 판매 기기에서 5개의 코인이 더 필요하다는 에러를 발생시킵니다.

 

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

 

 

Handling Errors 

Swift에서 4가지 에러를 처리 방법이 있습니다.

 

1. 에러가 발생한 함수에서 리턴 값으로 에러를 반환해 해당 함수를 호출한 코드에서 에러를 처리하도록 하는 방법

 

2. do-catch 구문을 사용하는 방법

 

3. 옵셔널 값을 반환하는 방법

 

4. assert를 사용해 강제로 크래쉬를 발생시키는 방법입니다. 

 

 

Propagating Errors Using Throwing Fuctions

- 어떤 함수, 메서드 혹은 초기자가 에러를 발생시킬 수 있다는 것을 알리기 위해서 throw 키워드를 함수 선언부의 파라미터 뒤에 붙일 수 있습니다. throw 키워드로 표시된 함수를 throwing function이라고 부릅니다. 만약 함수가 리턴 값을 명시했다면 throw 키워드는 리턴 값 표시 기호인 -> 전에 적습니다

 

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

 

- throwing function은 함수 내부에서 에러를(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(coinsNeeded: 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:) 메서드는 에러를 발생시키기 때문에 이 메서드를 호출하는 메서드는 반드시 do-catch, try?, try! 등의 구문을 사용해 에러를 처리해야 합니다. 예를 들어, 아래 예제의 buyFavoriteSnack(person:vendingMachine:) 또한 에러를 발생시키는 함수인데 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:) 메서드를 호출해 구매 시도를 합니다. 왜냐하면 vend(itemNamed:) 메서드는 에러를 발생시킬 수 있기 때문에 메서드 호출 앞에 try 키워드를 사용합니다. 에러 발생 초기자는 throwing function과 같은 방법으로 에러를 발생시킬 수 있습니다. 예를 들어, 아래 예제의 PurchasedSnack 구조체의 초기자는 초기화 단계의 일부분으로써 에러를 발생시킬 수 있는 함수입니다. 그리고 초기자가 실행될 때 발생한 에러는 이 초기자를 호출한 곳에 전달됩니다.

 

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

 

 

 Handling Error Using Do-Catch 

- 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."

 

- 위 코드처럼 모든 VendingMachineError에 대해 기술하는 것 대신 다음과 같이 처리할 수 있습니다.

 

func nourish(with item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch is VendingMachineError {    
        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)")
     
}
// Prints "Invalid selection, out of stock, or not enough money."

 

- 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()의 타입이 어떤 것이든 상관없이 옵셔널이 됩니다. 이 함수에서는 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
}

 

 

 Disabling Error Propagation 

- 함수나 메서드에서 에러가 발생되지 않을 것이라고 확신하는 경우 try! 를 사용할 수 있습니다. 혹은 runtime assertion을 사용해 에러가 발생하지 않도록 할 수 있습니다. 하지만 만약 에러가 발생하면 런타임 에러가 발생하게 됩니다. 예를 들어, 다음 코드는 loadImage(atPath:) 함수를 사용해 주어진 경로에서 이미지 리소스를 불러오거나 이미지를 불러오는데 실패한 경우 에러를 발생시킵니다. 이 경우에는 앱이 배포될 때 이미지가 포함돼 배포되기 때문에 런타임에는 아무 에러도 발생되지 않을 것이라 확신할 수 있어 try! 를 사용하는 것이 적절합니다. 

 

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

 

 

Specifying Cleanup Actions

- 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.
    }
}

 

 

내용은 https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html를 보면서 작성하였고 원문으로 작성된 내용을 옮기다 보니 이상한 부분이 있을 수 있습니다. 자세한 내용은 위의 링크를 확인해주시기 바랍니다.

반응형
profile

소소한개발팁

@개발자 뱅

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!