본문 바로가기
Swift

Error Handling(에러 처리)에 관하여

by iOS 개린이 2023. 3. 16.

Error Handling(에러 처리)

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

적절한 처리를 위해서 Swift에서는 에러의 발생(throwing), 감지(catching), 증식(propagating), 조작(manipulating)을 지원하는 일급 클래스를 제공한다.

 

 

 

Error 표현

-Swift에서 에러는 Error 프로토콜을 준수하는 타입의 값으로 표현된다.

Error 프로토콜은 요구사항이 따로 없는 빈 프로토콜이지만, 에러 표현을 위한 타입은 이 프로토콜을 채택한다.

 

-Swift의 열거형은 Error를 그룹화 하고, 연관값을 통해 추가적인 정보를 제공하기에 적합하다. 

 

ex) 게임 안에서 자판기를 작동시키려 할 때 발생할 수 있는 에러 상황

enum VendingMachineError : Error {
    case invalidSelection                    //유효하지 않은 선택
    case insufficientFunds(coinsNeeded: Int) //코인 부족
    case outOfStock                          //품절
}

//에러를 발생시키기 위한 throw 구문이용. (판매 기기에서 5개의 코인이 더 필요하다는 에러)
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

 

열거형 VendingMachineError는 Error 프로토콜을 채택하고 있어 에러 표현을 위한 타입이라는 것을 알 수 있다.

VendinMachine의 에러 종류에는 3가지가 있고, 에러가 발생하면 해당되는 에러를 throw를 통해 던져준다.

 

 

Error 처리

-throw를 통해 던져진 에러를 처리하는 부분이다.

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

예를 들어 던져진 에러가 무엇인지 판단하여 다시 문제를 해결한다든지, 다른 방법으로 문제 해결을 시도해 본다든지, 에러를 알리고 사용자에게 선택 권한을 넘겨 다음으로 어떤 동작을 하게 할 것인지 결정하도록 유도하는 등의 코드를 작성하여 처리한다.

 

Swift에서는 에러를 처리하기 위한 4가지 방법이 있다.

1. 에러가 발생한 함수에서 리턴값으로 에러를 반환해 해당 함수를 호출한 코드에서 에러를 처리하는 방법

2. do-catch 구문을 사용하는 방법

3. 옵셔널 값을 반환하는 방법

4. assert를 사용해 강제로 크래쉬를 발생시키는 방법

 

tip

Swift에서 에러처리는 다른 언어의 exception 처리와 닮았다. try - catch와 throw 키워드를 사용한다. 

Swift의 에러처리가 다른 언어의 exception과 다른 점은 많은 계산이 필요할 수 있는 콜스택 되돌리기와 관련이 없다는 것이다. 그렇기 때문에 에러를 반환하는 throw 구문은 일반적인 반환 구문인 return 과 비슷한 성능을 보여준다.

 

 

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

-함수에서 발생한 에러를 함수를 호출한 코드에 알리는 방법이다.

-에러를 발생시킬 수 있다는 것을 알려주기 위해 throws 키워드를 함수의 매개변수 뒤에 붙여준다. (리턴 값이 있다면 리턴 값 표시 기호 -> 전에 작성한다.)

throw 키워드로 표시한 함수를 throwing function이라고 부름.

 

func canThrowError() throws -> String

 

이렇게 throwing function을 호출했을 때, 동작 도중 에러가 발생하면 자신을 호출한 코드에 에러를 던져서 에러를 알릴 수 있다.

 

예제

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 메소드는 에러를 발생시킬 수 있는 throwing function이다.

guard문을 통해 구매하는 과정에서 에러가 발생하면, 발생한 에러를 throw 해주고 함수가 종료된다.

vend 메소드는 에러를 발생시킬 수 있기 때문에 이를 호출하는 메소드는 반드시 do-catch, try? try! 등의 구문을 이용해서 에러를 처리하는 코드를 작성해주어야 한다. 

 

let favoriteSnacks = ["Jiho" : "Chips",
                      "Bob" : "Biscuit",
                      "Chulsu" : "Chocolate",]

func buyFavoriteSnack(person : String, vendingMachine : VendingMachine) throws {
    
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)
}

 

각각 좋아하는 스낵이 어떤 것인지 확인하고, vend 메서드를 통해 구매를 시도한다.

vend 메소드는 에러를 발생시킬 수 있기 때문에 이를 호출하는 buyFavoriteSnack 함수도 throws 키워드를 통해 에러를 던질 수 있는 함수라고 선언했고, try를 통해 vend 메소드의 호출을 시도한다.

 

또한 초기화 메서드인 init도 throwing function과 같은 방법으로 에러를 발생시킬 수 있다.

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

 

init 메서드가 실행될 때 발생한 에러는 호출한 곳에 전달된다.

이렇게 함수에 throws를 사용하면, 함수의 동작 도중 에러가 발생했을 때, 호출한 곳에 에러를 전달할 수 있다.

 

