[Swift] Error Handling

728x90

Swift의 Error Handling에 대해 알아 보겠습니다.

 

Error Handling이란

프로그램 동작 중 예상 가능한 오류가 발생했을 때 이를 감지하고 복구하기 위한 일련의 처리 과정
Exception Handling과 유사하지만 다른 특성들을 지닌 부분이 있어 의도적으로 다른 용어를 사용

 

먼저 Error란 무엇일까요?

Error

Swift 에서 정의하는 심각도에 따른 4가지 오류의 유형입니다.

 

1. simple domain error (단순 도메인 오류)

 - 명백하게 실패하도록 되어 있는 연산 또는 추측에 의한 실행 등으로 발생 

    예)  1. 숫자가 아닌 문자로부터 정수를 파싱,

          2. 빈 배열에서 어떤 요소를 꺼내는 동작 등

 

 - 오류에 대한 자세한 설명이 필요하지 않으며 대개 쉽게 또 즉시 에러를 처리할 수 있음.

 - Optional Value 등을 통해 Swift 에 잘 모델링되어 있어서 더 복잡한 솔루션이 필요 없음.

 

2. recoverable (복구 가능한 오류)

 - 복잡한연산을 수행하는 도중 실패가발생 할 수 있지만 사전에 미리 오류를 합리적으로 예측 할 수 있는 작업

    예) 파일을 읽고 쓰는 작업, 네트워크 연결을 통해 데이터 읽기 등

 - iOS 에서는 NSError 또는 Error 를 이용하여 처리

 - 일반적으로 이런 오류의 무시는 좋지 않으며 위험할 수도 있으므로 오류를 처리하는 코드를 작성하는 것이 좋다.

 - 오류 내용을 유저에게 알려주거나, 다시 해당 오류를 처리하는 코드를 수행하여 처리하는 것이 일반적

 

3. universal error (범용 오류)

 - 시스템이나 어떤 다른 요인에 의한 오류

 - 이론적으로는 복구가 가능하지만, 어느 지점에서 오류가 발생하는 지 예상하기 어려움

 

4. logic failure (논리적 오류)

 - Logic 에 대한 오류는 프로그래머의 실수로 발생하는 것으로 프로그램적으로 컨트롤할 수 없는 오류에 해당

 - 시스템에서 메시지를 남기고 abort()를 호출하거나 Exception 발생

 

오류 타입 정의하기

Error Handling을 정리하다 꼼꼼한 재은씨의 Swift: 문법편에서 오류 처리 쪽이 정말 잘 나와있어서 참고 하였습니다.

 

프로그래밍을 하다보면 절대 피할 수 없는 것이 오류입니다. 그래서 프로그래밍 과정을 원하는 로직을 잘 구현하는 일 외에도 오류를 꼼꼼하게 처리하여 안정성을 높이는 일을 매우 중요하게 다룹니다.

 

스위프트는 옵셔널을 통해 오류를 충준비 처리할 수 있어서, 타 언어에서 즐겨 사용되는 try ~ catch와 같은 오류 처리 구문이 필요하지 않을 만큼 안정성이 뛰어나다는 점을 강점으로 세웠지만 옵셔널 타입을 사용한다고 해도 발생하는 오류의 종류에 상관없이 단순이 nil하나만 반환할 수 있기 때문에 실행하는 과정에서 발생할 수 있는 다양한 오류들을 함수 외부로 전달하기가 어렵다는 점에서 단점이 있습니다.

 

코코아터치 프레임워크에서 사용하는 오류 처리 구조 역시 스위프트가 바라던 아키텍처와 차이가 있습니다. 오브젝티브-C 기반으로 작성된 코코아터치 프레임워크에서 오류 정보를 반환하는 방식은 오류 정보를 저장할 객체를 인자값으로 함수나 메소드에 전달한 다음 필요할 때 꺼내어 사용하는 방식입니다. 이는 함수나 메소드에서 반환 타입을 준수하면서도 오류 정보를 제공할 수 있는 방법이지만, 기본적으로 오류에 대한 모든 대응을 옵셔널 타입으로 해결하고자 하는 스위트프의 언어 구조입장에서 그다지 좋은 입장은 아닙니다.

 

다른 객체 지향언어들은 오류가 발생했을 때 함수나 메소드에서 해당 오류를 반환(return)하는 것이 아니라 던지는(throws)처리를 할 수 있게끔 지원합니다.

 

