티스토리 뷰

반응형

이번 글에서는 Swift 공식 문서의 17번째 단원인 Error Handling을 읽고 정리한 글을 쓰려고 한다.

 

Apple Swift 공식 문서 17단원 - Error Handling

Error Handling

Error Handling(오류 처리)는 프로그램의 오류 조건에 응답하고 복구하는 프로세스이다. Swift는 런타임에 복구 가능한 오류를 throwing, catching, propagating, manipulating 하는 일급 클래스를 제공한다.

 

모든 작업이 항상 실행을 완료하거나 유용한 결과를 도출하는 것은 아니다. 작업이 실패했을 때 왜 실패했는지 원인을 이해하는 것이 중요한 경우가 많다. 예를 들어 디스크에 있는 파일에서 데이터를 읽고 처리하는 작업을 생각해보자. 이러한 작업에는 지정된 경로에 없거나 읽기 권한이 없다거나 호환 가능하지 않은 형식 등 다양한 실패 원인이 있을 수 있다. 이러한 원인을 구분하여 프로그램에서 처리할 수 있는 오류는 처리하고 처리할 수 없는 오류를 사용자에게 알릴 수 있다.


Representing and Throwing Errors

Swift에서 오류는 Error 프로토콜을 준수하는 타입 값으로 표현된다. Swift의 열거형은 오류의 원인들을 나누고 해당 오류들의 특성에 대한 추가 정보를 전달하는 모델을 만드는데 적합하다.

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

위의 코드와 같이 열거형에 Error 프로토콜을 채택할 수 있다. 위의 예는 게임 내에서 자동 판매기에 발생시킬 수 있는 오류 조건을 나타내는 방법이다. 오류가 발생하면 정상적인 실행을 계속할 수 없다는 것을 나타낸다. 이럴 때 throw 구문을 사용하여 오류를 발생시킬 수 있다.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

위의 코드의 throw를 사용하여 오류를 발생시킬 수 있다. 위의 예는 자동 판매기에 5개의 추가적인 코인이 필요하다는 오류를 발생시킨 것이다.


Handling Errors

그럼 이러한 오류들이 발생하면 어떻게 해야할까? 오류가 발생하면 해당 오류가 발생한 지점 근처의 코드가 오류를 처리해줘야 한다. 예를 들어 문제를 수정하거나 다른 방법을 시도하거나 사용자에게 오류를 알리는 것과 같은 방법으로 오류를 처리해야 한다.

 

Swift에서 오류를 처리하는 방법에는 네 가지가 있다. Propagate the error(오류를 전파), do-catch 구문 사용, 오류를 옵셔널 값으로 처리, 오류가 발생하지 않을 것이라고 정의하는 방법이 그 네 가지이다. 다음 섹션부터 해당 오류에 대해 하나씩 알아가 보자.

 

함수에서 오류가 발생하면 프로그램의 흐름이 변경되기 때문에 코드에서 오류가 발생할 수 있는 위치를 빠르게 식별할 수 있어야 한다. 코드에서 이러한 오류를 식별하려면 try, try?, try! 키워드를 사용하면 된다. 이 키워드에 대해서는 다음 섹션에서 알아보도록 하겠다.

 

Propagating Errors Using Throwing Functions

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

함수, 메서드, 생성자에서 오류가 발생할 수 있음을 나타내기 위해선 해당 매개변수 뒤에 throws 키워드를 써주면 된다. 

throws로 표시된 함수를 throwing 함수라고 하며 함수에 반환 값이 있다면 반환 값의 타입 앞에 throws를 써주면 된다. throwing 함수는 함수 내부에서 throw 되는 오류를 호출된 곳으로 전파한다. throwing 함수만 에러를 전파할 수 있다. 그러므로 throwing 함수가 아닌 함수에서는 오류를 내부에서 처리해야 한다.

 

struct Item {
    var price: Int
    var count: Int
}

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

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)")
    }
}

위의 코드에서 VendingMachine 클래스에서 발생할 수 있는 오류를 처리하기 위한 vend(itemNamed name:) 메서드가 있다. 해당 메서드에는 guard문과 throw문을 사용하여 오류를 처리한다. guard문의 코드가 오류를 발생하면 else로 넘어가 throw문에 의해 VendingMachineError 오류가 발생되고 메서드는 종료된다. 즉 세 개의 guard문에서 모두 오류가 발생하지 않아야 정상적으로 다음 코드가 수행될 수 있다. 이 메서드를 사용하면 오류가 발생할 수도 있기 때문에 do-catch, try, 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)
}

예를 들어 위의 코드의 buyFavoriteSnack(person: vendingMachine:) 함수에서 vendingMachine 인스턴스의 vend 메서드에서 오류가 발생한다면 이는 buyFavoriteSnack 함수의 내부에 전파된다. 따라서 이 함수에서 해당 메서드를 사용하려면 try 키워드를 함께 사용해줘야 한다. 그렇게 되면 오류 발생 시 buyFavoriteSnack 함수에서 오류가 전파된다.

 

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

