본문 바로가기

학습 노트/Swift (2021)

162 ~ 169. Error Handling (에러 처리)

Error Handling (에러 처리)

에러가 발생하면 보통 프로그램이 종료되지만, 미리 판단하여 처리하게 되면 종료를 막을 수 있다.

Compiletime Error & Runtime Error (컴파일 타임 에러와 런타임 에러)

에러는 컴파일타임 에러와 런타임 에러가 있다.
컴파일 타임 에러는 대부분 문법과 관련된 에러로, 컴파일러가 제공하는 정보로 쉽게 해결할 수도 있고, 자동 수정 기능을 제공하기도 한다.
런타임 에러는 프로그램이 실행중인 동안 발생한다. 문법적인 에러가 아닌 기기 상태나, 리소스의 상태 등 여러 이유로 발생할 수 있다.

Error Protocol (에러 프로토콜)

에러 처리에 사용되는 에러 형식은 에러 프로토콜을 채용하는 것 만으로 쉽게 구현할 수 있다.
보통은 열거형으로 선언한다.

enum errorTest: Error {

}

에러 프로토콜은 필수멤버가 없어 채용 선언 만으로 충분하다.
여러 발생 가능한 에러의 대응에는 다음과 같이 작성한다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

에러 형식은 일반 형식과는 다르게 swift에러 처리 시스템에 통합되게 된다.
errorTest에서 에러가 발생하면 새로운 에러 인스턴스를 생성하고, 에러를 처리하는 코드로 전달할 수 있다.
에러를 전달 받는 코드에서는 에러의 발생 종류를 확인하고, 처리한다.

이때 에러를 전달하는 것을 'Throw Error(에러를 던진다)'라고 표현한다.

Throw & Throws

Syntax

throw
throw expression

throws

    Throwing Function / Method
    func name(parameters) throws -> ReturnType {
        statements
    }

    Throwing Initializer
    init(parameters) throws {
        statements
    }

    Throwing Closure
    { (parameters) throws -> ReturnType in
        statements
    }

throw 키워드 다음에 오는 expression에는 에러 형식의 인스턴스를 작성한다.
또한 throw는 코드 블록이 에러를 던질 수 있다고 선언이 되어 있어야만 사용할 수 있다.

throw 키워드는 에러를 던지는 키워드, throws 키워드는 에러를 던질 수 있음을 나타내는 키워드이다.

Throwing function

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

//-----

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

throwing 함수 err은 문자열의 key와 범용 자료형의 value를 같은 딕셔너리를 파라미터로 받는다.
또한, throws가 선언되어있으므로 에러를 던지는 게 가능하다.
첫 번째 가드 문은 전달된 딕셔너리의 key가 name이 라니라면 에러 형식의 wrong3을 던진다.
두 번째 가드 문은 전달된 딕셔너리의 value가 Int로 캐스팅되지 않는다면 에러 형식의 wrong2를 던진다.

throws가 선언되어 있는 블럭에서 throw를 사용할 수 있는 것은 맞지만,
throw를 사용하지 않는다고 해서 문제가 되진 않는다.

Try

throwing 함수를 호출할 때는 try 키워드를 사용한다.

Syntax

try expression
try? expression
try! expression

expression 부분엔 throwing 함수, throwing 생성자, throwing 메소드, throwing 클로저를 작성한다.
첫 번째 문법은 이후에 언급할 do-catch에서 사용한다.
나머지 문법은 optional이 결합된 형태로,
두 번째 문법은 optional try로 에러가 발생할 경우 nil을 반환한다.
세 번째 문법은 const try로 에러가 발생할 경우 런타임 에러가 발생한다.

사진과 같이 함수의 자동 완성에 throws가 포함되어 있다면 try 키워드를 사용해야 한다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

//-----

try? err(data: [:])
결과

nil

빈 딕셔너리를 파라미터로 전달했으므로 첫 번째 가드 문이 실행되고,
optional try를 사용했기 때문에 런타임 에러가 발생하는 것이 아닌 nil을 반환한다.

에러 처리에선 크게 세가지 문법을 사용한다.

  1. do-catch
    코드에서 발생한 에러를 개별적으로 처리할 때 사용한다.
  2. try + optional binding
  3. hand over

 

do-catch Statements

Syntax

do {
    try expression
    statements
} catch pattern {
    statement
} catch pattern where condition {
    statements
}

문법에서 do 블럭은 필수 블록이다.
해당 블록에서는 try를 사용해 에러가 발생할 수 있는 코드를 실행한다.
만약 do 블록에서 에러가 발생한다면 즉시 해당되는 catch 블럭이 실행된다.

do 블럭에서 발생 가능한 모든 에러는 catch 블록을 통해 모두 대처되어야 한다.
만약 catch를 생략하는 경우 다른 코드로 전파될 수 있도록 구형해야 한다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
		guard let _ = data["age"] as? Int else {
	throw errorTest.wrong2
	}
}

//-----

do {
	try err(data: [:])
} catch errorTest.wrong2 {
	print("wrong2")
} catch {
	print("handle error")
}
결과

handle error

do 블록에서 에러가 발생할 코드를 실행하면,
해당 에러는 wrong3을 호출하게 된다.
따라서 첫 번째 catch 블록에 해당되지 않고,
두 번째 catch 블록이 실행된다.

catch 블록을 작성할 때는 가장 까다로운 패턴부터 작성해야 한다.
만약 위의 코드에서 두 블록의 순서를 바꾸게 된다면

이렇게 사용할 수 없는 catch 문이 생기게 된다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

//-----

do {
	try err(data: ["name": ""])
}  catch {
	print("handle error")
} catch errorTest.wrong2 {
	print("wrong2")
}
결과

handle error

의도대로라면 wrong2가 출력 되어야 하지만 첫 번째 catch문을 적용 한 뒤 종료하기 때문에 두 번째 catch문에는 접근할 수가 없다.
따라서 패턴을 생략한 catch 블록은 가장 마지막에 사용해야 한다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

	func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