먼저 YYYY-MM-DD형태를 보이는 문자열을 분석하여 연도, 월, 일 형식의 데이터로 각각 변환하는 함수가 있다고 해봅시다. 오류의 내용은 다양하겠지만 예상되는 오류를 정리했습니다.

 

  1. 입력된 문자열의 길이가 필요한 크기와 맞지 않는 오류
  2. 입력된 문자열의 형식이 YYYY-MM-DD 형태가 아닌 오류
  3. 입력된 문자열의 값이 날짜와 맞지 않는 오류

이런식의 에러를 가정했을때 가정 적합한 객체 타입이 바로 열거형 입니다.

 

오류 타입으로 사용되는 열거형을 정의할때는 반드시 Error라는 프로토콜을 구현해야합니다.
컴파일러는 Error 프로토콜을 구현한 열거형만을 오류 타입으로 인정합니다.

 

이때 Error는 아무 기능도 정의되지 않은 빈 프로토콜입니다.

protocol Error {

}

그렇지만 이 프로토콜을 구현한 열거형은 오류 타입으로 사용해도 된다는 뜻입니다!
일단 Error 프로토콜을 열거형에 추가하고 나면 나머지는 우리가 원하는 대로 정의할 수 있습니다.

 

enum DateParseError: Error {
	case overSizeString
	case underSizeString
	case incorrectFormat(part: String)
	case incorrectData(part: String)
}

 

오류 던지기

우리가 작성한 오류 타입 객체는 함수나 메소드를 실행하는 과정에서 필요에 따라 외부로 던져 실행 흐름을 옮겨버릴 수 있습니다. 이때 함수나 메소드는 오류 객체를 외부로 던질 수 있다는 것을 컴파일러에 알려주기 위해 throws 키워드를 추가 합니다.

 

throws 키워드는 반환 타입을 표시하는 화살표(->) 보다 앞에 작성해야되는데, 이는 오류를던지면 값이 반환되지 않는다는 의미이기도 합니다.

 

실제로 날짜를 분석하는 함수를 작성하고, 실행 과정에서 발생할 수 있는 오류 상황에서 오류 객체를 던져 보겠습니다. 앞서 작성한 DateParse오류 객체를 사용합니다.

func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

위 두 함수는 모두 문자열을 반환하지만, canThrowErrors()함수는 실행 과정에서 오류가 발생하면 그 오류를 객체로 만들어 던질 수 있는 반면, cannotThrowErrors()메소드는 오류가 발생하더라도 오류 객체를 던질 수 없다.
이렇게 throws키워드가 추가된 함수나 메소드, 또는 클로저는 실행 블록 어느 시점에서건 우리가 의도하는 오류를 던질 수 있다.

 

struct Date {
    var year: Int
    var month: Int
    var date: Int
}

func parseDate(param: NSString) throws -> Date {
    
    //입력된 문자열의 길이기 10이 아닐경우 분석이 불가능하므로 오류
    guard param.length == 10 else {
        if param.length > 10 {
            throw DateParseError.overSizeString
        } else {
            throw DateParseError.underSizeString
        }
    }
    
    //반환할 객체 타입 선언
    var dateResult = Date(year: 0, month: 0, date: 0)
    
    //연도 정보 분석
    if let year = Int(param.substring(with: NSRange(location: 0, length: 4))) {
        dateResult.year = year
    } else {
        //연도 분석 오류
        throw DateParseError.incorrectFormat(part: "year")
    }
    
    //월 정보 분석
    if let month = Int(param.substring(with: NSRange(location: 5, length: 2))) {
        //월에 대한 값은 1 ~ 12까지만 가능하므로 그 이외의 범위는 잘못된 값으로 처리한다.
        guard month > 0 && month < 13 else {
            throw DateParseError.incorrectData(part: "month")
        }
        
        dateResult.month = month
        
    } else {
        //월 분석 오류
        throw DateParseError.incorrectFormat(part: "month")
    }
    
    // 일 정보 분석
    if let date = Int(param.substring(with: NSRange(location: 8, length: 2))) {
        
        //일에 대한 값은 1 ~ 31까지만 가능하므로 그 이외의 범위는 잘못된 값으로 처리한다.
        guard date > 0 && date < 32 else {
            throw DateParseError.incorrectData(part: "date")
        }
        
        dateResult.date = date
        
    } else {
        throw DateParseError.incorrectFormat(part: "date")
    }
    
    return dateResult
}

 

오류잡아내기