Throwing 생성자는 함수에서 오류를 발생하는 것과 같은 방식으로 오류를 전파할 수 있다. 예를 들어 위의 코드에서 PurchasedSnack 구조체의 생성자는 초기화 프로세스의 일부로 throwing 함수를 호출하게 되는데 만약 오류가 발생한다면 해당 오류를 호출자에게 전파하여 처리할 수 있게 한다.

 

Handling Errors Using Do-Catch

오류가 발생하면 do-catch 문을 사용하여 처리할 수도 있다. do 절의 코드에서 오류가 발생하면 catch 절의 코드에서 오류를 처리하게 된다. 

do {
    try expression
    // statements
} catch pattern1 {
    // statements
} catch pattern2 where condition {
    // statements
} catch pattern3, pattern4 where condition {
    // statements
} catch {
    // statements
}

위의 형태로 do-catch 문을 사용하면 된다. do문에서 발생된 오류를 catch문에서 처리하게 되는데 pattern 부분은 오류의 종류를 나타낸다. 만약 pattern 부분을 따로 정의하지 않으면 error라는 지역 상수에 오류를 바인딩한다. 

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

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 함수는 아까 정의할 때 오류를 발생할 수 있는 함수로 정의했기 때문에 try와 함께 사용되어야 한다. 만약 오류가 발생하지 않는다면 그냥 실행되지만 오류가 발생하게 되면 어떤 오류가 발생했는지에 따라 catch 절이 선택된다. catch 절에서 do 절에서 발생 가능한 모든 오류를 처리할 필요는 없고 전파만 해도 된다. 하지만 결국 전파된 오류는 또 언젠가는 처리되어야 한다. 만약 오류가 처리되지 않고 최상위 범위로 전파되는 경우엔 런타임 오류가 발생하게 된다.

 

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

위의 코드에서의 함수는 함수 내에서 VendingMachineError를 처리할 수 있도록 정의되었다. 하지만 그 외의 오류는 처리하지 않고 그냥 전파만 하기 때문에 실제로 사용할 때는 do-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.")
    }
}

여러 개의 오류를 동일하게 처리하는 방법은 catch 뒤에 오류의 종류를 나타낼 때 쉼표로 구분하는 것이다. 위의 코드처럼 만들게 되면 세 가지 오류에 대하여 동일한 처리를 할 수 있다. 

Converting Errors to Optional Values

try?를 사용하여 오류를 옵셔널 값으로 변환하여 처리할 수 있다. try? 코드를 평가하는 동안 오류가 발생하게 되면 해당 코드의 값은 nil이 된다. 

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

예를 들어 위의 코드에서 x, y는 서로 같은 값과 동작을 갖는다. 즉 someThrowingFunction 함수에서 오류가 발생하면 x, y에는 값은 모두 nil이 되며 그렇지 않은 경우에는 함수가 반환한 값을 갖게 된다. 여기서 오류가 발생되지 않더라도 x, y는 옵셔널 타입의 Int이다.

 

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

try?를 사용하면 모든 오류를 동일한 방법으로 처리하고자 할 때 간결하게 코드를 작성할 수 있다. 위의 코드처럼 작성하게 되면 함수에서 발생하는 모든 오류를 동일한 방법으로 처리할 수 있고 모든 방식이 실패하면 nil을 반환하게 된다.

 

Disabling Error Propagation

만약 throwing 함수나 메서드가 런타임에 오류를 발생시키지 않을 것이라고 확신이 들면 try!를 사용하면 된다. 하지만 이 경우 오류가 발생하게 되면 런타임 오류를 발생한다. 

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

예를 들어 위의 코드는 지정된 경로의 이미지 리소스를 로드하는 코드이고 여기서 사용된 loadImage 함수는 오류를 발생할 수 있는 함수이다. 하지만 개발자가 절대로 오류가 발생하지 않을 것이라고 확신하면 위처럼 try!를 사용하면 된다.


Specifying Cleanup Actions

코드 실행이 현재 코드 블록을 떠나기 직전에 defer 문을 사용하여 특정 코드를 실행할 수 있다. 이 명령문을 실행하면 현재 코드 블록을 떠나는 방식에 관계없이 수행해야 하는 작업을 수행할 수 있다. 예를 들어 오류가 발생하여 종료하거나 return, break와 같은 명령문에 의해 종료되는 경우 defer 문을 사용하여 파일 디스크립터가 닫히고 메모리 할당을 수동으로 해제할 수 있다.

 

defer문은 현재 범위가 종료될 때까지 실행을 연기한다. 이 구문은 defer 키워드와 나중에 실행될 코드로 구성된다. 나중에 실행될 코드에는 break, return과 같은 제어를 전송하거나 오류를 발생시키는 코드를 작성할 수 없다. 지연된 작업은 소스 코드에서 작성된 순서와는 반대로 실행된다. 즉 첫 번째 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(_: ) 함수와 함께 호출되도록 도와준다. defer문은 오류 처리 코드를 포함하지 않아도 사용할 수 있다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함