do {
	try err(data: ["name": ""])
} catch errorTest.wrong2 {
	print("wrong2")
}

만약 do-catch 블록이 글로벌 스코프에 존재한다면 모든 에러에 대응하지 않아도 괜찮지만,
실제로 사용할 때에는 글로벌 스코프에 존재하는 경우가 매우 드물다.
따라서 do-catch 문은 do 블록에서 발생할 수 있는 모든 에러에 대응해야만 한다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

func handle() {
	do {
		try err(data: ["name": ""])
	} catch errorTest.wrong2 {
		print("wrong2")
	}
}
결과

//error

발생할 수 있는 모든 에러에 대응하지 않았기 때문에 에러가 발생한다.
따라서 catch 문을 추가해 에러에 대응하거나 handle 함수 자체가 에러를 다른 코드로 던질 수 있으면 문제는 해결된다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

func handle() throws {
	do {
		try err(data: ["name": ""])
	} catch errorTest.wrong2 {
		print("wrong2")
	}
}

이렇게 handle 함수가 throws 키워드를 가지게 되면 다른 에러들은 handle 함수를 호출한 코드로 전달되게 된다.
따라서 do-catch문을 사용하지 않고, try 키워드로만 호출해도 항상 오류를 던질 수 있으므로 문제가 없다.

패턴 없는 캐치 블록의 경우 패턴이 있는 캐치 블록에서 처리하지 못하는 모든 에러를 담당한다.
따라서 어떤 에러인지 판단할 수단이 필요하다.
패턴 없는 캐치 블록에는 error라는 특별한 로컬 상수가 자동으로 제공된다.

이 상수는 프로토콜의 형태로, 사용하기 위해선 타입 캐스팅이 필요하다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

func handle() throws {
	do {
		try err(data: ["name": ""])
	} catch {
		if let error = error as? errorTest {
			switch error {
			case .wrong1:
				print("wrong1")
			default:
				print("wrong2")
			}
		}
	}
}

패턴 없는 캐치 문은 대게 이런 패턴으로 구현된다.

 

Multi-pattern Catch Clasuses

do-catch 문은 에러를 매칭 시킬 수 있지만 여태 겪어왔던 것처럼 한 번에 하나의 에러만 매칭 시킬 수 있다.
만약 다른 에러지만 동일한 방식으로 에러를 처리해야 한다면, 코드의 중복 문제가 발생한다.
이를 위해 하나의 캐치 블록에서 복수의 에러를 적용하는 것은 불가능하다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

//-----

do {
	try err(data: [:])
} catch errorTest.wrong1, errorTest.wrong2 {

} catch errorTest.wrong3(let fieldName) {

} catch {

}
결과

//error

따라서 다음과 같이 해결하는 방법이 있었다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

//-----

do {
	try err(data: [:])
} catch errorTest.wring3(let fieldName) {

} catch let errr as errorTest {
	switch errr {
	case .wrong1, .wrong2:
		//something
		break
	default:
		break
	}
}

범용 캐치 블록에서 에러 형식을 바인딩 한 뒤 이를 다시 switch-case문으로 처리하는 방법이 있다.
의도한 대로 작동하는 코드이긴 하지만 이것을 개선한 기능이 swift 5.3에서 도입된다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func err(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong2
	}
}

//-----

do {
	try err(data: [:])
} catch errorTest.wrong1, errorTest.wrong2 {

} catch errorTest.wrong3(let fieldName) {

} catch {

}

처음 작성했던 코드와 동일하게 ', '를 사용해서 나열하면 따로 case문을 통해 분기하지 않아도 복수의 에러를 매칭 할 수 있다.

 

Optional Try

에러를 던지는 함수나 생성자를 호출할 때는 Try 키워드가 필요하다.
do 블록이 아닌 블록에서 호출하기 위해서는 Optional이나 forced try가 필요하다.

Syntax

try expression
try? expression
try! expression

optional try는 에러 발생 시 nil을 반환하고,
forced try는 에러 발생 시 코드를 종료한다.
두 형식은 모두 optional 형식이라 보통 optional 바인딩과 함께 사용된다.

enum errorTest: Error {
	case wrong1
	case wrong2
	case wrong3(String)
}

func error(data: [String: Any]) throws {
	guard let _ = data["name"] else {
		throw errorTest.wrong3("name")
	}
	
	guard let _ = data["age"] as? Int else {
		throw errorTest.wrong1
	}
}

//-----

if let _ = try? error(data: [:]) {
	print("success")
} else {
	print("fail")
}
결과

fail

error 함수는 값을 반환하지 않기 때문에 와일드카드 패턴을 사용했다.
함수에서 에러가 발생하지 않는다면 바인딩이 가능하고 success가 출력된다.
에러가 발생한다면 else 블록의 fail이 출력된다.
지금은 빈 딕셔너리를 전달하고 있기 때문에 코드에서 에러가 발생해 else 블록의 fail이 출력됐다.

같은 코드를 do-catch 문으로 바꾸면 다음과 같다.

do {
	try error(data: [:])
	print("success")
} catch {
	print("fail")
}
결과

fail

if 블록은 do 블록으로, else 블록은 패턴 없는 catch 블록으로 대체되었다.

optional try를 사용할 때 반드시 optional 바인딩을 사용해야 하는 것은 아니다.

try? error(data: [:])
try! error(data: ["name": "kim", "age": 44])
결과

nil

빈 딕셔너리를 전달 한 optional try는 별도의 에러 처리 없이 nil을 반환하고,
정상적인 딕셔너리를 전달 한 forced try는 에러 없이 정상적인 작동을 했다.
이때 forced try에 빈 딕셔너리를 전달하면

try! error(data: [:])
결과

//error

에러가 발생하게 되는데 이는 런타임 에러로 실 사용에서는 충돌에 해당한다.
위와 같이 forced try는 에러를 전달할 수 없고, 따라서 do-catch 문으로 대응할 수도 없다.
따라서 최대한 사용을 하지 않는 것이 좋다.
에러가 나는 상황이 확실할 때에만 이를 사용하게 된다.

 

