Error Handling

Error Handling?

오류 처리란 말 그대로 프로그램을 사용하면서 발생하는 오류를 감지하고 처리하는 것을 말한다. 프로그램이 우리가 예상하는 대로 동작한다는 확신은 없기에 오류를 적절히 감지하고 오류에 따른 적절한 대처가 중요하다.

예를 들어 네트워크를 사용하는 앱을 생각해보자. 네트워크에서 데이터를 받아와 앱에 보여줘야 하는데 그 데이터가 없을 수도 있고, 아니면 앱에서 요청을 할 때 매개 변수를 잘못 입력하거나 빠뜨릴 수도 있고, 또 권한이 없는 경우일 수도 있다.

좋은 품질의 앱은 다양한 측면에서 평가할 수 있겠지만, 이와 같은 다양한 오류 상황에 대해 사용자가 불편하지 않도록, 각각의 오류에 대해 올바르게 대응할 수 있는 것은 그 앱을 평가하는 데에 무시할 수 없는 기준일 것이다.

Error 표현

Swift에서 오류는 Error 프로토콜을 따르는 타입으로 표현한다. 이 프로토콜은 요구사항이 없기 때문에 커스텀 타입에 프로토콜을 따른다고 선언만 하면 된다.

Swift에서 열거형(Enumerations)은 오류를 표현하기 적합한 타입이다.

1
2
3
4
5
6
7
8
9
10
enum OperatingSystem {
case window
case mac
case linux
}

enum GamePlayError: Error {
case performance
case unsupportedOS(os: OperatingSystem)
}

오류의 종류를 표현하기 쉽고 연관 값을 통해 부가적인 정보까지 전달할 수 있다. 위의 예에서 GamePlayError 타입의 unsupportedOS(:) 케이스의 경우 지원하지 않는 운영체제가 무엇인지도 알려준다. 이처럼 오류의 종류를 몇가지로 예상해 볼 수 있다.

Error 처리하기

오류를 표현했다면 적절한 처리가 필요하다. Error Handling - The Swift Programming Language에서는 네 가지 방법을 소개한다.

  • 함수가 발생시킨 오류를 그 함수의 코드에 알리기
  • do-catch 구문을 통한 처리
  • 옵셔널을 통한 처리
  • 오류가 발생하지 않을 거라 단정하기

1. 함수가 발생시킨 오류를 그 함수의 코드에 알리기

함수에서 오류를 던지고 그것을 받아 적절히 처리하는 방법이다. try 키워드로 받을 수 있으며 오류를 언제, 어디서 받아 처리할지 잘 결정해야 한다. 왜냐하면 오류로 인해 프로그램 전체의 흐름이 달라지기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct Computer {
var os: OperatingSystem
var performance: Performance

init(os: OperatingSystem, performance: Performance) {
self.os = os
self.performance = performance
}

func playMyGame() throws -> Bool {
guard self.performance == .good else {
throw GamePlayError.performance
}

guard self.os == .window else {
throw GamePlayError.unsupportedOS(os: self.os)
}

return true
}
}

func playInPCRoom(pcRoom: String) throws {
print("Play game in \(pcRoom)")

let computer = Computer(os: .mac, performance: .bad)
try computer.playMyGame()
}

// func playInPCRoom(pcRoom: String) {
// print("Play game in \(pcRoom)")
// let computer = Computer(os: .mac, performance: .bad)
// try computer.playMyGame() // Errors thrown from here are not handled
// }

위의 playMyGame 메서드는 Guard 문을 통해 오류가 날 경우 오류를 던지고 빠른 종료를 한다. playMyGame 메서드는 오류를 던지기 때문에 호출할 시에도 에러를 적절히 처리해줘야 한다. 그래서 playInPCRoom 함수에서는 playMyGame 메서드를 호출하기 때문에 throws를 붙여 에러를 처리해야 한다.

2. do-catch 구문을 통한 처리

do 절에서 try를 통해 오류를 던지게 되면 catch가 이를 포착해 오류를 받아 오류에 따른 처리를 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func playInPCRoom(pcRoom: String)  {
print("Play game in \(pcRoom)")

let computer = Computer(os: .mac, performance: .bad)
do {
try computer.playMyGame()
} catch GamePlayError.performance {
print("플레이 불가 사유: 성능 문제")
} catch GamePlayError.unsupportedOS(os: .linux) {
print("플레이 불가 사유: 리눅스 지원 불가")
} catch GamePlayError.unsupportedOS(os: .window) {
print("플레이 불가 사유: 윈도우 지원 불가")
} catch GamePlayError.unsupportedOS(os: .mac) {
print("플레이 불가 사유: 맥 지원 불가")
} catch {
print("그 외의 플레이 불가 사유: \(error))")
}
}

