[Swift] Error Handling

LeeEunJae·2023년 5월 1일
0

Swift

목록 보기
1/2

앱 개발 중 통신과 같이 에러가 발생할 수 있는 기능을 구현할 때, 에러 처리는 필수적으로 해야하는 작업 중 하나인데, Swift 에서는 Error Handling 을 어떻게 하는지 확실하게 짚고 넘어가려합니다.

Error Handling(에러 처리)

에러에 응답하고 프로그램을 복구하기 위해서 Error Handling을 합니다.

앱 사용중에 에러가 발생했는데 따로 에러 처리를 하지 않았다면, 그 앱은 런타임 오류가 발생하고 강제종료될 것 입니다.
따라서 좋은 앱을 만들기 위해서는 에러가 발생할 수 있는 구간에 에러 처리를 담당하는 코드를 작성하고, 에러에 따른 적절한 알림문구를 사용자에게 보여줘야 합니다.

에러 처리에 대해서 알아보기위한 예제로 자판기의 기능을 코드로 구현해보겠습니다.

발생가능한 Error

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

먼저 자판기를 사용하면서 발생할 수 있는 에러들을 Error 프로토콜을 준수하는 enum 열거형으로 정의합니다.

inValidSelection : 잘못된 입력(자판기에 없는 음식을 입력한 경우)
insufficientFunds(Int) : 코인 수가 부족한 경우
outOfStock : 재고가 없는 경우

예를 들어 사용자가 잘못된 입력을 한 경우 다음과 같이 에러를 발생시킵니다.

throw VendingMachineError.invalidSelection

Error Handling(에러 처리)

Swift에서는 에러를 처리하는 4가지 방법이 있습니다.
함수에서 해당 함수를 호출하는 코드로 에러를 전파하거나 do-catch 구문을 사용하거나 옵셔널 값으로 에러를 처리하거나 에러가 발생하지 않을 것이라고 주장할 수 있습니다.

함수에서 에러가 발생하면 프로그램의 흐름이 변경되므로 코드에서 에러가 발생할 수 있는 부분을 신속하게 알아야합니다.
코드에서 이러한 위치를 알려면 에러가 발생할 수 있는 함수, 메서드, 또는 초기화 구문 앞에 try, try?, try! 키워드를 작성해야 합니다.

던지기 함수를 이용한 에러 전파

에러가 발생할 수 있는 함수, 메서드, 또는 초기화 구문을 나타내기 위해 함수의 파라미터 뒤에 throws 키워드를 작성합니다.
throws 로 표시된 함수는 던지기 함수라고 합니다.
함수에 리턴 타입이 명시되어있으면 throws 를 반환 화살표(->) 전에 작성합니다.

func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

던지기 함수는 내부에서 발생한 에러를 호출된 범위로 전파합니다.

아래 예제에서 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 -= item.price
        
        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        
        print("우당탕탕탕(자판기에서 떨어지는 소리) => \(name)")
    }
}

vend(itemNamed:) 메서드는 guard 구문을 사용해서 매개변수로 들어온 항목이 inventory 에 존재하는지 확인하고, 해당 항목의 재고와 가격에 대한 에러를 처리한 후 모든 요구사항이 충족되면, 자판기에서 해당 항목이 나오도록 구현되었습니다.

vend(itemNamed:) 메서드는 발생하는 에러를 전파(throws)하기 때문에 이 메서드를 호출하는 코드는 do-catch 구문, try? 또는 try! 를 사용하여 에러를 처리하거나 계속 전파해야 합니다.

아래와 같이 자판기에서 간식을 구매하는 함수와 자판기에 코인을 넣는 함수를 작성해보겠습니다.

func buyFavoriteSnack(snack: String, vendingMachine: VendingMachine) throws {
    // vend(itemNamed) 메서드는 에러를 발생할 수 있으므로 try 키워드를 앞에 두어 호출됩니다.
    try vendingMachine.vend(itemNamed: snack)
}

func insertCoin(coin: Int, vendingMachine: VendingMachine) {
    vendingMachine.coinsDeposited += coin
}

buyFavoriteSnack(String, VendingMachine)함수는 vendingMachine.vend(itemNamed:) 메서드를 호출하기 때문에 앞에 try 키워드를 붙여서 에러를 계속 전파합니다.