defer Statements

defet 문은 코드의 실행을 스코프가 종료되는 시점으로 미루게 된다.
주로 코드에서 사용한 자원을 정리하기 위해 사용한다.

Syntax

defer {
    statements
}

defer문을 만나게 되면 즉시 defer 블록 안의 코드가 실행되는 것이 아닌 defer문이 포함된 스코프가 종료될 때까지 실행을 연기한다.

func practice(thing: String) {
	let file = FileHandle(forReadingAtPath: thing)
	
	//something
	
	file?.closeFile()
}

practice 함수는 파라미터로 받은 경로를 통해 파일을 불러오고, 작업 이후 파일을 닫는 간단한 함수이다.

func practice(thing: String) {
	let file = FileHandle(forReadingAtPath: thing)
	
	if thing.hasSuffix(".jpg") {
		return
	}
	
	file?.closeFile()
}

이렇게 기능을 추가하게 되면 파라미터인 thing으로 전달된 경로에 '. jpg'가 포함되어 있으면 반환하도록 변경됐다.
이렇게 되면 이후에 작성된 closeFile()이 실행되지 않고 함수가 종료되기 때문에 바일을 닫을 수가 없게 되는데,
이때 사용하는 것이 defer문이다.

func practice(thing: String) {
	let file = FileHandle(forReadingAtPath: thing)
	
	defer {
		file?.closeFile()
	}
	
	if thing.hasSuffix(".jpg") {
		return
	}
}

이렇게 defer문을 추가하면 함수 안에서 무슨 일이 일어나건 간에,
함수가 종료되는 시점에 열었던 파일을 닫도록 할 수 있다.

func practice(thing: String) {
	print("open")
	let file = FileHandle(forReadingAtPath: thing)
	
	defer {
		print("close")
		file?.closeFile()
	}
	
	if thing.hasSuffix(".jpg") {
		print("check")
		return
	}
	
	print("end")
}

practice(thing: ".png")
결과

open
end
close

practice 함수에 png 파일이 포함된 경로가 입력됐다고 가정하자.
일단은 open이 출력되고,
defer를 기억하고 코드를 진행한다.
if문에 해당되지 않으므로 작성된 end를 출력 한 뒤,
함수가 종료되는 시험에 defer 블록이 실행돼 close를 출력한다.

func practice(thing: String) {
	print("open")
	let file = FileHandle(forReadingAtPath: thing)
	
	defer {
		print("close")
		file?.closeFile()
	}
	
	if thing.hasSuffix(".jpg") {
		print("check")
		return
	}
	
	print("end")
}

practice(thing: ".jpg")
결과

open
check
close

이번엔 경로에 jpg 파일이 포함되어 있다고 가정하자.
일단은 open이 출력되고,
defer를 기억한 후 코드를 진행한다.
if문의 조건에 해당되므로 check가 출력되고, 함수가 종료되는데,
이때 defer 블록이 실행돼 close가 출력된다.

func practice(thing: String) {
	print("open")
	let file = FileHandle(forReadingAtPath: thing)
	
	defer {
		print("close")
		file?.closeFile()
	}
	
	if thing.hasSuffix(".jpg") {
		print("check")
		return
	}
	
	defer {
		print("test")
	}
	print("end")
}

practice(thing: ".jpg")
결과

open
check
close

이번엔 end 직전에 defer문을 하나 더 추가했다.
하지만 test는 출력되지 않는다.

defer문을 선언했다고 항상 실행되지는 않는다.
함수가 종료되기 이전에 호출이 되어야 하는 특성 때문에 defer문은 보통 함수의 최상단에 작성한다.

func practice2() {
	defer {
		print("1")
	}
	defer {
		print("2")
	}
	defer {
		print("3")
	}
}

practice2()
결과

3
2
1

이번엔 새로운 함수 안에 defer문 세 개를 만들고 함수를 호출했다.
위에서부터 1, 2, 3을 출력하도록 작성했지만,
출력된 결과는 3, 2, 1이다.
defer문들끼리는 가장 마지막에 호출된 defer문이 우선권을 가진다.
따라서 순서에 유의하여 사용해야 하고, 특별한 이유가 없으면 하나만 사용하는 것이 좋다.

 

Result Type

swift 에러 처리 시스템의 발전

1.x

var error: NSError?
let str: NSString
let url: URL
let success = str.writeToURL(url, atomatically: true, encoding: NSUTF8StringEncoding, error: &error)
if !success {
	println("Error: \(error!)")
}

스위프트 1.x의 오류 처리 방식은 objective-c와 같은 방식의 오류처리 방식을 사용했었다.
pointer를 사용하는 방식이기 때문에 pointer 사용을 지양하는 swift의 방식에는 맞지 않는 방법이었다.

2.x

enum MyError: ErrorType {
	case someError
}

func do something() throws {
	throws MyError.someError
}

do {
	try doSomthing()
} catch {
	print(error)
}

지금까지 사용되는 에러 처리 모델은 2.x 에서 추가된 에러 처리 모델이다.
에러가 발생할만한 코드 블록을 throwing 함수나 throwing 메서드로 선언한다.
이후 do-catch문 안에서 try를 통해 호출하고 발생한 에러를 처리한다.

enum NumberError: Error {
	case negativeNumber
	case evenNumber
}

enum AnotherNumberError: Error {
	case tooLarge
}

func process(oddNumber: Int) throws -> Int {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	return oddNumber * 2
}

do {
	let result = try process(oddNumber: 1)
	print(result)
} catch {
	print(error.localizedDescription)
}
결과

2

process는 throwing 함수로서 홀수만 처리하도록 구현되어 있다.
파라미터로 음수가 전달되면 negaticeNumber 에러를,
짝수가 전달되면 evenNumber 에러를 던진다.

do-catch 문은 이를 try로 호출하고, catch 블록에서 에러를 처리하게 된다.