func playInPCRoom(pcRoom: String) {
print("Play game in \(pcRoom)")

let computer = Computer(os: .mac, performance: .bad)
do {
try computer.playMyGame()
} catch let cause {
print("플레이 불가 사유: \(cause)")
}
}

첫번째 경우 모든 경우에 대해 catch를 해주는 경우이다. catchswitch문과 마찬가지로 exhaustive해야 한다. 그러므로 default까지 우리가 예상하지 못한 에러도 처리해주어야한다. 예외 상황에서 catch 구문에서 오류의 종류를 명시하지 않는 다면 error라는 이름의 지역 상수를 코드 블록에서 사용할 수 있다.
두번째 경우에는 error 지역 상수를 cause라는 이름으로 사용하였다.

3. 옵셔널을 통한 처리

try?를 통한 오류 처리로 오류가 반환될 시 반환 값으로 Optional.none, 즉 nil이 나오게 된다. 중요한 것은 에러가 나오지 않는 경우에서 반환값이 옵셔널이 아닌 메소드, 함수였더라도 반환 타입이 옵셔널이 붙여져 나온다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
func playInPCRoom(pcRoom: String, computer: Computer)  {
print("Play game in \(pcRoom)")

let isPlayable = try? computer.playMyGame()
print(isPlayable)
}

playInPCRoom(pcRoom: "3POP", computer: Computer(os: .window, performance: .good))
// Play game in 3POP
// Optional(true)
playInPCRoom(pcRoom: "LineUp", computer: Computer(os: .mac, performance: .good))
// Play game in LineUp
// nil

4. 오류가 발생하지 않을 거라 단정하기

개발자가 오류가 발생하지 않을 것이라 확신할 때 try! 키워드를 통해 사용하는 방법이다. 에러가 나는 경우 다른 느낌표(강제 타입 캐스팅, 강제 언래핑) 사용처럼 크래시가 나기 때문에 쓰지 않는 것이 좋을 것이다.

1
2
3
4
5
6
7
8
9
10
func playInPCRoom(pcRoom: String, computer: Computer)  {
print("Play game in \(pcRoom)")

let isPlayable = try! computer.playMyGame()
print(isPlayable)
}

playInPCRoom(pcRoom: "LineUp", computer: Computer(os: .mac, performance: .good))
// Play game in LineUp
// Fatal error: 'try!' expression unexpectedly raised an error: ...

defer로 마지막 청소

Swift 공식 문서에서 오류처리 가이드의 마지막으로 소개한 것이 defer 구문이다. 코드블럭에서 벗어날 때 defer 구문 안의 코드 블럭이 실행되는 특징을 가지고 있기 때문에 개발자가 코드 블럭이 언제 끝나는지 신경쓰지 않아도 마지막 정리를 해줄 수 있다. 에러가 있든, 없든 무조건 실행이 보장된다. 공식 문서에서 사용한 예제는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
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.
}
}

파일을 열고 읽어서 사용하려고 할 때 에러가 발생할 수도, 발생하지 않을 수도 있다. 어쨌든 파일을 정상적으로 닫아주어 메모리에서 해제해줘야 하기 때문에 defer 문을 통해 파일을 닫아준다.

defer 문은 오류 처리에 유용하게 쓰이지만 이뿐만 아니라 함수, 반복문, 조건문 등등 코드 블록 어디에서든 사용이 가능하다고 한다.

defer 문은 특이하게 스택 구조처럼 실행이 된다. 한 코드 블록 내에서 defer 문이 여러 개가 있다면 나중에 작성된 구문부터 실행이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func deferExample() {
print(1, separator: "", terminator: " ")

defer {
print(5, separator: "", terminator: "") // 가장 마지막에 실행되므로 ""
}

print(2, separator: "", terminator: " ")

defer {
print(4, separator: "", terminator: " ")
}

print(3, separator: "", terminator: " ")
}

deferExample()
// 1 2 3 4 5

Reference