반응형

[최종 수정일 : 2018.09.04]

원문 : 애플 개발 문서 Swift 4.2 Language Guide - Error Handling

오류 처리(Error Handling)

오류 처리(Error handling)은 프로그램에서 오류 상태를 복구(recovering)하고 응답(responding)하는 과정입니다. Swift는 실시간으로 오류를 복구하는 던지기(throwing), 잡기(catching), 전달하기(propagating), 조작하기(manipulation) 일류 클래스(first-class)를 지원합니다

일부 작업은 항상 실행이 완료되거나 유용한 결과물을 만들어 내지는 못합니다. 옵셔널은 값이 없는 것을 표현하기 위해 사용되지만, 까끔식 동작이 실패할때, 무엇때문에 실패했는지 인해하는데 유용하며, 그에 알맞는 코드로 대응할 수 있습니다.

예를들어, 디스크에서 파일 데이터를 읽고 처리하는 과정을 생각해 봅시다. 지정된 위치에서 파일이 존재하지 않는 파일을 포함하거나, 파일에 대한 권한이 없거나 파일이 호환되지 않는 포멧으로 인코딩 되어 있는 경우에 이 작업들은 실패할 수 있습니다. 이러한 서로 다른 상을 구분해서 프로그램의 일부 오류를 해결하고 해결할 수 없는 오류는 사용자에게 알려줍니다.

주의
Swift에서의 오류 처리는 Cocoa와 Objective-C 에서 NSError클래스를 사용한 오류처리와 호환됩니다. 이 클래스에 대한 자세한 정보는, Swift에서의 Cocoa 오류 처리하기)Handling Cocoa Errors in Swift)를 보세요.

오류를 표시하고 던지기(Representing and Throwing Errors)

Swift에서, 오류는 Error 프로퍼컬을 준수(conform)하는 타입의 값으로 표현됩니다. 빈 프로토콜은 오류처리에 대해 사용될 수 있는 타입임을 가리킵니다.

Swift 열거형은 특히 전달되야하는 오류의 특징(nature)에 관해서 추가적인 정보를 허용하는 연관된 값들로 관련된 오류 조건의 그룹을 모델링 하기에 적합합니다. 예를들어, 다음은 게임내의 자동 판매기(vending machine) 기능의 오류 조건을 나타내는 방법입니다.

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

예상치 않게 오류를 던지고 일반적인 실행을 계속 할수 없다는 것을 가리킵니다. 오류를 던지기 위해 throw 문을 사용합니다. 예를 들어, 다음에 오는 코드는 자동판매기에서 동전 5개가 필요하다는 것을 가리키기 위해 오류를 던집니다.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

오류 처리하기(Handling Errors)

오류를 던질때, 코드의 주변에서 오류를 처리해야 하는 책임이 있습니다.- 예를 들어, 문제를 해결하기, 다른 접근법 시도, 사용자에게 실패 정보를 알리기

Swift에서 오류처리하는 4가지 방법이 있습니다. 함수의 오류를 함수를 호출하는 코드로 전달 할 수 있으며, do-catch문을 사용하여 오류를 처리하며. 옵셔널 값으로 오류를 처리 하거나 오류가 발생하지 않도록 assert 사용합니다. 각 접근법은 아래 섹션에 설명되어 있습니다.

함수가 오류를 던질때, 프로그램의 흐름이 바뀌며, 코드에서 오류를 던질수 있는 곳에서 빨리 식별할 수있는 것이 중요합니다. 코드에서 이런 위치를 식별하기 위해, 오류를 던질수 있는 함수, 메소드 , 초기화 코드 앞에 try 키워드(try? 또는 try!도 가능) 를 작성합니다. 이 키워드는 아래 섹션에서 설명되어 있습니다.