근데 위의 예제들은 모두 try로 메소드를 호출하기만 하고, 에러를 처리해주는 코드를 작성하지 않았다.

이제 던져진 에러를 처리하는 방법에 대해 알아보자.

 

 

do-catch 구문을 이용한 에러 처리

-do-catch를 이용해서 에러를 처리하는 코드를 작성할 수 있다.

-함수, 메서드, 이니셜라이저 등에서 에러를 던져주면 전달받은 쪽에서는

do 절 내부 코드에 오류를 던지고, catch 절 내부 코드에서 오류를 받아 적절히 처리하는 식이다.

 

do-catch 문 예시

do {
    try 에러가 발생할 수 있는 코드
    
    에러가 발생하지 않았을 때, 실행할 코드
    
} catch 에러 패턴 1 {
    처리 코드
} catch 에러 패턴 2 where 추가 조건 {
   처리 코드
} catch {
   처리 코드
}

 

catch 키워드 뒤에는 처리할 에러 종류를 작성한다.

만약 에러 종류를 명시하지 않았을 경우에는 모든 에러 내용가 블록 내부에 암시적으로 error 라는 이름의 지역 상수로 바인딩 된다. 

 

 

do-catch를 사용한 예제

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8

do {
    try buyFavoriteSnack(person: "Jiho", vendingMachine: vendingMachine)
    
    print("과자구매 성공")
    
} catch VendingMachineError.invalidSelection {
    print("유효하지 않은 선택")
    
} catch VendingMachineError.outOfStock {
    print("재고 없음.")
    
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("동전 부족 - \(coinsNeeded)개의 동전이 부족합니다.")
    
} catch {
    print("그 외의 오류 발생 : \(error)")
}

//동전 부족 - 2개의 동전이 부족합니다.

 

buyFavoriteSnack(person:, vendingMachine) 함수는 에러를 발생시킬 수 있는 함수이기 때문에 try를 통해 호출했다.

buyFavoriteSnack() 함수에서 에러가 발생한다면, 발생한 에러는 일치하는 catch 문으로 들어가서 에러를 적절하게 처리할 수 있다. 만약 catch문과 일치하는 에러가 없다면 맨 마지막 catch문에 들어가서 지역상수 error를 통해 처리할 수 있다.

아예 에러가 발생하지 않으면 do 구문에 있는 "과자구매 성공" 이 실행된다.

 

 

옵셔널 값으로 에러 처리

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

try? 를 통해 동작하던 코드가 에러를 발생시키면 그 코드의 반환값이 nil이 된다.

 

func someThrowingFunction(shouldThrowError : Bool) throws -> Int {
    
    if shouldThrowError {
        
        enum SomeError : Error {
            case justSomeError
        }
        
        throw SomeError.justSomeError
    }
    
    return 100
}

let x = try? someThrowingFunction(shouldThrowError: true)
print(x) //nil

let y = try? someThrowingFunction(shouldThrowError: false)
print(y) //Optional(100)

 

try? 를 통해 호출한 someThrowingFunction() 함수가 에러를 발생시키면 nil을 반환하고,

에러를 발생하지 않으면 반환값이 옵셔널 값으로 반환된다. 

someThrowingFunction() 의 반환값은 Int지만 try? 를 사용하면 반환타입이 옵셔널 타입으로 변경된다.

 

또한 try? 는 발생하는 모든 에러를 같은 방법으로 처리하고 싶을 때 유용하다.

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

 

data를 Disk에서 찾아보고 없다면 Server에서도 찾아보고 그래도 없다면 nil을 반환한다.

이렇게 do-catch문 없이도 옵셔널을 이용해서 에러를 처리해줄 수 있다.

 

 

 

 

에러 발생을 중지하기(에러 처리의 마지막 방법)

-에러가 발생하지 않을 것이라고 확신할 때 try!를 사용할 수 있다.

혹은 runtime assertion을 사용해서 에러가 발생하지 않도록 할 수도 있다.

에러가 발생하지 않을 것이라는 확신을 했음에도 불구하고 에러가 발생했을 때

try!는 다른 표현에  !를 붙이는 것(강제 타입캐스팅, 강제 언래핑)과 마찬가지로 런타임 에러를 발생시킨다.

 

let photo = try! loadImage(atPath : "지호 프로필.jpg")

 

loadImage() 함수를 통해 경로에 맞게 이미지를 불러오는 코드이다.

try!를 사용해서 만약 이미지를 불러오는데 실패한다면 런타임 에러를 발생시킨다.

하지만 이 경우에는 앱이 배포될 때, 이미지를 포함시켜 배포하기 때문에 아무 에러가 발생하지 않을 것이라는 확신이 있다고 한다.

이렇게 에러가 발생하지 않는다고 확신할 때 try! 를 사용하자. (웬만하면 안쓰는 것이 좋겠죠?)

 

 

