본문 바로가기

iOS/Swift - UIKit

[Swift 문법] Error Handing(오류처리), try-throw와 do-catch, rethrow, defer

✳️ Error Handling(오류처리)

✨ try-throw / do-catch를 이용한 오류 처리

오류처리란?

프로그램이 실행되는 중에 발생한 오류를 감지하고 각 오류에 알맞은 처리를 부여하여 프로그램을 바른 방향으로 제어하는 것

 

오류가 발생하는 이유

개발자의 실수, 서버로부터 잘못된 데이터 전달, 데이터에 접근하는 자원이 많을 경우 등등 다양한 이유로 프로그램에 오류가 발생할 수 있다.

오류가 발생했을 때 오류의 유형과 성격에 맞게 처리하여 프로그램이 문제없이 작동할 수 있도록 할 필요가 있다.

 

Swift에서 오류처리의 구조

(1) enum: Error : 발생 가능한 오류를 예상하여 오류를 표현한다

Error 프로토콜을 채택한 열거형으로 주로 표현

(2) try - throw : 오류가 발생함을 알리기

throw로 오류가 발생 가능한 함수, 메서드, 생성자에 명시

try로 오류가 발생 가능한 함수, 메서드 실행 및 객체 생성

(3) do-catch : 발생한 오류를 받아서 처리하기

do-catch 구문으로 (2)의try 구문을 감싸줌

1️⃣ enum으로 발생 가능한 오류 표현하기

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

Error 타입 프로토콜을 채택한 enum 열거형으로 vendingMachine 클래스에서 발생할 수 있는 에러들을 선언해주고 있다.

2️⃣ throw로 오류 던지기

기본 형태 : throw Error의 형태로 오류를 던져줄 수 있다. 던져진 오류를 받는 것은 do-catch 문이 해준다.

// throw Error의 형태
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

 

throws를 메서드에 명시하기

만약 오류가 발생 가능하여 throw가 블록 내에 존재하는 메서드라면 아래와 같이 함수 선언부에 throws를 명시하여 이 메서드가 오류가 발생 가능한 메서드라는 것을 표시해줘야 한다.

func errorVend(coin: Int) throws -> Void {
		throw VendingMachineError.insufficientFunds(coinsNeeded: coin)
}

 

throws를 init에 명시하기

또한 생성자에도 throws를 사용하여 오류를 던져줄 수 있다.

Struct MyPoint {
	let name: String
	init(name: String, point: Int) throws {
		guard point<500 else { throw MyPointError.invalidPoint }
	}
}

3️⃣ try로 오류가 발생할 수 있는 메서드 실행하기

앞서 생성한 errorVend() 메서드의 실행 및 MyPoint 구조체의 객체 생성 방법

아래와 같이 throw 구문을 포함하는 메서드 및 생성자 앞에 try를 붙여서 오류가 발생가능한 구문을 실행할 수 있다.

try errorVend(150)

var dunoPoint: MyPoint = try MyPoint("duno", 150) // MyPoinError.invalidPoint 발생

📌  여기까지 정리하면

(1) 오류가 발생가능한 지점에서 throw를 통해 오류를 던져줬으며

(2) throws를 통해 오류를 던지는 구문을 포함하는 메서드 및 생성자에 ‘오류가 발생가능한 메서드'라는 것을 밝혔고

(3) try를 통해 ‘오류가 발생가능한 메서드'를 실행했다.

❗ 이제 할 일은, 오류가 발생한 경우에 따른 ‘처리'를 해주는 것이다.

4️⃣ do-catch 구문을 통해 던져진 오류를 잡아서 처리하기

do {
	try errorVend(150)
  print("errorVend() 실행 성공")
  var dunoPoint: MyPoint = try MyPoint("duno", 150)
	print("dunoPoint 생성 성공")
} catch VendingMachineError.insufficientFunds(let coinNeeded) {
	print("\\(coinNeeded) 만큼 코인이 부족해요")
} catch MyPointError.invalidPoint {
	print("잘못된 포인트 입력입니다")
}

do-catch 구문은 일단(do) 오류가 발생가능한 코드를 실행하고, 오류가 발생할 경우 오류를 종류별로 잡아서(catch) 처리한다는 뜻이다.

do 뒤의 첫 번째 블록 스코프에 들어있는 코드들은 기본적으로 오류가 없을 경우 실행해줄 순서대로 코드를 작성해주면 되고, 오류가 발생할 경우 즉시 catch (Error타입)에 해당하는 블록의 코드가 실행된다.

