study record

[Swift] 오류 처리 본문

Swift/스위프트 프로그래밍

[Swift] 오류 처리

asong 2022. 2. 6. 14:04

*이 글은 책 '스위프트 프로그래밍'을 읽고 작성한 글입니다.

 

오류처리란

오류처리(Error Handling)는 프로그램이 오류를 일으켰을 대 이것을 감지하고 회복시키는 일련의 과정이다.

 

주의할 점은 스위프트의 오류처리 기능을 통해 시스템(운영체제 등 우리가 작성하는 응용프로그램 외부)에서 발생한 오류를 처리할 수 있는 것은 아니라는 것이다.

 

오류의 표현

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

 

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

 

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

Error 프로토콜을 채택함으로써 오류처리를 위한 타입임을 알 수 있다. 이처럼 열거형을 통해 오류의 종류를 표현하는 것이 가장 일반적이며 편리한 방법이다.

 

이렇게 오류의 종류를 미리 예상한 다음, 오류 때문에 다음에 행할 동작이 정상적으로 진행되지 않을 때라면 오류를 던져주면 된다. 오류를 던져줄 때는 throw 구문을 사용한다. 만약 자금이 부족하고 동전이 5개 더 필요한 상황이라면 throw VendingMachineError.insufficientFunds(coinNeeded: 5) 라고 오류를 던져줄 수 있다.

 

오류 포착 및 처리

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

 

스위프트의 오류 처리를 위한 네가지 방법

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

 

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

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

 

함수, 메서드, 이니셜라이저의 매개변수 뒤에 throws 키워드를 사용하면 해당 함수, 메섣, 이니셜라이저는 오류를 던질 수 있다. 일반적으로 func cannotThrowErrors() → String 처럼 표현하던 것에 func canThrowErrors() throws → String 처럼 throws 키워드를 명시해주면 오류를 던질 수 있다. 이런 함수는 호출했을 때, 동작 도중 오류가 발생하면 자신을 호출한 코드에 오류를 던져서 오류를 알릴 수 있다.

 

class VendingMachine {
	func vend(itemNamed name: String) throws {
		guard let item = self.inventory[name] else {
			throw VendingMachineError.invalidSelection
		}

		guard item.count > 0 else {
			throw VendingMachineError.outOfStock
		}
}
}

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

 

오류를 던질 수 있는 vend(itemNamed:)메서드 내부에서는 오류가 발생했을 때 흐름을 제어하기 위해 guard를 통한 빠른 종료 구문을 사용한다. 특정 조건이 충족되지 않는다면 throws 키워드를 통해 오류를 던져서 오류가 발생했다는 것을 자신을 호출한 코드에게 알린다.

 

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

함수, 메서드, 이니셜라이저 등에서 오류를 던져주면 오류 발생을 전달받은 코드 블록은 do-catch 구문을 사용하여 오류를 처리해주어야 한다. do 절 내부의 코드에서 오류를 던지면 catch 절에서 오류를 전달받아 적절이 처리해주면 된다.

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

 

catch 절에서는 catch 키워드 뒤에 처리할 오류의 종류를 써준다. 만약 catch 뒤에 오류의 종류를 명시하지 않고 코드 블록을 생성하면 블록 내부에 암시적으로 error 라는 이름의 지역 상수가 오류의 내용으로 들어온다.

 

func tryingVend(itemNamed: String, vendingMachine: VendingMachine) {
	do {
		try vendingMachine.vend(itemNamed: itemNamed)
	} catch VendingMachineError.invalidSelection {
		print("유호하지 않은 선택")
	} catch VendingMAchineError.outOfStock {
		print("품절")
	} catch {
		print("그 외 오류 발생")
	}
}

 

do-catch 구문을 사용하여 던져진 오류를 처리하는 함수를 별도로 만들었다. 그에 따라 오류를 받아서 다시 던지던 함수들이 더 이상 다른 곳으로 오류를 던지지 않아도 되는 형태가 되었다. 더불어 오류를 적절히 처리해주자 코드가 중간에 중단되지 않고 정상 동작할 수 있다.

 

옵셔널 값으로 오류처리

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

let x: Optional = try? someThrowingFunction(shouldThrowError: true)

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

try? 표현을 사용하여 호출한 함수가 오류를 던지면 반환 값이 nil로 반환되고, 오류가 발생하지 않으면 옵셔널 값으로 반환된다.

 

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

fetchData() 함수는 반환 타입을 옵셔널로 정의했다. 함수 내부에서 제대로 처리가 이루어지지 않으면 반환 값이 nil이 될 수 있음을 내포하는 것이다. 예시로 데이터를 디스크에서 가져오지 못하면 서버에서 가져오는 것을 시도해보고 그조차 없으면 nil을 반환하는 것이다.

 

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

프로그래머가 오류를 던질 수 있는 함수를 호출할 때 오류가 절대로 발생하지 않을 것이라고 확신할 수 있는 상황이라면 try! 표현을 사용할 수 있다. 만약 실제 오류가 발생하면 런타임 오류가 발생하여 프로그램이 강제 종료된다.

 

후처리 defer

defer 구문을 사용하여 현재 코드 블록을 나가기 전에 꼭 실행해야 하는 코드를 작성할 수 있다. defer 구문은 코드가 블록을 어떤 식으로 빠져나가든 간에 꼭 실행해야 하는 마무리 작업을 할 수 있도록 도와준다. 오류가 발생하여 코드 블록을 빠져나가든, 정상적으로 코드가 블록을 빠져나가든 defer 구문은 무조건 실행된다.

 

예시로 함수 내에서 파일을 열어 사용하다가 오류가 발생하여 코드가 블록을 빠져나가더라도 파일을 정상적으로 닫아 메모리에서 해제해야하기 때문에 defer 구문 내부에 파일을 닫는 코드를 작성해주어 정상적으로 파일이 메모리에서 해제될 수 있도록 한다.

func writeData() {
	let file = openFile()

	defer {
		closeFile(file)
	}
}

defer 구문 내부에는 break, return 등과 같이 구문을 빠져나갈 수 있는 코드 또는 오류를 던지는 코드는 작성하면 안 된다. 또한 여러 개의 defer 구문이 하나의 범위 내부에 속해 있다면 맨 마지막에 작성된 구문부터 역순으로 실행된다.

'Swift > 스위프트 프로그래밍' 카테고리의 다른 글

[Swift] 불명확 타입  (0) 2022.02.08
[Swift] 메모리 안전  (0) 2022.02.07
[swift] ARC  (0) 2022.02.04
[Swift] where절  (0) 2022.01.24
[Swift] 패턴  (0) 2022.01.20