Error Handling Swift


Error Handling

에러 핸들링은 프로그램 내에서 에러 조건으로부터 되살리고 에러에 반응하기 위한 처리이다. 스위프트는 런타임에서 스로잉, 캐칭, 프로파게이팅, 그리고 메니퓰레이팅하는 최상의 지원을 제공한다.

몇몇 연산은 완전한 실행이나 유용한 출력을 만든다는 보장은되지 않는다. 옵셔널은 값 없음 상태에대해 사용할 수 있지만, 연산이 실패한 경우 무엇이 실패의 원인인지를 이해하는 것이 유용할 때가 있는데, 이를 통해 코드가 적절하게 반응할 수 있게 할 수 있다.

예를 들어, 디스크상의 파일로부터 데이터를 읽고 처리하는 작업을 예로들어보자. 이 작업이 실패할 수 있는 상황은 여러가지가 있는데, 지정된 경로에 파일이 없는 경우, 퍼미션이 없는 경우, 또는 연관된 포맷으로 파일이 인코드되어있지 않는 경우이다. 이런 다른 상황을 구별지음으로서 프로그램이 몇몇 에러를 얻고 커뮤니케이트할 수 있게 한다.

일러두기)
스위프트의 에러핸들링은 코코아나 오브젝티브C의 NSError 를 사용하는 에러핸들링과 교신한다. For more information about this class, see Handling Cocoa Errors in Swift.

에러 표현하고 던지기

스위프트에서, 에러는 형식의 값들로 표현되는데 Error 프로토콜을 따른다. 이 빈 프로토콜은 형식이 에러 핸들링을 위해 사용될 수 있음을 가리킨다.

Swift enumerations are particularly well suited to modeling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated. For example, here’s how you might represent the error conditions of operating a vending machine inside a game:

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

에러를 던지는 것은 기대하지 않던 상황이나 실행의 정상적인 흐름이 계속되지 못할 때 에러가 무엇인지 알 수 있게 한다. throw 문으로 에러를 던진다. 예를 들어, 다음 코드는 5개의 추가 코인이 필요함을 벤딩머신에게 가리키도록 에러를 던진다.

throw VendingMachineError.insuffientFunds(coinsNeeded:5)

에러 핸들링

에러가 던져졌을 때, 몇몇 코드 코드는 에러를 핸들링하기 위해ㅐ 반드시 반응해야 하는데, 예를들어, 문제를 맞게 하고, 대체적인 접근을 시도하거나, 실패를 사용자에게 알리는 것이다.

스위프트에서는 4가지의 에러핸들링하는 방법을 제공한다. 함수로 부터 함수를 호출하는 코드로 전이한다. do-catch 문을 사용해 에러를 다루거나, 옵셔널 값으로 에러를 다루거나, 에러가 발생하지 않게 어설션을 하거나이다. 각 접근은 아래 섹션에서 설명되어 있다.

함수가 에러를 던질 때 프로그램의 흐름을 변경하니 에러를 던지기 위해 코드의 위치를 빠르게 확인하는 것이 중요하다. try 키워드를 사용하여 (try?, try!) 코드의 부분이 에러를 던지는 함수, 메소드, 또는 이니셜라이져를 호출하기 전에 말이다. 이들 키워드는 아래 섹션들에서 설명된다.

일러두기)
스위프트의 에러 핸들링은 try, catch, throw 키워드를 사용하는 다른 언어들의 예외처리와 유사하다. 오브젝티브C를 포함한 다른 언어의 예외처리와 다른점은 계산적으로 비싼 콜스택, 프로세스를 언윈딩하는 것과 연관없다는 점이다. throw 문의 성능 성격은 return 문과 비교할수 있다.

던지기 함수를 사용한 에러 파생

그 함수, 메소드, 또는 이니셜라이져가 에러를 던질 수 있음을 나타내기 위해 이 파라미터 뒤의 함수 정의에 throws 키워드를 사용한다. throws 로 미크된 함수는 스로잉 함수라 불린다. 만약 함수가 반환형이 정의되면 -> 전에 throws 키워드를 사용한다.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

스로잉 함수는 에러를 파생하여 이 것이 호출된 범위로 던져진다.

일러두기)
오직 스로잉 함수만이 에러를 파생시킬 수 있다. 논스로잉 함수의 내부에서 던져진 에러는 반드시 함수 내에서 처리되어야 한다.

아래의 예시에서 VendingMachine 클래스는 vend(itemNamed:) 메소드를 가지며 만약 요청된 아이템이 불가하거나 없거나, 현 적립된 양을 초과하는 가격이 있다면 적절한 VendingMachineError 를 던진다.

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 문을 사용해 메소드 시작부분에서 주어진 조건에 충족되지 않으면 적절한 에러를 던지고 있다. throw 문이 프로그램 제어를 전달하므로 아이템은 오직 필요한 요구사항이 충족될 때만 벤드될 것이다.

