🔥 오류 처리하기
오류가 던져지면, 그 오류를 처리할 책임이 있는 코드가 있어야 합니다. 예를 들어, 문제를 고치거나, 다른 방법을 시도하거나, 사용자에게 실패를 알리는 식으로요.
Swift에서 오류를 처리하는 방법에는 네 가지가 있습니다.
- 함수에서 오류를 그 함수를 호출한 코드로 전파하기
do``catch
문을 사용해 오류 처리하기- 옵셔널 값으로 오류 처리하기
- 오류가 발생하지 않을 것이라고 단언하기
오류를 던지는 함수는 프로그램의 흐름을 바꾸기 때문에, 코드 중 어디에서 오류를 던질 수 있는지 빨리 파악하는 것이 중요합니다. 이를 위해 오류를 던질 수 있는 함수, 메서드, 이니셜라이저를 호출하는 코드 앞에 try
키워드 (또는 try?
, try!
변형)를 씁니다.
던지는 함수를 사용해 오류 전파하기
함수, 메서드, 이니셜라이저가 오류를 던질 수 있음을 나타내려면, 함수 선언부에서 매개변수 뒤에 throws
키워드를 씁니다. throws
로 표시된 함수를 throwing function
이라고 합니다. 함수가 반환 타입을 지정한 경우, throws
키워드를 반환 화살표(->
) 앞에 씁니다.
func canThrowErrors() throws -> String func cannotThrowErrors() -> String
swift
throwing function은 그 안에서 던져진 오류를 자신이 호출된 영역으로 전파합니다.
아래 예제에서, 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)") } }
swift
vend(itemNamed:)
메서드의 구현은 guard
문을 사용해 스낵 구매 요건 중 하나라도 충족되지 않으면 일찍 메서드를 종료하고 적절한 오류를 던집니다. throw
문은 즉시 프로그램 제어를 전달하므로, 모든 요건이 충족될 때만 항목이 판매됩니다.
vend(itemNamed:)
메서드는 자신이 던지는 모든 오류를 전파하므로, 이 메서드를 호출하는 모든 코드는 do
-catch
문, try?
, try!
를 사용해 오류를 처리하거나 계속 전파해야 합니다. 예를 들어, 아래 예제의 buyFavoriteSnack(person:vendingMachine:)
도 throwing function이며, vend(itemNamed:)
메서드가 던지는 모든 오류는 buyFavoriteSnack(person:vendingMachine:)
함수가 호출되는 지점까지 전파됩니다.
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) }
swift
이 예제에서, buyFavoriteSnack(person: vendingMachine:)
함수는 주어진 사람이 좋아하는 스낵을 찾아 vend(itemNamed:)
메서드를 호출하여 그들을 위해 구매하려고 합니다. vend(itemNamed:)
메서드가 오류를 던질 수 있으므로, 앞에 try
키워드를 붙여 호출합니다.
throwing 이니셜라이저도 throwing 함수와 같은 방식으로 오류를 전파할 수 있습니다. 예를 들어, 아래 코드에서 PurchasedSnack
구조체의 이니셜라이저는 초기화 과정의 일부로 throwing 함수를 호출하고, 만난 어떤 오류든 호출자에게 전파하여 처리합니다.
struct PurchasedSnack { let name: String init(name: String, vendingMachine: VendingMachine) throws { try vendingMachine.vend(itemNamed: name) self.name = name } }
swift
Do-Catch를 사용해 오류 처리하기
do
-catch
문을 사용하면 코드 블록을 실행하여 오류를 처리할 수 있습니다. do
절의 코드에서 오류가 던져지면, 어떤 catch
절이 그 오류를 처리할 수 있는지 결정하기 위해 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#> }
swift
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).") } // "Insufficient funds. Please insert an additional 2 coins." 출력
swift
위 예제에서, buyFavoriteSnack(person:vendingMachine:)
함수는 오류를 던질 수 있기 때문에 try
표현식에서 호출됩니다. 오류가 던져지면, 실행은 즉시 catch
절로 옮겨가고, 전파를 계속할지 결정합니다. 매칭되는 패턴이 없다면, 오류는 마지막 catch
절에 잡혀 로컬 error
상수에 바인딩됩니다. 오류가 던져지지 않으면, do
문의 남은 문장들이 실행됩니다.
catch
절이 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("Couldn't buy that from the vending machine.") } } do { try nourish(with: "Beet-Flavored Chips") } catch { print("Unexpected non-vending-machine-related error: \(error)") } // "Couldn't buy that from the vending machine." 출력
swift
nourish(with:)
함수에서, vend(itemNamed:)
가 VendingMachineError
열거형의 케이스 중 하나인 오류를 던지면, nourish(with:)
는 메시지를 출력하여 오류를 처리합니다. 그렇지 않으면, nourish(with:)
는 오류를 호출 지점으로 전파합니다. 그러면 오류는 일반 catch
절에 의해 잡힙니다.
여러 관련 오류를 잡는 또 다른 방법은 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.") } }
swift
eat(item:)
함수는 잡을 자판기 오류를 나열하고, 오류 텍스트는 그 목록의 항목에 해당합니다. 세 개의 나열된 오류 중 어떤 것이라도 던져지면, 이 catch
절이 메시지를 출력하여 처리합니다. 다른 오류는 나중에 추가될 수 있는 자판기 오류를 포함하여 주변 범위로 전파됩니다.
오류를 옵셔널 값으로 변환하기
try?
를 사용하면 오류를 옵셔널 값으로 변환하여 처리할 수 있습니다. try?
표현식을 평가하는 동안 오류가 던져지면, 표현식의 값은 nil
이 됩니다. 예를 들어, 다음 코드에서 x
와 y
는 같은 값과 동작을 가집니다:
func someThrowingFunction() throws -> Int { // ... } let x = try? someThrowingFunction() let y: Int? do { y = try someThrowingFunction() } catch { y = nil }
swift
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 }
swift
오류 전파 비활성화하기
때로는 던지는 함수나 메서드가 실제로는 런타임에 오류를 던지지 않을 것임을 알고 있을 때가 있습니다. 그런 경우, 표현식 앞에 try!
를 써서 오류 전파를 비활성화하고 오류가 던져지지 않을 것이라는 런타임 어설션으로 호출을 감쌀 수 있습니다. 실제로 오류가 던져지면, 런타임 오류를 얻게 됩니다.
예를 들어, 다음 코드는 주어진 경로에 이미지 리소스를 로드하거나 이미지를 로드할 수 없으면 오류를 던지는 loadImage(atPath:)
함수를 사용합니다. 이 경우, 이미지가 애플리케이션과 함께 제공되므로, 런타임에 오류가 던져지지 않을 것이므로 오류 전파를 비활성화하는 것이 적절합니다.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
swift
이렇게 try!
를 사용하면 오류 전파 로직을 제거하여 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다. 하지만 try!
는 신중하게 사용해야 합니다. 오류가 실제로 던져질 가능성이 있다면, 오류 전파를 비활성화하는 것은 잠재적인 버그와 크래시의 원인이 될 수 있기 때문이죠.
이로써 오류를 처리하는 네 가지 주요 방법에 대해 살펴보았습니다:
- 오류를 전파하기
do``catch
로 처리하기- 옵셔널로 변환하기
- 오류 전파 비활성화하기
실제 개발에서는 상황에 따라 적절한 방법을 선택하는 것이 중요합니다. 오류 처리는 강건하고 오류에 대응할 수 있는 코드를 작성하는 데 필수적이에요. 오류 처리를 잘 활용하면 사용자 경험을 향상시키고 애플리케이션의 안정성을 높일 수 있습니다.
다음 섹션에서는 오류를 정의하고 사용하는 더 발전된 기술에 대해 알아보겠습니다. 커스텀 오류 타입을 정의하는 방법, defer
문을 사용하여 정리 코드를 작성하는 방법 등 유용한 주제가 남아 있으니 계속해서 살펴봅시다!