위의 코드는 의도한 결과를 출력하지만 몇 가지 문제가 있다.
throws 키워드는 해당 블록에서 에러를 던질 수 있다는 것을 나타내지만, 에러의 형식을 특정하진 못한다.
하나의 블록에서 다양한 형식의 에러를 던질 수 있고, 코드 블록을 호출할 때에는 어떤 형식인지 파악할 방법이 없다.
다시 말해 catch로 전달될 때 실제 에러의 형식이 아닌 에러 protocol 형식이 전달되는 것이다.

어떠한 형식인지 판단할 수 있다면 그때는 타입 캐스팅이 필요하다.

enum NumberError: Error {
	case negativeNumber
	case evenNumber
}

enum AnotherNumberError: Error {
	case tooLarge
}

func process(oddNumber: Int) throws -> Int {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	return oddNumber * 2
}

do {
	let result = try process(oddNumber: 1)
	print(result)
} catch let myErr as NumberError {
	switch myErr {
	case .negativeNumber:
		print("negative number")
	case .evenNumber:
		print("even number")
	}
} catch {
	print(error.localizedDescription)
}

이렇게 새로운 catch문을 만들면서 타입 캐스팅을 진행한다.
이제는 numberError가 전달되면 첫 번째 catch 블록이 처리하고,
이외의 경우엔 두 번째 catch 블록이 처리하게 된다.

enum NumberError: Error {
	case negativeNumber
	case evenNumber
}

enum AnotherNumberError: Error {
	case tooLarge
}

func process(oddNumber: Int) throws -> Int {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	guard oddNumber < 10000 else {
		throw AnotherNumberError.tooLarge
	}
	return oddNumber * 2
}

do {
	let result = try process(oddNumber: 100001)
	print(result)
} catch let myErr as NumberError {
	switch myErr {
	case .negativeNumber:
		print("negative number")
	case .evenNumber:
		print("even number")
	}
} catch {
	print(error.localizedDescription)
}
결과

The operation couldn’t be completed. (__lldb_expr_3.AnotherNumberError error 0.)

이번엔 process에 guard문을 추가해 너무 큰 수가 들어오면 tooLarge 에러를 던지도록 작성했다.
이제는 oddNumber에서 두 가지의 에러를 던지게 된다.

oddNumber에 100001을 전달하면 우리의 의도와는 다르게 에러를 처리하고 있는 것을 확인할 수 있다.
이는 컴파일러가 새로운 형식의 에러가 추가됐음을 인식하지 못하기 때문으로, 논리적인 오류가 발생할 가능성이 높아진다.

enum NumberError: Error {
	case negativeNumber
	case evenNumber
}

enum AnotherNumberError: Error {
	case tooLarge
}

func process(oddNumber: Int) throws -> Int {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	guard oddNumber < 10000 else {
		throw AnotherNumberError.tooLarge
	}
	return oddNumber * 2
}

do {
	let result = try process(oddNumber: 100001)
	print(result)
} catch let myErr as NumberError {
	switch myErr {
	case .negativeNumber:
		print("negative number")
	case .evenNumber:
		print("even number")
	}
}

process 함수가 NumberError형식의 에러만 던진다면 문제가 없지만,
지금처럼 다른 형식의 에러를 던지게 되면 런타임 에러가 발생하게 된다.
컴파일 시점에 문제를 파악할 수 없기 때문에 코드 안정상이 낮아진다.

Result Type

@frozen public enum Result<Success, Failure> where Failure : Error {
	
	case success(Success)
	
	case failure(Failure)
	
	@inlinable public func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure>
	
	@inlinable public func mapError<NewFailure>(_ transform: (Failure) -> NewFailure) -> Result<Success, NewFailure> where NewFailure : Error
	
	@inlinable public func flatMap<NewSuccess>(_ transform: (Success) -> Result<NewSuccess, Failure>) -> Result<NewSuccess, Failure>
	
	@inlinable public func flatMapError<NewFailure>(_ transform: (Failure) -> Result<Success, NewFailure>) -> Result<Success, NewFailure> where NewFailure : Error
	
	@inlinable public func get() throws -> Success
}

Result 형식의 구현 부이다.
result 타입은 제너럴 열거형으로 Success 케이스와 Failure 케이스가 선언되어 있으며, 연관 값을 가지고 있다.
success 케이스에 저장할 수 있는 형식에는 제한이 없지만 Failure 케이스에는 에러 형식만 저장할 수 있다.
generic으로 선언되어있기 때문에 형식이 명확하다는 의미이다.
두 번째 형식 파라미터로 형식을 명확히 선언하기 때문에 모호함이 사라진다.

작업이 성공하면 success 케이스에 결과가 저장된다.
작업이 실해하면 failure 케이스에 에러가 저장된다.

let result = Result {
	try process(oddNumber: 1)
}
switch result {
	case .success(let data):
		print(data)
	case .failure(let error):
		print(error.localizedDescription)
}

Result 형식은 throwing 클로저로 초기화하는 생성자를 제공한다.
따라서 기존의 코드를 수정하지 않고 바로 처리할 수 있다.

result 상수에 Result 인스턴스가 저장되게 되는데,
Result 인스턴스는 success 케이스와 failure 케이스를 가지는 열거형이다.
따라서 switch 문을 활용하는 것이 가능하다.

이렇게 되면 성공했을 경우 결괏값을 출력하고,
에러가 발생했을 경우 failure 케이스와 매칭 되어 에러를 확인할 수 있다.

func processResult(oddNumber: Int) -> Result {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	guard oddNumber < 10000 else {
		throw AnotherNumberError.tooLarge
	}
return oddNumber * 2
}
결과

//error

이번엔 process 함수와 비슷한 구조의 함수 processResult 함수를 만들고,
throws가 아닌 Result 타입을 반환형으로 지정했다.
Result 타입은 제네릭 열거형으로 성공했을 때의 반환 형식과 에러 형식을 직접 선언해야 한다.

enum NumberError: Error {
	case negativeNumber
	case evenNumber
}

enum AnotherNumberError: Error {
	case tooLarge
}