vend(itemNamed:)메소드가 던지는 모든 에러를 파생하므로 이 메소드를 호출하는 모든 코드에서는 에러를 적절히 처리해야한다. do-catch문, try?, try! 를 사용하여 또는 계속 파생하도록 둔다. For example, the buyFavoriteSnack(person:vendingMachine:) in the example below is also a throwing function, and any errors that the vend(itemNamed:) method throws will propagate up to the point where the buyFavoriteSnack(person:vendingMachine:) function is called.

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 키워드를 이 앞에 지정한다.

이니셜라이져 던지기 또한 함수 던지기와 같은 방식으로 에러를 파생한다. 예를 들어, 아래 리스팅에 PurchasedSnack 의 이니셜라이져는 초기화 단계에서 함수를 던지기를 호출하며 발생하는 모든 에러를 처리를 호출자로 파생한다.

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

do-catch 를 사용해 에러다루기


You use a do-catch statement to handle errors by running a block of code. If an error is thrown by the code in the do clause, it is matched against the catch clauses to determine which one of them can handle the error.

Here is the general form of a do-catch statement:

  1. do {
  2. try expression
  3. statements
  4. } catch pattern 1 {
  5. statements
  6. } catch pattern 2 where condition {
  7. statements
  8. } catch {
  9. statements
  10. }
catch 뒤에 패턴을 적어 처리할 에러를 지정한다. If a catch clause doesn’t have a pattern, the clause matches any error and binds the error to a local constant named error. For more information about pattern matching, see Patterns.

예를 들어, 다음 코드는 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 "Insuffient funds. Please insert an additional 2 coins."

위의 예시에서, buyFavoriteSnack(person:vendingMachine:) 함수는 try 표현식에서 호출되어 에러를 던질 수 있다. 만약 에러가 던져지면, catch 문으로 전달되고 파생이 계속될 수 있는지 결정한다. 만약 어떤 패턴도 매치되지 않으면 최종 catch 문에 잡히고 지역 error 상수에 할당된다. 에러가 던져지지 ㅇ낳으면 do 문의 나머지 문이 실행된다.

catch 문은 do 에서 던져질 수 있는 모든 에러를 다루지 않아도 된다. 만약 어떤 catch 도 처리하지 못하면 에러는 감싸진 범위로 다시 파생된다. 그러나, 파생된 에러는 감싸는 어떤 스코프에서는 처리가 되긴 해야 한다. 만약 에러가 처리되지 않고 최상위 레벨까지 전달되면 런타임 에러가 된다.

For example, the above example can be written so any error that isn’t a VendingMachineError is instead caught by the calling function:

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 는 이 함수의 콜 범위로 전달한다. 에러는 일반 catch 에서 잡힌다

에러를 옵셔널 값으로 컨버팅하기

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 는 옵셔널 형식이다.Here the function returns an integer, so x and y are optional integers.

try? 는 같은 방식으로 모든 에러를 다루고자할 때 견고한 에러 핸들링 코드를 작성할 수 있게 한다. 예를 들어, 다음 코드는 데이터를 패치하는 몇몇 접근을 사용하거나 만약 접근이 실패하면 nil을 반환한다.

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

에러 파생 금지하기

간혹 실제로는 함수나 메소드가 런타임에 에러를 던지지 않을 것임을 안다. 이런 상황에서 try! 를 표현식 앞에 작성해 에러 파생을 금지시키고 실시간 어셜선내의호출을 감싸 에러가 던져지지 않게 한다. 만약 에러가 실제로 던져지면 런타임 에러가 발생한다.

예를 들어 다음 코드는 loadImage(atPath:) 함수를 사용하며 주어진 경로에서 이미지 리소스를 로드하거나 이미지가 로드되지 못한다면 에러를 던진다. 이 상황에서 이미지는 어플리케이션에 전달되고 실시간에서 에러를 던지지 않게 하여, 에러 파생을 막는 적절한 방법이다.

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

클린업 액션 지정하기

You use a defer statement to execute a set of statements just before code execution leaves the current block of code. This statement lets you do any necessary cleanup that should be performed regardless of how execution leaves the current block of code—whether it leaves because an error was thrown or because of a statement such as return or break. For example, you can use a defer statement to ensure that file descriptors are closed and manually allocated memory is freed.

A defer statement defers execution until the current scope is exited. This statement consists of the defer keyword and the statements to be executed later. The deferred statements may not contain any code that would transfer control out of the statements, such as a break or a return statement, or by throwing an error. Deferred actions are executed in the reverse of the order that they’re written in your source code. That is, the code in the first defer statement executes last, the code in the second defer statement executes second to last, and so on. The last defer statement in source code order executes first.

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

The above example uses a defer statement to ensure that the open(_:) function has a corresponding call to close(_:).

NOTE

You can use a defer statement even when no error handling code is involved.


덧글

댓글 입력 영역