오류 처리

피터·2022년 10월 31일
0
post-thumbnail

오류처리(Error Handling)란

프로그램이 오류를 일으켰을 때 이것을 감지하고 회복시키는 일련의 과정입니다. 주의할 점은 스위프트의 오류처리 기능을 통해 시스템에서 발생한 오류를 처리할 수 있다는 것이 아님을 명심해야 합니다.

오류의 표현

스위프트에는 오류는 Error라는 프로토콜을 준수하는 타입의 값을 통해 표현됩니다. Error 프로토콜은 사실상 요구사항이 없는 빈 프로토콜일 뿐이지만, 오류를 표현하기 위한 타입(주로 열거형)은 이 프로토콜을 채택합니다.

스위프트의 열거형은 오류의 종류를 나타내기에 아주 적합한 기능입니다. 연관 값을 통해 오류에 관한 부가 정보를 제공할 수도 있습니다.

예시

자판기 동작에 대한 오류의 종류를 열거형으로 표현해보겠습니다.

enum vendingMachineError: Error {
    case invalidSelection // 유효하지 않은 선택
    case insufficientFunds(coinsNeeded: Int) // 금액 부족, coinsNeeded 필요한 코인 수
    case outOfStock // 품절
}

그렇다면 이 오류로 인해 다음에 행할 동작이 정상적으로 진행되지 않을 때라면 오류를 던져(Throw Error)주면 됩니다. 오류를 던져줄 때는 throw 구문을 사용합니다.

오류 포착 및 처리

오류를 던질 수도 있지만 오류가 던져지는 것에 대비하여 던져진 오류를 처리하기 위한 코드도 작성해야 합니다.

스위프트에는 오류를 처리하기 위한 네 가지 방법이 있습니다.

  • 함수에서 발생한 오류를 해당 함수를 호출한 코드에 알리는 방법
  • do-catch 구문을 이용하여 오류를 처리하는 방법
  • 옵셔널 값으로 오류를 처리하는 방법
  • 오류가 발생하지 않을 것이라고 확신하는 방법

함수에서 발생한 오류 알리기

try 키워드로 던져진 오류를 받을 수 있습니다. try 키워드는 try 또는 try?, try! 등으로 표현할 수 있습니다.

함수, 메서드, 이니셜라이저의 매개변수 뒤에 throws 키워드를 사용하면 해당 함수, 메서드, 이니셜라이저는 오류를 던질 수 있습니다.

💡 throws는 함수나 메서드의 자체 타입에도 영향을 미칩니다. 즉, throws 함수나 메서드는 같은 이름의 throws가 명시되지 않는 함수나 메서드와 구분됩니다. 또, throws를 포함한 함수, 메서드, 이니셜라이저는 일반 함수, 메서드, 이니셜라이저로 재정의할 수 있습니다. 반대로 일반 함수, 메서드, 이니셜라이저는 throws 함수, 메서드, 이니셜라이저로 재정의할 수 있습니다.

예시

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

struct Item {
    var price: Int
    var count: Int
}

class VendingMachine {
    var inventory: [String : Item] = [
        "막대사탕" : Item(price: 12, count: 7),
        "감자칩" : Item(price: 10, count: 4),
        "비스켓" : Item(price: 7, count: 11)
    ]
    
    var coinsDeposited: Int = 0
    
    private func 제공(snack: String) {
        print(snack + " 제공")
    }
    
    func vend(itemName: String) throws {
        guard let item = inventory[itemName] else {
            throw VendingMachineError.invalidSelection
        }
        
        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }
        
        guard item.price < self.coinsDeposited else {
            throw VendingMachineError.insufficientFunds(
                coinsNeeded: (item.price - self.coinsDeposited)
            )
        }
        
        self.coinsDeposited -= item.price
        
        var newItem = item
        newItem.count -= 1
        self.inventory[itemName] = newItem
        
        self.제공(snack: itemName)
    }
}

let 먹고싶은목록 = [
    "피터": "감자칩",
    "채드": "막대사탕",
    "히로": "초콜릿"
]

func 자판기사용해보기(person name: String, vendingMachine: VendingMachine) throws {
    let snackName = 먹고싶은목록[name] ?? "비스켓"
    try vendingMachine.vend(itemName: snackName)
}

let machine: VendingMachine = VendingMachine()
machine.coinsDeposited = 30

for (person, snack) in 먹고싶은목록 {
    print(person, snack)
    try 자판기사용해보기(person: person, vendingMachine: machine)
}

// 피터 감자칩
// 감자칩 제공
// 채드 막대사탕
// 막대사탕 제공
// 히로 초콜릿

// 오류 발생!
// Playground execution terminated: An error was thrown and was not caught:
// __lldb_expr_59.VendingMachineError.invalidSelection

오류를 던질 수 있는 함수, 메서드, 이니셜라이저를 호출하는 코드는 반드시 오류를 처리할 수 있는 구문을 작성해주어야 합니다.