에러를 전파하는 함수이기 때문에 throws 키워드를 붙여서 함수를 작성했습니다.

이제 우리는 아래와 같이 3이 입력되어 자판기 이용을 멈출 때까지 반복하는 프로그램을 작성할 수 있습니다.

do-catch 를 사용한 에러 처리

do-catch 구문을 사용하여 코드의 블럭을 실행하여 에러를 처리합니다.
에러가 do 절에서 발생되면 catch절에서 발생한 에러를 처리합니다.

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 10

while(true) {
    print("1. 자판기에 코인 넣기")
    print("2. 자판기에서 음식 뽑기")
    print("3. 끝내기")
    
    let choice = Int(readLine(strippingNewline: true)!)!

    switch choice {
    case 1:
        print("코인 몇개?")
        let coins = Int(readLine()!)!
        insertCoin(coin: coins, vendingMachine: vendingMachine)
    case 2:
        print("뭐 먹을래 ?")
        print("Candy Bar")
        print("Chips")
        print("Pretzels")
        let snackName = readLine()!
        do {
            try buyFavoriteSnack(snack: snackName, vendingMachine: vendingMachine)
            print("자판기에서 \(snackName)이 나왔어요. 맛있게 드세요")
        } catch {
            switch error {
            case VendingMachineError.invalidSelection:
                print("\(snackName)은 자판기에 없습니다.")
            case VendingMachineError.insufficientFunds(let coinsNeeded):
                print("코인이 부족합니다. \(coinsNeeded) 코인을 더 넣어주세요.\n현재 코인 : \(vendingMachine.coinsDeposited)")
            case VendingMachineError.outOfStock:
                print("\(snackName)의 재고가 없습니다")
            default:
                print("예상치 못한 오류입니다 : \(error).")
            }
        }
    case 3:
        break
    default:
        print("잘못된 입력입니다.")
    }
}

buyFavoriteSnack() 함수에서는 에러가 발생할 수 있기 때문에 try 키워드를 앞에 붙여줍니다.
에러가 발생하면 즉시 catch 절에 의해 포착되고, error 상수에 바인딩 됩니다.
에러가 발생하지 않으면 do 구문의 나머지 코드가 실행됩니다.

에러를 옵셔널 값으로 변환

에러를 옵셔널 값으로 변환하여 처리하기 위해 try? 를 사용합니다.
try? 표현식을 실행하는 동안 에러가 발생하면 이 표현식의 값은 nil 입니다.
이를 이용하여 위 코드에서 buyFavoriteSnack() 함수를 호출하는 부분을 아래와 같이 바꿀 수 있습니다.

if let result = try? buyFavoriteSnack(snack: snackName, vendingMachine: vendingMachine) {
	print("자판기에서 \(snackName)이 나왔어요. 맛있게 드세요")
} else {
	print("에러가 발생했습니다.")
}

try? 를 사용하면 위와 같이 모든 에러를 같은 방식으로 처리하려는 경우 간결한 에러 처리 코드를 작성할 수 있습니다.

에러 전파 비활성화

던지는 함수(throws 키워드가 작성된 함수) 또는 메서드가 실제로 런타임 에러를 전혀 발생시키지 않는다는 사실을 아는 경우에 에러 전파를 비활성화 하기 위해 try! 를 작성할 수 있습니다.
하지만 예상과는 반대로 에러가 발생하는 경우 런타임 오류가 발생하겠죠.

try! buyFavoriteSnack(snack: snackName, vendingMachine: vendingMachine)

위 함수는 에러가 발생할 여지가 충분합니다. vendingMachine의 inventory에 존재하지 않는 snackName이 들어간다면, 에러가 발생하고 런타임 오류가 발생합니다.
따라서 이런 경우에는 try! 키워드를 사용하기에는 부적절 합니다.

다음과 같이 에러가 발생하지 않는 것이 분명한 경우 try! 사용이 적절합니다.

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

해당 경로에 이미지가 존재해서 에러가 발생할 일이 없으므로 try! 키워드로 에러 전파를 비활성화하는 것이 적절합니다.

profile
매일 조금씩이라도 성장하자

0개의 댓글