오늘은 Enum과 Error에 대해 얘기해 보려 한다.
그러기 위해서는 내가 어떤식으로 Error를 처리해 왔었는지 따라 가보자.
처음에 Error를 처리할때 어떻게 처리할까 생각이 많았다.
Error는 같은 타입이니까 하나로 뭉쳐져 있으면 좋겠다!
Error가 하나로 뭉쳐져 있으니 관련 Error Message도 그 곳에서 가져올 수 있으면 좋겠다!
라는 생각의 흐름으로 아래와 같은 코드가 나왔다.
enum SignUpError: String {
case getTxext: "텍스트를 가져오는데 문제가 있습니다.\n잠시 후 다시 시도해주세요."
case isNotValidText: "유효하지 않은 텍스트입니다.\n올바르게 입력해주세요."
case isNotValidEmail: "유효하지 않은 이메일 주소입니다.\n올바르게 입력해주세요."
}
Enum
으로 Error를 하나로 뭉쳤고, Error Message도 한 곳에서 가져올 수 있도록 rawValue
를 사용했다.
(고마워요 붱이🦉)
Apple 공식 문서를 확인해보자.
Error
프로토콜을 준수하다면 어떤 타입이든 Swift의 에러 핸들링 시스템에서 에러를 표시할 수 있다고 한다.
그리고 그 밑으로 Enum
으로 Error를 처리하는 예시들이 나와있다.
하나씩 따라가보자.
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문에서 위와 같이 가져다 사용할 수 있다.
위에서 알아본 예제는 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
도 프로토콜이다.
오류와 오류가 발생한 이유를 설명하는 메시지를 제공한다.
위와 같은 프로퍼티들이 정의되어 있다.
errorDescription
은 이름에서 알 수 있듯 오류에 대한 설명을 정의하는 프로퍼티이다.
나머지 프로퍼티들도 직관적인 이름으로 설명하지 않아도 무엇인지 알 수 있다.
그렇다면 위에서 알아본 내용을 토대로 처음 작성한 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
라는 변수를 하나 선언해주고, error
가 nil
이 아니라면 error message를 출력해주고 있다.
signUp()
에서 에러가 발생하면 에러를 throw해준다.
test()
에서는 do-catch
문을 통해 에러를 받고, 에러 타입에 따라 error message를 출력해주고 있다.
왼쪽보다 오른쪽이 Error
를 처리하는데 더 Cool하다.
왼쪽은 에러가 발생하지 않는 경우 Error
가 아닌 무언가를 return해줘야하는데 nil
을 return한다면 function의 반환 타입이 optional
이 되어 버린다.
(optional이 반환되면 바인딩해주는 후처리가 필요하므로 depth가 깊어진다.)
오른쪽은 에러가 발생하면 do-catch
로 해당 Error
를 잡고 거기서 처리해주면 된다.
또한, error case가 추가되는 상황을 생각해보자.
rawValue
로 error message를 설정한 경우, rawValue
를 지정하지 않아도 빌드 상으로는 문제가 없다.
오늘은 Enum과 Error에 대해서 알아봤다.
Error 프로토콜을 사용해 에러 처리를 하고 있었지만 오늘 정리해보니 좀 더 명확해지는거 같다.
그럼 이만👋