GGURUPiOS

Swift 공식 문서 정리 - (17) Error Handling 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (17) Error Handling

꾸럽 2023. 4. 28. 15:45

Error Handling (에러 처리)

프로그램 실행시 에러가 발상하면 그 상황에 대해 적절한 처리가 필요함

스위프트는 런타임에 에러가 발생한 경우 그것의 처리를 위해 에러의 발생(throwing), 감지(catching), 증식(propagating), 조작(manipulating)을 지원하는 일급 클래스를 제공함

어떤 명령은 항상 완전히 실행되는 것이 보장되지 않는 경우가 있음

그런 경우에 옵셔널을 사용해 에러가 발생해 값이 없다는 것을 표시할 수 있지만

어떤 종류의 에러가 발생했는지 확인할 수는 없음

이럴 때는 구제적으로 발생한 에러를 확인할 수 있어야 코드를 작성하는 사람이 적절한 처리가 가능함

에러의 표시와 발생

스위프트에서 에러는 Error 프로토콜을 따르는 타입의 값으로 표현됨.

비어있는 이 프로토콜은 프로토콜을 따르는 타입이 에러 처리를 위해 사용될 수 있다는 것을 가르킴

스위프트의 열거형은 특별히 이런 관련된 에러를 그룹화 하고 추가적인 정보를 제공하기에 적합함

아래 예제는 게임 안에서 판매기기 동작의 에러상황임

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

에러를 발생시키기 위해 throw 구문을 사용할 수 있음

예를들어, 다음 코드는 5개의 코인이 더필요하다는 에러를 발생시킴

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

에러 처리

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

문제를 해결 또는 우회 방법을 시도하거나 아니면 사용자에게 실패 상황을 알리는 것이 에러 처리 방법이 될 수 있음

스위프트에서는 4가지의 에러처리방법이 있음

  1. 에러가 발생한 함수에서 리턴값으로 에러를 반환해 해당 함수를 호출한 코드에서 에러를 처리하도록 하는 방법
  2. do-catch 구문을 사용하는 방법
  3. 옵셔널 값을 반환하는 방법
  4. assert를 사용해 강제로 크래쉬를 발생시키는 방법

에러를 발생시키는 함수 사용

어떤 함수, 메소드, 혹은 초기자가 에러를 발생 시킬 수 있다는 것을 알리기 위해

throw 키워드를 함수 선언부의 파라미터 뒤에 붙일 수 있음

이러한 함수를 throwing function 이라고 부름

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

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

// Throwing 함수만 오류를 전파할 수 있음. throwing 가 아닌 함수 내부에서 발생한 모든 오류는 함수 내부에서 처리해야함

아래 예에서 vend 메서드는 재고가 없가나 비용이 현재 예치된 금액을 초과하는 경우 오류를 발생시키는 메서드임

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

이 메서드의 안에서 guard 구문을 사용해 구매하는 과정에서 에러가 발생하면 함수에서 에러를 발생시키고 빠르게 함수를 탈출하도록 함.

vend 메서드는 에러를 발생시키기 때문에 이 메서드를 호출하는 메서드는 반드시 do-catch, 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)
		 // vend 메소드가 에러를 발생 시킬 수 있기 때문에 try 키워드 사용
}

// butFavoritSnack(...) 함수 또한 에러를 발생시키는 함수임
// vend 메소드에서 발생한 에러는 butFavoriteSnack 함수가 실행되는 곳까지 전해짐 

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

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

// 위의 에러 발생 초기자는 초기화 프로세스의 일부로 던지는 함수를 호출하고 발생하는 모든 오류를 호출자에게 전파

Do-Catch를 사용한 오류 처리

do-catch을 이용해 에러를 처리하는 코드 블럭을 작성할 수 있음

만약 에러가 do 구문 안에서 발생한다면 발생하는 에러의 종류를 catch 구문으로 처리

do {
    try expression // expression 이 에러를 발생 시킬 수 있기 때문에 try 키워드 작성
    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."

위의 예제에서 아무런 에러도 발생하지 않는다면 do 구문이 실행됨

catch 구문에서 발생가능한 모든 에러에대해 반드시 종류별로 처리를 할 필요는 없음

에러를 발생 시키지 않는 함수에서는 관련된 do-catch 구문에서 그 에러를 반드시 처리해야 하고,

에러를 발생 시키는 함수에서는 에러를 do-catch 구문에서 처리하거나 함수를 호출한 곳에서 에러를 처리해야 함

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 {
    print("Unexpected non-vending-machine-related error: \\(error)")
      // 여기에서 처럼 catch를 그냥 if-else에서 else 같이 사용 가능
}
// Prints "Invalid selection, out of stock, or not enough money."

에러를 옵셔널 값으로 변환

try? 구문을 사용해 에러를 옵셔널 값으로 변환할 수 있음

만약 에러가 try? 표현 내에서 발새한다면, 그 표현의 값은 nil 이 됨

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

let x = try? someThrowingFunction()

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

try? 는 발생하는 모든 에러를 같은 방법으로 처리하고 싶을 때 사용함

예를 들어, 다음코드는 데이터를 가져오는 여러접근 방법을 시도하는데 접근 방법이 모두 실패하면 nil 을 반환하는 코드임

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

에러 발생을 중지하기 (!)

함수나 메소드에서 에러가 발생되지 않을 것이라고 확신하는 경우 try!를 사용할 수 있음

하지만 만약 에러가 발생하면 런타임 에러가 발생하게 됨

정리 액션 기술

코드 실행이 현재 코드 블록을 떠나기 직전에 defer문을 실행함.

명령문 defer는 현재 범위가 종료될 때까지 실행을 연기함.

구문 밖으로 제어를 이전하거나 return 오류를 발생시키는 코드가 포함될 수 없음.

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

위 예제는 defer 구문을 이용한 예시