func process(oddNumber: Int) throws -> Int {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	guard oddNumber < 10000 else {
		throw AnotherNumberError.tooLarge
	}
	return oddNumber * 2
}

let result = Result {
	try process(oddNumber: 100001)
}
switch result {
case .success(let data):
	print(data)
case .failure(let error):
	print(error.localizedDescription)
}

//-----

func processResult(oddNumber: Int) -> Result<Int, NumberError> {
	guard oddNumber >= 0 else {
		return Result.failure(NumberError.negativeNumber)
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		return Result.failure(NumberError.evenNumber)
	}
	
	//    guard oddNumber < 10000 else {
	//        return Result.failure(AnotherNumberError.tooLarge)
	//    }
	return Result.success(oddNumber * 2)
}

Result 타입으로 에러를 전달할 때는, 함수에서 에러를 직접 던지지 않고,
연관 값으로 저장해 반환한다.
이전과는 달리 작업이 성공하면 정수를 반환하고, 실패하면 에러를 전달한다는 게 명확해진다.
또한, 에러 형식을 직접 지정하기 때문에 형식 안정성이 보장되고,
잘못된 형식으로 인한 문제는 컴파일 타임에 확인할 수 있게 된다.

또한 형식 추론을 지원하기 때문에 아래와 같이 더욱 단순화하는 것도 가능하다.

func processResult(oddNumber: Int) -> Result<Int, NumberError> {
	guard oddNumber >= 0 else {
		return .failure(.negativeNumber)
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		return .failure(.evenNumber)
	}
	
	return .success(oddNumber * 2)
}

failure 타입을 NumberError로 선언했기 때문에 AnotherNumberError 타입의 tooLarge 에러는 전달할 수 없다.
반환 형은 success 케이스에 담아 반환한다.

이전과 비교하면 에러를 직접 전달하는 것이 아닌 연관 값으로 전달한다.
따라서 throwing 함수로 선언하지 않아도 동작하고, 호출 방식과 에러 처리 방식이 달라진다.
성공과 실패가 열거형으로 전달되고, 실질적인 에러 처리는 결과를 사용하는 시점에 진행하게 된다.
이러한 것을 Delayed Error Handling이라고 한다.

enum NumberError: Error {
	case negativeNumber
	case evenNumber
}

enum AnotherNumberError: Error {
	case tooLarge
}

func process(oddNumber: Int) throws -> Int {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	guard oddNumber < 10000 else {
		throw AnotherNumberError.tooLarge
	}
	return oddNumber * 2
}

func processResult(oddNumber: Int) -> Result<Int, NumberError> {
	guard oddNumber >= 0 else {
		return Result.failure(NumberError.negativeNumber)
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		return Result.failure(NumberError.evenNumber)
	}

	return Result.success(oddNumber * 2)
}

//-----

let result2 = processResult(oddNumber: 1)
switch result2 {
case .success(let data):
	print(data)
case .failure(let error):
	print(error.localizedDescription)
}
결과

2

Result 타입을 사용한 함수는 위와 같이 호출할 수 있다.
이때, 실패했을 때의 처리 없이 성공했을 때만 가정한다면 get 메서드를 사용할 수 있다.

enum NumberError: Error {
	case negativeNumber
	case evenNumber
}

enum AnotherNumberError: Error {
	case tooLarge
}

func process(oddNumber: Int) throws -> Int {
	guard oddNumber >= 0 else {
		throw NumberError.negativeNumber
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		throw NumberError.evenNumber
	}
	
	guard oddNumber < 10000 else {
		throw AnotherNumberError.tooLarge
	}
	return oddNumber * 2
}

func processResult(oddNumber: Int) -> Result<Int, NumberError> {
	guard oddNumber >= 0 else {
		return Result.failure(NumberError.negativeNumber)
	}
	
	guard !oddNumber.isMultiple(of: 2) else {
		return Result.failure(NumberError.evenNumber)
	}
	
	return Result.success(oddNumber * 2)
}

//-----

if let result = try? result2.get() {
	print(result)
}

해당 메소드는 작업이 성공하면 결과를 반환하고, 실패하면 에러를 던지는 throwing 메소드이다.
processResult 함수에서 성공 시 반환형을 Int로 선언했기 때문에 자동완성에서도 Int로 표시된다.
해당 값은 do-catch문을 사용해도 되고, Optional try로 차리 하는 것도 가능하다.

이렇게 되면 코드가 더 간결해진 것을 확인할 수 있다.

throing function은 정확히 어떤 에러를 던지는지 판단하기 힘들다.
반면 result 형식을 사용하면 에러 형식이 명시적으로 선언된다.
컴파일 타임에 에러를 판단할 수 있다는 형식 안전성이 높다는 의미이다.
따라서 타입 캐스팅 없이 에러를 처리할 수 있고, 형식 추론을 사용할 수 있어 코드가 단순해진다.
작업의 성공과 실패를 명확히 구분해서 실행할 수 있다는 장점이 있다.
또한 get 메소드를 사용하면 더욱 단순하게 구현할 수 있다.

이렇게 좋은 점이 많지만, 기존의 에러 처리 방식을 완전히 대체할 수는 없다.

비동기 코드에서의 Result type

guard let url = URL(string: "http://kxcoding-study.azurewebsites.net/api/books") else {
	fatalError("invalid url")
}

struct BookListData: Codable {
	let code: Int
	let totalCount: Int
	let list: [Book]
}

struct Book: Codable {
	let title: String
}

typealias CompletionHandler = (BookListData?, Error?) -> ()

func parseBookList(completion: @escaping CompletionHandler) {
	let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
		if let error = error {
			completion(nil, error)
			return
		}
		
		guard let data = data else {
			completion(nil, nil)
			return
		}
		
		do {
			let list = try JSONDecoder().decode(BookListData.self, from: data)
			completion(list, nil)
		} catch {
			completion(nil, error)
		}
	}
	task.resume()
}

parseBookList { (data, error) in
	if let error = error {
		print(error.localizedDescription)
		return
	}
	
	data?.list.forEach { print($0.title) }
}
결과