오류를 받은 코드가 적절히 오류를 처리해주지 않는다면 이후의 코드는 작동하지 않습니다. 던져진 오류를 처리하는 방법에 대해 알아보겠습니다.

do-catch 구문을 이용하여 오류처리

do {
    try 오류 발생 가능코드
    오류가 발생하지 않으면 실행할 코드
} catch 오류 패턴1 {
    처리코드
} catch 오류 패턴2 {
    처리코드
}

예시

func 자판기를사용(itemName: String, 자판기: VendingMachine) {
    do {
        try 자판기.vend(itemName: itemName)
    } catch VendingMachineError.invalidSelection {
        print("유효하지 않은 선택입니다.")
    } catch VendingMachineError.insufficientFunds(let 부족한_동전) {
        print("자금 부족 - 동전 \(부족한_동전)개를 추가로 지급해주세요.")
    } catch {
        print("그 외 오류 처리: ", error)
    }
}

let machine: VendingMachine = VendingMachine()
machine.coinsDeposited = 20

for (person, snack) in 먹고싶은목록 {
    print(person, snack)
    try 자판기를사용(itemName: snack, 자판기: machine)
}

// 피터 감자칩
// 감자칩 제공
// 채드 막대사탕
// 자금 부족 - 동전 2개를 추가로 지급해주세요.
// 히로 초콜릿
// 유효하지 않은 선택입니다.

옵셔널 값으로 오류처리

try?를 사용하여 옵셔널 값으로 변환하여 오류를 처리할 수도 있습니다. try? 표현을 통해 동작하던 코드가 오류를 던지면 그 코드의 반환 값은 nil이 됩니다.

예시

let value = try? 오류를던지는함수(shouldThrowError: true)
print(value)

let newValue = try? 오류를던지는함수(shouldThrowError: false)
print(newValue)

// nil
// Optional(100)

오류가 발생하지 않을 것이라고 확신하는 방법

오류가 발생하지 않을 것이라는 전제하에 try! 표현을 사용할 수 있습니다. 다만 실제 오류가 발생하면 런타임 오류가 발생하여 프로그램이 강제로 종료됩니다.

다시 던지기

함수나 메서드는 rethrows 키워드를 사용하여 자신의 매개변수로 전달받은 함수가 오류를 던진다는 것을 나타낼 수 있습니다. rethrows 키워드를 사용하여 다시 던지기(Rethrowing)가 가능하게 하려면 최소 하나 이상의 오류 발생 가능한 함수를 매개변수로 전달받아야 합니다.

func 오류를던지는함수() throws {
    enum SomeError: Error {
        case justError
    }
    throw SomeError.justError
}

func 어떤함수(callback: () throws -> Void) rethrows {
    enum AnotherError: Error {
        case justAnotherError
    }
    do {
				// 매개변수로 전달한 오류 던지기 함수가 아니므로 
				// catch 절에서 제어할 수 있습니다.
        try callback()
    } catch {
        throw AnotherError.justAnotherError
    }
    
    do {
				// 매개변수로 전달한 오류 던지기 함수가 아니므로 
				// catch 절에서 제어할 수 없습니다.
        try 오류를던지는함수()
    } catch  {
				// 오류 발생!
        throw AnotherError.justAnotherError
    }
    
		// rethrows시 catch 절 외부에서 단독으로 오류를 던질 수는 없습니다.
		// 오류 발생!
    throw AnotherError.justAnotherError
}

부모클래스의 rethrows 메서드는 자식클래스에서 throws 메서드로 재정의할 수 없습니다. 그러나 부모클래스의 throws 메서드는 자식클래스에서 rethrows 메서드로 재정의할 수 있습니다.

후처리 defer

defer 구문을 사용하여 현재 코드 블록을 나가기 전에 꼭 실행해야하는 코드를 작성해줄 수 있습니다. 즉, defer 구문은 코드가 블록을 어떤 식으로 빠져나가든 간에 꼭 실행해야 하는 마무리 작업을 할 수 있도록 도와줍니다.

예를 들어 코드가 닫히기 전에 파일을 꼭 닫는 코드를 추가해야하는 경우에 유용합니다.

func 파일을씁니다() {
    defer {
        일단파일을닫으세요()
    }
    
    if ... {
        return
    }
    
    if ... throws {
        throw
    }
    
    // 처리 끋
}

defer 구문 내부에는 break, return 등과 같이 구문을 빠져나갈 수 있는 코드 또는 오류를 던지는 코드는 작성하면 안 됩니다.

여러 개의 defer 구문이 하나의 범위 내부에 속해 있다면 맨 마지막에 작성된 구문부터 역순으로 실행됩니다.

func someFunc() {
    print(1)
    
    defer {
        print(2)
    }
    
    do {
        defer {
            print(3)
        }
        print(4)
    }
    
    defer {
        print(5)
    }
    
    print(6)
}

someFunc()

// 1
// 4
// 3
// 6
// 5
// 2

자료 출처: 야곰 스위프트 프로그래밍 3판

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
profile
iOS 개발자입니다.

0개의 댓글