Enum과 Error에 대하여

19786 단어 swiftswift

오늘은 Enum과 Error에 대해 얘기해 보려 한다.
그러기 위해서는 내가 어떤식으로 Error를 처리해 왔었는지 따라 가보자.


Enum과 rawValue

처음에 Error를 처리할때 어떻게 처리할까 생각이 많았다.

Error는 같은 타입이니까 하나로 뭉쳐져 있으면 좋겠다!
Error가 하나로 뭉쳐져 있으니 관련 Error Message도 그 곳에서 가져올 수 있으면 좋겠다!

라는 생각의 흐름으로 아래와 같은 코드가 나왔다.

enum SignUpError: String {
    case getTxext: "텍스트를 가져오는데 문제가 있습니다.\n잠시 후 다시 시도해주세요."
    case isNotValidText: "유효하지 않은 텍스트입니다.\n올바르게 입력해주세요."
    case isNotValidEmail: "유효하지 않은 이메일 주소입니다.\n올바르게 입력해주세요."
}

Enum으로 Error를 하나로 뭉쳤고, Error Message도 한 곳에서 가져올 수 있도록 rawValue를 사용했다.

Error Protocol에 대해 알아보는건 어떻냐는 피드백을 받았다.

(고마워요 붱이🦉)


Error Protocol

Apple 공식 문서를 확인해보자.

Error 프로토콜을 준수하다면 어떤 타입이든 Swift의 에러 핸들링 시스템에서 에러를 표시할 수 있다고 한다.
그리고 그 밑으로 Enum으로 Error를 처리하는 예시들이 나와있다.
하나씩 따라가보자.


Error protocol & Enum

Error protocol을 준수하고 error가 발생할 수 있는 상황을 case로 정의한 열거형은 error를 표현하기에 아주 알맞다고 한다.
만약 error에 관한 추가적인 세부사항들이 있다면 정보를 포함한 associated value를 사용하면 된다.

코드를 통해 하나씩 알아가보자.

예제

enum IntParsingError: Error {
    case overflow
    case invalidInput(Character)
}

이전 포스팅: Protocol에서 프로토콜은 열거형에서도 채택할 수 있다고 했다.
Error 프로토콜을 채택한 IntParsingError는 String을 Int로 변환하는 중 발생할 수 있는 Error를 정리해뒀다.
overflow는 변환할 String의 길이가 너무 길어 Int 자료형의 MAX값을 넘어가는 경우의 Error이다.
invalidInput은 숫자가 아닌 character를 입력 받은 경우의 Error이다.

extension Int {
    init(validating input: String) throws {
        // ...
        let c = _nextCharacter(from: input)
        if !_isValid(c) {
            throw IntParsingError.invalidInput(c)
        }
        // ...
    }
}

if !_isValid(c) {...}에서 invalidInput Error를 throw 하고 있다.

do {
    let price = try Int(validating: "$100")
} catch IntParsingError.invalidInput(let invalid) {
    print("Invalid character: '\(invalid)'")
} catch IntParsingError.overflow {
    print("Overflow error")
} catch {
    print("Other error")
}

Int 이니셜라이저에서 Error를 throws하므로 try로 호출해준다.
catch문에서 throw된 Error를 case 별로 구분하여 다른 print문을 출력하는 것을 확인할 수 있다.

정리

만약 error에 관한 추가적인 세부사항들이 있다면 정보를 포함한 associated value를 사용하면 된다.

그렇다면 위의 얘기를 다시 한 번 생각해보자.

이 부분은 예제에서 invalidInput Error case를 통해 확인할 수 있다.
case invalidInput(Character)에서 Character 타입의 associated value를 정의해뒀다.
associated value

if !_isValid(c) {
	throw IntParsingError.invalidInput(c)
}

위의 코드처럼 어떤 문자에서 Error가 발생했는지 넘겨준다.

catch IntParsingError.invalidInput(let invalid) {
    print("Invalid character: '\(invalid)'")
}

전달 받은 invalid 문자는 catch문에서 위와 같이 가져다 사용할 수 있다.

including More Data in Errors

위에서 알아본 예제는 invalidInput에서만 추가적인 data를 처리해줬다.
만약 모든 error case에서 공통적으로 추가적인 data가 필요하다면 어떻게 처리할 수 있을까?

예제

struct XMLParsingError: Error {
    enum ErrorKind {
        case invalidCharacter
        case mismatchedTag
        case internalError
    }

    let line: Int
    let column: Int
    let kind: ErrorKind
}

func parse(_ source: String) throws -> XMLDoc {
    // ...
    throw XMLParsingError(line: 19, column: 5, kind: .mismatchedTag)
    // ...
}

Error프로토콜을 채택하는 XMLParsingError 구조체를 정의했다.

위의 예제와는 다르게 struct에 Error 프로토콜을 채택했다.
ErrorKind 열거형을 통해 error case들을 나누고 있다.
line, column, kind 프로퍼티들을 정의해뒀다.