앞에서는 오류를 던지는 방법을 알아보았는데 이번엔 오류가 던져졌을 경우 잡아내는 방법을 알아 보겠습니다.

 

syntax

do {
    try <오류를 던질 수 있는 함수>
} catch <오류 타입1> {
    // 오류 타입 1에 대한 대응
} catch <오류 타입2> {
    // 오류 타입 2에 대한 대응
} catch <오류 타입3> {
    // 오류 타입 3에 대한 대응
} catch …

do 구문은 오류가 발생하지 않는 상황에서 실행할 구문이 작성되는 영역입니다. 물론 do 구문 내에서 함수의 호출도 이루어져야 합니다. 컴파일러는 do 구문 내부에서 작성된 순서대로 코드를 실행하다가 try 함수 호출에서 오류가 던져지면 이를 catch구문으로 전달합니다.

 

func getPartsDate(date: NSString, type: String) {
    do {
        let date = try parseDate(param: date)
        
        switch type {
        case "year":
            print("\(date.year)년 입니다.")
        case "month":
            print("\(date.month)월 입니다")
        case "date":
            print("\(date.date)일 입니다.")
        default:
            print("입력값에 해당하는 날짜 정보가 없습니다.")
        }
    } catch DateParseError.overSizeString {
        print("입력된 문자열이 너무 깁니다. 줄여주세요.")
    } catch DateParseError.underSizeString {
        print("입력된 문자열이 불충분 합니다. 늘려주세요.")
    } catch DateParseError.incorrectFormat(let part) {
        print("입력값의 \(part)에 해당하는 형식이 잘못 되었습니다.")
    } catch DateParseError.incorrectData(let part) {
        print("입력값의 \(part)에 해당하는 값이 잘못사용되었습니다. 확인해주세요.")
    } catch {
        print("알 수 없는 오류가 발생하였습니다.")
    }
}

throw키워드는 아무코드에서나 호출 할 수 없고 오류를 던지도록 선언된 코드블록 내부에서만 호출 할 수 있습니다.
리턴문과 마찬가지로 호출된 코드블록을 즉시 종료합니다. 즉, 같은 블록 내 이어지는 코드들은 실행되지 않는다는 뜻!

 

날짜를 입력 받아 parseDate(param:)함수를 호출하고, 요청된 부분의 날짜 정보를 출력해주는 getPartsDate(date:type:)함수를 작성하였습니다. 함수의 내부에서는 do~catch 구문이 작성되어있는데, parseDate(param:)메소드가 던지는 오류를 잡아낼 수 있도록 catch구문에서 각 오류 타입을 명시하고 있습니다. 이렇게 오류타입으로 나누어진 catch구문은 그에 맞는 오류가 던져졌을 때 잡아내게 되고, 그에 맞는 출력 구문을 통해 오률 정보를 보여줍니다.

getPartsDate(date: "2018-111-11", type: "year") //입력된 문자열이 너무 깁니다. 줄여주세요.
getPartsDate(date: "2015-12-31", type: "month") // 12월 입니다.
getPartsDate(date: "2018-12-40", type: "month") // 입력값의 date에 해당하는 값이 잘못사용되었습니다. 확인해주세요.

 

Converting Errors to Optional Values

try?

try? 를 사용하여 do ~ catch 구문 없이 오류 처리 가능
정상 수행 시 Optional 값 반환, 오류 발생 시 nil 반환

 

func parseDate(param: NSString) throws -> Date { }
do {
    let date = try parseDate(param: date)
	    ...
} catch DateParseError.overSizeString {
		...
} catch {
	
}
try? parseDate(param:date)

위와같은 throws메서드를 호출해야될때
try? 또는 try! 를 사용하면 do ~ catch 없이도 가능합니다.

 

try!

do ~ catch 구문 없이 throws 메서드 처리 가능하지만 오류 발생 시 앱 Crash 오류가 발생하지 않는다고 확신할 수 있는 경우에만 try! 사용
e.g. 앱 번들에 함께 제공되는 이미지 로드 등

 

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

 

'Swift' 카테고리의 다른 글

[Swift] Magnitude, Abs  (0) 2019.07.24
[Swift] 서브스크립트(Subscript)  (0) 2019.07.24
[Swift] 타입캐스팅(Type Casting)  (0) 2019.07.22
[Swift] 타입체크(TypeCheck)  (0) 2019.07.22
[Swift] Closure - 3 (NoEscaping, Escaping, AutoClosure)  (0) 2019.07.22