주의
Swift에서 오류처리(Error handling)는 try, catch, throw 키워드를 사용하는 다른 언어의 예외처리(exception handling)와 유사합니다. 다른 언어의 예최 처리와 다른 점은 Swift에서의 오류 처리는 호출 스택(call stack)을 해제하는(unwinding) 것은 포함되지 않으며, 처리하는 계산 비용이 비쌉니다. 이와같이, throw문의 성능상 특징은 return문과 비교가능합니다.

던지는 함수를 사용하여 오류 전달하기(Propagating Errors Using Throwing Functions)

함수, 메소드, 초기화가 오류를 던질수 있는 것을 가리키기 위해, 함수 선언에서 매개변수 뒤에 throws 키워드를 작성합니다. throws로 표시된 함수를 던지는 함수(throwing function)라고 합니다. 함수가 반환 타입을 지정하는 경우, 반환 화살표(->) 앞에 throws 키워드를 작성합니다.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

던지는 함수(throwing function)는 호출된 범위 안쪽으로 전달 받은 오류를 던집니다.

주의
던지는 함수(throwing functions)만 오류를 전달 할수 있습니다. 모든 오류는 던지지않는 함수(nonthrowing function) 안쪽에서 던지며 그 함수 안에서 처리해야만 합니다.

아래 예제에서, VendingMachine 클래스는 요청한 항목이 품절이 되어 불가능하거나, 입금 액을 초과하는 경우에, 적절한 VendingMachineError를 던지는 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:) 메소드의 구현은 스낵(snack) 구입 요건 중 하나라도 충족되지 않은 경우에, 메소드를 일찍 탈출하거나 적절한 오류를 던지기 위해 guard 문을 사용합니다. throw 문은 프로그램 제어권을 즉시 편환해서 넘겨주기 때문에, 항목은 이러한 요구사항이 만족됬을때에만 판매 될것입니다.

vend(itemNamed:) 메소드는 던지는 모든 오류를 전달하기 때문에, 메소드를 호출하는 코드에서 오류(do-catch 문, try?try!)를 처리하거나 계속 전달해야 합니다. 예를 들어, 아래 예제에서 buyFavoriteSnack(person:vendingMachine:)는 던지는 함수(throwing function)이고, buyFavoriteSnack(person:vendingMachine:) 함수가 호출되는 곳에서 vend(itemNamed:) 메소드는 위로 전달할 것입니다.

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 initializers)는 던지기 함수(throwing functions)와 같은 방법으로 오류를 전달 할 수 있습니다. 예를 들어, 아래 목록에 있는 PurchasedSnack 구조체에 대한 초기화는 초기화 과정에서 던지는 함수(throwing function)를 호출하고, 호출자에게 전달하여 오류를 처리하도록 합니다.

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

Do-Catch 를 사용해서 오류처리하기(Handing Errors Using Do-Catch)

코드 블럭을 실행해서 오류를 처리하기 위해 do-catch 문을 사용합니다. do 절(clause) 안쪽 코드에 의해 오류가 던져지는 경우, 오류를 처리할 수 있는 것들 중 하나를 결정하기 위해 일치하는 catch절을 사용합니다.

다음은 do-catch 문의 일반적인 형식입니다.

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
} catch {
    statements
}

클로져가 오류를 처리할 수 있다는 것을 가리키기 위해 패턴 뒤에 catch를 작성합니다.. catch절과 일치하는 패턴이 없는 경우, 그 절(clause)은 모든 오류와 일치하고 그 오류를 지역 상수 error로 연결(binds)됩니다. 패턴 매칭(pattern matching)에 대한 자세한 내용은 패턴(Patterns)을 보세요.

예를들어, 다음에 오는 코드는 VendingMachinError 열거형의 모든 3가지 경우(case)와 일치합니다.

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 절에 의해 잡히고 지역 상수 error로 연결됩니다. 오류없이 던지는 경우, do문의 남은 구문들이 실행됩니다.