parse(_:)XMLParsingError인스턴스를 throw한다.
XMLParsingError 구조체가 Error protocol을 채택하고 준수하고 있으므로 throw 할 수 있다.
구조체는 이니셜라이저를 정의하지 않아도 저장 프로퍼티에 대해서는 자동으로 이니셜라이저가 지정된다.

do {
    let xmlDoc = try parse(myXMLData)
} catch let e as XMLParsingError {
    print("Parsing error: \(e.kind) [\(e.line):\(e.column)]")
} catch {
    print("Other error: \(error)")
}

catch문으로 throw된 Error를 처리한다.
print문을 보면 throw된 Error가 XMLParsingError인스턴스이므로 프로퍼티를 가져다 사용할 수 있다.


LocalizedError

LocalizedError도 프로토콜이다.
오류와 오류가 발생한 이유를 설명하는 메시지를 제공한다.

위와 같은 프로퍼티들이 정의되어 있다.

errorDescription은 이름에서 알 수 있듯 오류에 대한 설명을 정의하는 프로퍼티이다.

나머지 프로퍼티들도 직관적인 이름으로 설명하지 않아도 무엇인지 알 수 있다.

Enum과 Protocol

그렇다면 위에서 알아본 내용을 토대로 처음 작성한 Error 코드를 바꿔보자.

enum SignUpError: Error {
    case getTxext
    case isNotValidText
    case isNotValidEmail
}

extension SignUpError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .getTxext:
            return "텍스트를 가져오는데 문제가 있습니다.\n잠시 후 다시 시도해주세요."
        case .isNotValidText:
            return "유효하지 않은 텍스트입니다.\n올바르게 입력해주세요."
        case .isNotValidEmail:
            return "유효하지 않은 이메일 주소입니다.\n올바르게 입력해주세요."
        }
    }
}

추가적인 data가 필요하지 않아 enum에서 Error 프로토콜을 채택했다.
error message를 가져오기 위해 LocalizedError 프로토콜의 errorDescription 프로퍼티를 사용했다.
SignUpError 열거형에서 Error, LocalizedError 프로토콜들을 복수로 채택해도 되지만 가독성과 관심사를 분리해주고 싶어 extension으로 분리해줬다.
(Error 프로토콜에서는 error case만 관리하고, LocalizedError 프로토콜에서는 error message만 관리하도록)

func signUp() throws {
    throw SignUpError.getText
}

func test() {
    do {
        try signUp()
    }
    catch let error as SignUpError {
        print(error.errorDescription)
    } catch {
        print("Other error: \(error)")
    }
}

위와 같은 코드로 Error를 사용할 수 있다.
LocalizedError 프로토콜을 사용해 error message를 지정해줘서 error.errorDescription을 사용할 수 있다.


첫번째 코드와 비교해보기

왼쪽이 첫번째 코드이고, 오른쪽이 Error 프로토콜을 사용해 수정한 코드이다.
오른쪽과 같이 수정한 이후 아래의 피드백을 받았다.

열거형에 프로퍼티를 저장해보셨군요!
이렇게 만드는 것과 열거형에 rawValue로 문자열을 저장하는 것과 어떤 차이가 있을까요?

(고마워요 도미닉🔥)

두개를 비교해보고 질문에 답을 해보자.

우선 오른쪽 코드 Error 정의부가 더 길고 복잡하다.
그렇다면 Error를 던지고 받아서 처리하는 부분을 봐보자.

왼쪽

signUp()에서 에러가 발생하면 SignUpError enum type을 반환해주고 있다.
에러가 발생하지 않고 정상적으로 function을 끝냈다면 nil을 반환하고 있다.

그렇다면 signUp()을 호출한 test()를 살펴보자.
에러가 발생했는지 확인하기 위해 error라는 변수를 하나 선언해주고, errornil이 아니라면 error message를 출력해주고 있다.

오른쪽

signUp()에서 에러가 발생하면 에러를 throw해준다.
test()에서는 do-catch문을 통해 에러를 받고, 에러 타입에 따라 error message를 출력해주고 있다.

Answer

왼쪽보다 오른쪽이 Error를 처리하는데 더 Cool하다.

왼쪽은 에러가 발생하지 않는 경우 Error가 아닌 무언가를 return해줘야하는데 nil을 return한다면 function의 반환 타입이 optional이 되어 버린다.
(optional이 반환되면 바인딩해주는 후처리가 필요하므로 depth가 깊어진다.)

오른쪽은 에러가 발생하면 do-catch로 해당 Error를 잡고 거기서 처리해주면 된다.

또한, error case가 추가되는 상황을 생각해보자.
rawValue로 error message를 설정한 경우, rawValue를 지정하지 않아도 빌드 상으로는 문제가 없다.


마무리

오늘은 Enum과 Error에 대해서 알아봤다.
Error 프로토콜을 사용해 에러 처리를 하고 있었지만 오늘 정리해보니 좀 더 명확해지는거 같다.
그럼 이만👋

좋은 웹페이지 즐겨찾기