iOS 앱 개발을 위한 Swift 3
Objective-C 개발자를 위한 Swift
iOS 앱 개발을 위한 Swift 4
Hello, Swift

보통의 네트워크 통신은 비동기 방식으로 구현된다.
서버로부터 데이터가 전달되면 task 변수에서 가장 마지막에 전달된 클로저가 호출되고,
이 클로저는 parseBookList의 클로저인 completion 클로저를 호출한다.
이런 클로저를 completion handler라고 부른다.

completion handler는 optional BookListData와 Optional Error를 파라미터로 전달받는 클로저이다.
parseBookList 함수를 throwing 함수로 선언하는 것도 불가능하기 때문에,
해당 함수에 에러가 발생하면 첫 번째 파라미터로 nil을 반환하고, 두번째 파라미터로 에러를 전달한다.

반대로 정상적인 데이터라면 첫번째 파라미터로 데이터를 전달하고, 두번째 파라미터로 nil을 전달한다.

실제 오류 처리는 해당 함수를 호출하는 부분에서 진행한다.
호출 부분을 보면 parseBookList 함수를 호출하고 두번째 파라미터를 비교해 에러를 출력하고,
에러가 발생하지 않았다면 배열의 데이터를 열거하고, tilte을 출력한다.

에러가 발생해 두번째 파라미터로 nil이 전달되는 시점에 첫번째 파라미터로 데이터가 전달된다는 것을 문법적으로 보장하지 못한다.
completion handler의 파라미터가 optional인 이유는 성공과 실패에 따라서 불필요한 값을 전달하지 않기 위함인데, 이 때문에 실제 값이 전달되는지 확인하는 과정도 필요하다.
따라서 위의 코드처럼 optional binding이나 optional chining을 사용한다.

guard let url = URL(string: "http://kxcoding-study.azurewebsites.net/api/books") else {
	fatalError("invalid url")
}

struct BookListData: Codable {
	let code: Int
	let totalCount: Int
	let list: [Book]
}

struct Book: Codable {
	let title: String
}

enum ApiError: Error {
	case general
	case invalidFormat
}

typealias CompletionHandler = (Result<BookListData, ApiError>) -> ()

func parseBookList(completion: @escaping CompletionHandler) {
	let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
		if let error = error {
			completion(nil, error)
			return
		}
		
		guard let data = data else {
			completion(nil, nil)
			return
		}
		
		do {
			let list = try JSONDecoder().decode(BookListData.self, from: data)
			completion(list, nil)
		} catch {
			completion(nil, error)
		}
	}
	task.resume()
}

optional로 선언되어 있던 CompeltionHandler의 파라미터를 result type으로 변경했다.

func parseBookList(completion: @escaping CompletionHandler) {
	let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
			if let error = error {
				completion(.failure(.general))
				return
			}
		
		guard let data = data else {
			completion(.failure(.general))
			return
		}
		
		do {
			let list = try JSONDecoder().decode(BookListData.self, from: data)
			completion(.success(list))
		} catch {
			completion(.failure(.general))
		}
	}
	task.resume()
}

수정 전에는 성공했을 때의 데이터와 에러를 직접 전달했으나 result type으로 선언한 지금은 결과를 각각의 클래스에 담아 전달하게 되고,
따라서 형식도 optional이 아니게 된다.
따라서 실패 시 failure를 통해 APIError를 전달하고,
성공 시 success를 통해 결과인 list를 전달한다.

optional의 구분이 사라지고, 성공과 실패의 여부에 따라 어떤 값이 전달되는지 명확히 알 수 있다.

func parseBookList(completion: @escaping CompletionHandler) {
	let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
		if let error = error {
			completion(.failure(.general))
			return
		}
		
		guard let data = data else {
			completion(.failure(.general))
			return
		}
	
		do {
			let list = try JSONDecoder().decode(BookListData.self, from: data)
			completion(.success(list))
		} catch {
			completion(.failure(.general))
		}
	}
	task.resume()
}

//-----

parseBookList { (result) in
	switch result {
	case .success(let data):
		data.list.forEach {print($0.title)}
	case .failure(let error):
		print(error.localizedDescription)
	}
}

이는 호출 문에도 동일하게 적용된다.
optional이 사라졌기 때문에 optional 바인딩이나 optional 체이닝, 타입 캐스팅이 사라져 간결해졌다.