catch 절은 throw할수 있는 do절 안의 코드는 모든 오류 가능한 것들을 처리하지 않습니다. 오류를 처리하는 catch 절이 없는 경우, 오류는 주변 영역으로 전달됩니다. 하지만, 전달된 오류는 반드시 주변 범위에서 처리 됩니다. 던지지 않는(nonthrowing) 함수에서는, do-catch절 안에서 반드시 오류를 처리합니다. 던지는(throwing) 함수에서는, do-catch절 안이나 호출한 곳에서 반드시 오류를 처리합니다. 오류를 처리하는 곳 없이 최상위 단계 범위까지 전달하는 경우, 런타임 오류가 발생할 것입니다.

예를 들어, 위 예제의 모든 오류를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 열거형의 케이스(case) 중 하나로 오류를 던지는 경우, nourish(with:)는 메시지를 출력하여 오류를 처리합니다. 반면에, nourish(with:)는 오류를 호출한 곳으로 전달합니다. 오류가 발생하면 일반적인catch절로 잡습니다.

옵셔널 값으로 오류 변환하기(Converting Error 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() 반환한 옵셔널 타입 입니다. 여기에서 함수가 정수형으로 반환하므로, x와 y 는 옵셔널 정수형입니다.

동일한 방법으로 모든 오류를 처리하길 원할때 try?을 사용하여 간결하게 오류 처리하는 코드를 작성할 수 있습니다. 예를들어, 다음에 오는 코드는 몇가지 접근법을 사용하여 데이터를 가져오거나 모든 접근법이 실패한 경우에는 nil을 반환합니다.

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

오류 전달을 사용하지 않기(Disabling Error Propagation)

가끔씩 던지는 함수나 메소드를 모르는 경우에, 사실상 실행중(runtime)에 오류를 던집니다(throw). 이러한 경우에, 표현식 앞에 오류를 전달하지 않거나 실행중에 오류를 던지지 않도록 assertion의 호출을 감싸기 위해 try!을 작성할 수 있습니다.

예를들어, 다음에 오는 코드는 주어진 경로에서 이미지 리소스를 불러오거나 이미지를 불러오지 못하는 경우 오류를 던지는 loadImage(atPath:) 함수를 사용합니다. 이 경우에, 이미지는 앱과 함게 제공되기 때문에, 실행중에 오류를 던지지 않을것이며, 오류 전달하지 않는것이 적절합니다.

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

## 정리 작업 지정하기(Specifying Cleanup actions)

현재 코드의 블럭의 코드 실행이 끝나기 직전에 설정한 구문들을 실행하기 위해, defer문을 사용합니다. 이 구문은 사용하면 현재 코드의 블럭을 끝나는 방법과 관계없이 필요한 정리 작업을 할 수 있습니다. - 오류가 던저졌거나 return 또는 break문으로 끝나게 됩니다. 예를 들어, 파일 설명자가 종료하고 수동으로 할당된 메모리가 해제되는지 확인하기 위해, defer문을 사용할 수 있습니다.

defer 문은 현재 범위가 종료될때까지 실행을 미룹니다. 이 구문은 defer문과 나중에 실행될 구문으로 구성되어 있습니다. 미뤄진 구문들은 break나 return문, 오류 던지기(throwing) 처럼, 구문의 바깥으로 제어를 넘기는 어떤 코드도 포함할 수 없습니다. 미뤄진 동작은 소스코드로 작성된 역순으로 실행됩니다. 이것은, 첫번째 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.
    }
}

위 예제는 open(_:)함수가 close(_:)를 적절히 호출하는 것을 보증하는 defer문을 사용합니다.

주의
오류 처리가 없는 경우에도 defer문을 사용할 수 있습니다.


반응형

'Swift > Language Guide' 카테고리의 다른 글

Protocols  (0) 2018.09.18
Extensions  (0) 2018.09.18
Nested Types  (0) 2018.09.18
Type Casting  (0) 2018.09.18
Optional Chaining  (0) 2018.09.18
Deinitialization  (0) 2018.09.18
Initialization  (0) 2018.09.17
Inheritance  (0) 2018.08.30
Posted by 까칠코더
,