rethrows

-rethrows 키워드를 사용한 함수는 throwing function을 매개변수로 가진다.

-rethrow 함수는 최소 하나 이상의 throwing function 매개변수가 있어야 한다.

 

func someThrowingFuction() throws {
    
    enum SomeError : Error {
        case justSomeError
    }
    
    throw SomeError.justSomeError
}

func rethrowingFuntion(callback : () throws -> Void) rethrows {
    try callback() //오류를 다시 던질 뿐 따로 처리하지 않는다.
}

do {
    try rethrowingFuntion(callback: someThrowingFuction)
    
}catch {
    print(error)
}

//justSomeError

 

rethrowingFunction은 rethrows 키워드를 통해 에러를 발생시킬 수 있는 someThrowingFunction 함수를 매개변수로 받고 있다고 알려준다.

다음 do-catch문에서는 someThrowingFunction에서 발생한 에러를 rethrow 해주고 catch문에서 에러를 처리한다.

 

 

rethrow 함수는 기본적으로 스스로 에러를 던지지 못한다. 따라서 함수 내부에 throw 구문을 사용하지 못함.

하지만 catch 절 내부에서는 throw 구문을 사용할 수 있다.

do-catch문 내에서 에러를 발생시키는 함수를 호출하고, catch에서 다른 에러를 던짐으로써 에러를 던지는 함수에서 발생한 에러를 제어할 수 있다.

 

func someThrowingFuction() throws {
    
    enum SomeError : Error {
        case justSomeError
    }
    
    throw SomeError.justSomeError
}

func rethrowingFuntion(callback : () throws -> Void) rethrows {
    enum AnotherError : Error {
        case justAnotherError
    }
    
    do {
        try callback()
        
    }catch {
        throw AnotherError.justAnotherError
    }
    
    //매개변수로 들어온 함수만 catch절에서 처리해줄 수 있음.
    do {
        try someThrowingFuction()
        
    }catch {
        throw AnotherError.justAnotherError //에러발생
    }
    
    //단독으로 에러를 던질 수도 없음.
    throw AnotherError.justAnotherError //에러발생
}

 

위의 코드로 rethrow 함수의 특징 3가지를 알 수 있다.

1. rethrow 함수의 내부에서 throw를 사용할 수 없지만, catch문 안에서는 사용이 가능하다.

2. 에러를 발생하는 함수를 호출하고, catch문에서 다른 에러를 던짐으로써 에러 처리가 가능한 것을 볼 수 있다.

3. 매개변수로 들어온 함수만 catch절에서 처리해줄 수 있다.

 

 

rethrow 함수와 클래스 

 

-슈퍼 클래스에서 rethrow 메서드는 서브  클래스에서 throw 메서드로 오버라이딩이 불 가능하다.

하지만 슈퍼 클래스에서 throw 메서드는 서브 클래스에서 rethrow 메서드로 오버라이딩이 가능하다.

 

 

rethrow 함수와 프로토콜

 

-프로토콜 요구사항 중에 rethrow 함수가 있다면, throw 메서드를 구현한다고 해서 프로토콜을 준수할 수 없다.

하지만 프로토콜 요구사항 중에 throw 함수가 있다면, rethrow 메서드를 구현해서 프로토콜을 준수할 수 있다.

 

 

 

 

후처리 defer

-defer를 사용하여 현재 코드 블록을 나가기 전에 꼭 실행해야 하는 코드를 작성해 줄수 있다.

에러가 발생해서 코드 블록에서 나가는 경우, 정상적인 실행 후 코드 블록에서 나가는 경우 등의 상관없이

defer 구문은 무조건 실행된다.

 

예를 들어 함수 내에서 파일을 열어 사용하다가 에러가 발생하여 코드가 블록을 빠져나가더라도

defer문 내부에 파일을 정상적으로 닫아 메모리에서 해제하는 코드를 작성해주면 에러 발생 후 처리에 도움을 준다.

 

tip

defer 구문은 꼭 에러 처리에만 사용되지 않고, 여러 가지 상황에도 사용이 가능하다.

(함수, 메서드, 반복문, 조건문 등등 보통의 코드 블록 어디에서든지 사용가능)

 

ex) defer의 사용

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가 여러 개 있을 경우에는 맨 마지막 줄부터 실행된다.(bottom - up 순)

 

 

 

 

 

 

Reference :

-야곰님의 Swift문법 개정 3판

-https://jusung.gitbook.io/the-swift-language-guide/language-guide/17-error-handling

'Swift' 카테고리의 다른 글

CodingKey에 관하여  (0) 2023.03.31
메모리 안전에 관하여  (0) 2023.03.17
Where절에 관하여  (0) 2023.03.15
패턴에 관하여  (0) 2023.03.15
Nested Types(중첩 타입)에 관하여  (0) 2023.03.13