parseBookList { (result) in
	switch result {
	case .success(let data):
		data.list.forEach {print($0.title)}
	case .failure(let error):
		print(error.localizedDescription)
	switch error {
	case .general:
		//code
		break
	case .invalidFormat:
		//code
		break
	}
}

또한 내부에 다시 switch case문을 선언해 에러들의 개별적인 대응도 간단히 구현할 수 있다.

Higher-order Functions (고차 함수)

Map

Map은 성공 값을 새로운 형식으로 변환할 때 사용한다.

enum MyError: Error {
	case error
}

enum ValueError: Error {
	case evenNumber
}

func doSomethingWithResult(data: Int) -> Result<Int, MyError> {
	guard data.isMultiple(of: 2) else {
		return .failure(MyError.error)
	}
	
	return .success(data)
}

doSomethingWithResult 함수는 Int를 파라미터로 받고, 반환형은 Result, success시 Int, Failure시 MeError를 반환한다.
파라미터의 값이 짝수면 Failure를 반환하고, 홀수면 Success를 반환한다.

enum MyError: Error {
	case error
}

enum ValueError: Error {
	case evenNumber
}

func doSomethingWithResult(data: Int) -> Result<Int, MyError> {
	guard data.isMultiple(of: 2) else {
		return .failure(MyError.error)
	}
	
	return .success(data)
}

//-----

let a = doSomethingWithResult(data: 0)
let b = a.map {
	$0.isMultiple(of: 2) ? "even number" : "odd number"
}
결과

even number

파라미터로 0을 전달하며 함수를 호출한다.
이후 map을 사용해 짝수면 even number를, 홍수면 odd number를 반환하도록 했다.

map은 자동완성에서 보여주는 것과 같이 Success 값을 받아 새로운 형식의 Success 값을 반환한다.


위의 코드에서의 map을 적용한 변수는 기존의 <Int, MyError> 형식이 아닌 <String, MyError>의 형태를 가지고 있다.

flatMap


flatMap은 조금 다르다.
현재 success 값을 파라미터로 받아, 새로운 Result 인스턴스를 반환한다.

enum MyError: Error {
	case error
}

enum ValueError: Error {
	case evenNumber
}

func doSomethingWithResult(data: Int) -> Result<Int, MyError> {
	guard data.isMultiple(of: 2) else {
		return .failure(MyError.error)
	}
	
	return .success(data)
}

//-----
let a = doSomethingWithResult(data: 0)
let c = a.flatMap {
	$0.isMultiple(of: 2) ? .success("even number") : .success("odd number")
}
결과

success("even number")

flatMap은 Map과는 다르게 Result에 담아 전달해야 한다.

Map과 flatMap은 성공한 결과만 변환하고, 실패 시엔 변환하지 않는다.

연습

아래의 둘은 실패 값을 변환한다는 것 외엔 동일하다.

a.mapError(transform: (MyError) -> Error(MyError) -> Error)
a.flatMapError(transform: (MyError) -> Result<Int, Error>(MyError) -> Result<Int, Error)

이 둘을 사용해 MyError를 ValueError를 변환해 보자.

mapError


mapError는 마찬가지로 Failure를 받아 새로운 형식의 Failure를 반환한다.

enum MyError: Error {
	case error
}

enum ValueError: Error {
	case evenNumber
}

func doSomethingWithResult(data: Int) -> Result<Int, MyError> {
	guard data.isMultiple(of: 2) else {
		return .failure(MyError.error)
	}
	
	return .success(data)
}

let a = doSomethingWithResult(data: 1)

//-----

let d = a.mapError {
	_ in ValueError.evenNumber
}
결과

failure(__lldb_expr_57.ValueError.evenNumber)

에러를 그대로 받아 ValueError의 evenNumber로 변환한다.

이렇게 반환된 d는 <Int, ValueError>의 형식을 갖는다.

flatMapError


flatMap과 동일하게 파라미터로 failure를 전달받아 새로운 Result 형식을 반환한다.

enum MyError: Error {
	case error
}

enum ValueError: Error {
	case evenNumber
}

func doSomethingWithResult(data: Int) -> Result<Int, MyError> {
	guard data.isMultiple(of: 2) else {
		return .failure(MyError.error)
	}
	
	return .success(data)
}

let a = doSomethingWithResult(data: 1)

//-----

let e = a.flatMapError {
	_ in .failure(ValueError.evenNumber)
}
결과

failure(__lldb_expr_57.ValueError.evenNumber)

failure가 ValueError.evenNumber 형식으로 변환돼 저장된 것을 확인할 수 있다.

 

Assertion and Precondition

코드가 실행 중인 상태에서 입력된 값의 유효성을 확인하거나 실행 결과를 검증할 때 사용하는 디버깅 도구이다.
또한 특정 조건이 true로 평가되면 계속 코드를 실행하고, false로 평가되면 코드를 중지한 다음 메시지를 출력하고 해당 코드로 이동한다.
조금 과격한 방식으로 작동하기 때문에 코드의 문제점을 비교적 빠르고 확실하게 발견 할 수 있다는 장점이 있다.

예제 파일에는 텍스트 필드와 버튼으로 구성된 스토리보드가 존재한다.

class ViewController: UIViewController {
	
	@IBOutlet weak var inputField: UITextField!
	
	
	@IBAction func processValue(_ sender: Any) {
		guard let value = inputField.text, let number = Int(value) else {
		return
	}
	
	
	print(number)
}

override func viewDidLoad() {
	super.viewDidLoad()
	
	}
}

ViewController 파일에 선언된 메소들 중 processValue 메소드는 버튼과 연결되어 있다.
입력된 값을 숫자로 바꾼 다음에 출력하는 동작을 한다.
이때 입력된 값이 반드시 0을 초과해야 한다고 가정해 보자.
또한, assertion을 사용해 값이 조건에 만족하는지 확인해 보도록 하자.

Assertion

func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)

assertion은 assert 함수로 구현되어 있다.
해당 함수에는 첫 번째 파라미터로 테스트할 조건을, 두 번째 파라미터로 조건이 false일 때 표시할 문자열을 가지고 있다.
세 번째 파라미터는 조건이 false일 때 출력할 파일의 이름을, 기본값은 assert가 호출된 파일의 이름이다.
마지막 파라미터는 조건이 false일 때 출력할 라인 번호이고, 기본값은 assert가 호출된 라인이다.
보통 첫 번째, 두 번째 파라미터만 전달한다.

해당 코드는 shipping code에는 영향을 주지 않는다는 설명이 문서에 있는데,
이는 assert 함수가 디버그 모드에서만 동작하고, 릴리즈 모드에서는 동작하지 않기 때문이다.
따라서 배포용 코드에 포함이 되어 있어도 문제가 없다.

class ViewController: UIViewController {
	
	@IBOutlet weak var inputField: UITextField!
	
	
	@IBAction func processValue(_ sender: Any) {
		guard let value = inputField.text, let number = Int(value) else {
			return
		}
		
		assert(number > 0, "number must larger than 0")
		
		print(number)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
	
	}
}

위와 같이 assert 함수로 조건과 false일 때의 출력문을 작성했다.

해당 프로젝트를 시뮬레이션하면 다음과 같은 결과가 콘솔에 표시된다.

0을 초과하는 값을 입력하고 버튼을 누르면 assert 함수는 true로 판단되고 코드는 그대로 진행돼 콘솔에 값이 출력되게 된다.
하지만 0을 입력하면 assert 함수는 false로 판단되고, 크래시가 발생하면서 코드의 실행이 중지된다.
또한 에디터에서 assert가 호출된 곳으로 이동하고, 지정한 에러 메시지가 표시된다.
콘솔도 마찬가지로 에러가 발생한 라인과 지정한 메세지가 출력된다.