즉, 오류가 발생하면 즉시 catch문으로 이동하기 때문에, 해당 오류의 발생 지점으로부터 아래에 있는 코드들은 실행되지 않는다.

이처럼 여러 종류의 오류가 발생하는 경우 각각의 Error에 대한 catch 구문을 연달아 작성해주어 해당 오류에 적합한 처리를 해줄 수 있다.

⁉️ throw와 guard~else 문의 조합

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)")
}

위의 코드와 같이 guard~else문을 통해 조건이 맞지 않는 경우 else문 안에 throw를 넣어서 오류를 던질 수도 있다.

✳️ 옵셔널 값으로 오류 처리

앞선 오류 처리 방법은 try 구문을 통해 오류가 발생가능한 메서드를 실행했고, do-catch를 통해 발생한 오류를 처리하는 방식이었다. 이러한 방법 외에도 오류를 처리하는 방법이 있는데, 바로 옵셔널을 이용하는 것이다.

var dunoPoint: MyPoint? = try? MyPoint("duno", 150)

위의 코드는 error가 발생할 경우 nil이 dunoPoint에 저장되고, error가 없을 경우 MyPoint 객체가 생성되도록 한다.

 

📌 응용 : 오류 발생을 가정한 데이터 받아오기

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

위의 메서드는 서버로부터 데이터를 fatch받을 때, 다양한 방식으로 data를 불러와보고 모두 실패할 경우 nil이 반환되게 하는 메서드이다.

✳️ rethrows 다시 던지기

rethrows는 특정 메서드가 오류가 발생가능한 함수를 매개변수로 받은 경우, 매개변수로 받은 함수가 오류를 던질 수 있음을 표현하기 위해 사용할 수 있다.

func errorVend(coin: Int) throws -> Void {
  throw VendingMachineError.insufficientFunds(coinsNeeded: coin)
}

// (1) 잘 작성된 코드
func doubleErrorVend(callback: () throws -> Void) rethrows {
  do {
    try callback()
  } catch {
    print("errorVend()가 오류를 던졌습니다")
    throw VendingMachineError.rethrowError
  }
}
doubleErrorVend(errorVend(150)) // "errorVend()가 오류를 던졌습니다"

// (2) 잘못 작성된 코드
func doubleErrorVend(callback: () throws -> Void) rethrows {
  do {
    try errorVend(150)
  } catch {
    print("errorVend()가 오류를 던졌습니다")
    throw VendingMachineError.rethrowError   //  컴파일 에러 발생
  }
}

위의 doubleErrorVend()메서드를 보면, 콜백으로 throws를 하는 함수를 매개변수로 받는 것을 알 수 있다.

 

(1)의 경우에는 throws를 하는 함수를 매개변수로 받았기에 catch 부분에서 새로운 오류를 throw 가능하지만, (2)의 경우에는 콜백을 사용하지 않았기 때문에 throw가 불가능하다(컴파일 에러 발생)

 

즉, rethrows 메서드는 자체적으로 오류를 throw하지는 못하고, 매개변수로 받은 함수가 throw를 발생시킬 때 이를 다시 받아서 다른 Error를 throw하도록 처리할 수 있게 한다.

 

⚠️ 원래는 errorVend()메서드가 VendingMachineError.insufficientFunds(coinsNeeded: coin)를 던졌지만, doubleErrorVend는 이 오류를 받아서 VendingMachineError.rethrowError라는 새로운 오류를 던지도록 한다.

✳️ defer를 통해 오류처리 보장하기

스위프트에서 defer은, 해당 블록이 종료되기 전에 반드시 실행되어야 하는 코드를 보장해주는 역할을 한다.

defer 안에 작성된 코드들은 코드 블록을 빠져나가기 직전 무조건 실행되는 것이다.

defer가 오류처리에만 사용되는 것은 아니지만, 오류처리의 과정을 용이하게 만들어준다.

아래와 같이 반복문을 실행하다가 오류가 발생한 경우 close(file)이 실행되고 함수가 종료된다. 이처럼 defer를 통해 어떠한 오류가 발생한 경우에도 반드시 실행되어야 하는 코드들이 잘 실행되도록 할 수 있다.

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

Error Handling - The Swift Programming Language (Swift 5.6)