[Swift5] Error Handling

26807 단어 swiftswift
  • 다음은 Swift 5.6 Doc의 Error Handling 공부 내용을 정리했음을 밝힙니다.

Error Handling

에러 핸들링은 프로그램 에러 조건을 조정하는 과정이다. 스위프트는 런타임 도중 복구 가능한 에러를 스로잉하고, 캐칭하고, 프로파게이팅하고, 조작할 수 있도록 지원한다.

특정 연산이 언제나 성공한다고 보장할 수 없을 때가 있다. 옵셔널을 통해 특정 값이 널 값인지 체크할 수 있다.

가령 디스크에 존재하는 파일에서 데이터를 읽고 쓰는 작업을 할 때 파일이 경로 상에 없거나 읽기 권한이 없거나 읽을 수 있는 포맷으로 인코딩되어 있지 않을 수 있다.

에러 표현 및 스로잉

스위프트에서 에러는 에러 프로토콜 중 어느 한 타입의 값으로 표현된다. 스위프트는 열거 타입을 통해 연관된 에러 조건을 그룹으로 만들어 사용하고 있다.

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

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

에러 스로잉을 통해 예상치 못한 일이 발생했고 실행 중 정상 흐름이 지속하지 못함을 알 수 있다. 즉 스로잉 문으로 에러를 던져버릴 수 있는 것이다.

에러 핸들링

에러 스로잉이 일어나면 스로잉 문을 둘러싼 코드가 에러 핸들링을 담당한다.

스위프트에서 에러를 핸들링하는 방법은 네 가지다.

  1. 특정 함수에서 에러가 일어났을 때 이 함수를 호출한 코드 블록으로 에러를 프로파게이트한다.
  2. do-catch 문으로 에러를 핸들링한다.
  3. 옵셔널 값으로 에러를 취급한다.
  4. 에러가 일어나지 않는다고 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하는 기능이다.

좋은 웹페이지 즐겨찾기