assert 함수는 항상 condition을 전달해야 하는데,
이미 condition이 도출된 상태라면 다른 함수를 사용해도 된다.

class ViewController: UIViewController {

	@IBOutlet weak var inputField: UITextField!
	
	
	@IBAction func processValue(_ sender: Any) {
		guard let value = inputField.text, let number = Int(value) else {
		return
	}
	
	if number > 0 {
	
	} else {
		assertionFailure("number must lerger than 0")
	}
	
	print(number)
}

override func viewDidLoad() {
	super.viewDidLoad()
	
	}
}

이번엔 조건 판단을 if에 맞기고,
조건에 맞지 않는다면 else문을 통해 assertionFailure 함수를 사용해 메시지만 출력하도록 구현했다.

이번엔 디버그 모드가 아닌 릴리즈 모드로 테스트했다.

조건에 맞을 때도, 조건에 맞지 않을 때도 크래쉬나 메시지 표시 없이 동작하므로,
릴리즈 모드에선 assertion이 동작하지 않는 것이 확인됐다.

릴리즈 모드는 사용자가 사용하는 코드로, 이때 에러가 발생했다고 해서 크래쉬로 대응하는 것은 부적절하다.
따라서 이 때는 경고창을 띄워 주는 것이 옳다.

만약 릴리즈 모드에서도 같은 기능을 활용하고 싶다면 precondition 함수를 사용한다.

func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)

전달받는 파라미터들은 assert 함수와 완전히 동일하다.
precondition 함수는 디버그와 릴리즈에서 모두 동작하는 assert 함수라고 볼 수 있다.

문서에 따르면 precondition 함수가 조건을 따르지 않는 경우는 '-Ounchecked'플래그가 포함되어 있지 않는 경우 단 한 가지이다.

class ViewController: UIViewController {
	
	@IBOutlet weak var inputField: UITextField!
	
	
	@IBAction func processValue(_ sender: Any) {
		guard let value = inputField.text, let number = Int(value) else {
			return
		}
		
		assert(number > 0, "number must larger than 0")
		precondition(number > 0, "number must larger than 0")
		print(number)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
	}
}

이번엔 assert와 같은 파라미터를 전달하는 precondition 함수를 작성하고 릴리즈 모드에서 테스트한다.

assert만 사용했을 때는 아무 반응이 없었지만, precondition 함수가 함께 존재할 때는 콘솔과 에디터에 별 다른 변화 없이 시뮬레이터가 강제로 종료된다.
라인도 알려주지 않고, 메시지도 띄우지 않는 이 precondition은 특정 조건을 만족시키지 못한 경우에 앱을 강제로 종료해야 하는 경우 사용한다.

예를 들어 어느 사용자가 잘못된 비밀번호를 수회 입력했을 때 계속해서 경고를 띄우는 것보다는 강제로 앱을 종료하는 것이,
메크로나 다른 악용 사용자로부터 보호할 수 있는 방법이 될 수 있다.

precondition도 assert와 마찬가지로 preconditionFailure 함수를 제공한다.
동일하게 메시지만 출력한다.

보통은 디버그 모드에서 테스트를 진행하고, 릴리즈 모드에서 사용자에게 제공한다.
하지만 최근엔 사용자에게 제공하기 이전에 베타테스터에게 제공해야 하는 경우가 생긴다.
이러한 경우에는 사용자 제공과는 다르게 조건을 테스트해야 한다.
또한 베타테스터 제공과 일반 사용자 제공은 모두 릴리즈로 빌드해야 한다.

이때도 지금처럼 precondition을 사용하지만, 이대로 배포하면 베타 테스터가 사용해도 별 조치 없이 종료된다는 점에서 다를 것이 없다.
하지만 테스터들은 '어떠한 상황에서 크래시가 발생했다.'라고 전달해 줄 수 있다.

반대로 생각하면 일반 사용자들은 영문도 모른 채로 크래시를 경험해야 한다.
precondition 함수를 삭제하거나 주석 처리하는 방법이 있지만 너무 번거로운 방법이다.

이 때 필요한 것이 문서에서 말 한 '-Ounchecked' 플래그다.

프로젝트 설정 > Targets > Build Setting

에서 'optimization'을 검색한다.
이 중 Release 항목을 Other로 선택한 뒤 -Ounchecked를 작성해 준다.
이후 빌드를 진행하면 동일한 코드로 0을 입력해도 멀쩡히 동작하는 것을 확인할 수 있다.

 

FatalError

assert 함수는 릴리즈 빌드에선 동작하지 않고,
precondition 함수는 -Ounchecked 플래그를 포함하면 동작하지 않았다.
하지만 fatalError는 어떠한 조건에서도 반드시 동작하는 아주 강력한 조건이다.

func fatalError(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Never

fatalError는 조건에 상관없이 무조건 작동을 중지하기 때문에 condition을 파라미터로 받지 않는다.
나머지 파라미터는 assert와 precondition과 동일하다.

class ViewController: UIViewController {
	
	@IBOutlet weak var inputField: UITextField!
	
	
	@IBAction func processValue(_ sender: Any) {
		guard let value = inputField.text, let number = Int(value) else {
			return
		}
	
		assert(number > 0, "number must larger than 0")
		precondition(number > 0, "number must larger than 0")
		
		fatalError("fatal Error")
		
		print(number)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
	}
}
결과

//worning

fatalError를 추가하면 이후에 존재하는 코드에 경고가 표시된다.
fatalError를 만나면 반드시 실행이 중지되기 때문에 이후에 존재하는 코드는 절대 실행되지 않는다.

테스트는 릴리즈 빌드에 -Ounchecked 플래그가 선언된 상태에서 진행된다.
해당 환경은 assert와 precondition 함수가 모두 동작하지 않는 환경이다.
결과는 다음과 같다.


시뮬레이터가 강제 종료됨과 report navigator와 콘솔에 표시된다.

 


Log

2021.09.15.
블로그 이전으로 인한 글 옮김 및 수정