[iOS/Swift] Typed throws - Swfit6에서의 에러처리 패턴 개선사항

Swift에서 Error를 Handling하는 방법으로 do try - catch 패턴을 사용하곤 한다.

하지만, Swift5에서는 이 패턴을 이용할 때 Error타입과 관련해서 아쉬운 점이 존재했다.

 

이번 글에서는 Swift5에서의 에러처리 패턴의 구조와, Swfit6에서 개선된점을 소개하겠다.

 


기존의 에러처리 패턴

문자열의 유효성을 검증하는 로직이 있다고 가정할 때 다음과 같이 에러를 처리할 수 있을것이다.

 

  • 열거형으로 Error 정의
enum ValidationError: Error {
    case emptyString
    case isNotInt
    case isNotDate
}
  • 이렇게 Error 프로토콜을 채택한 열거형을 생성하고, 컴파일러는 이 세 가지 경우를 제외하고는 성공으로 간주하게 된다.

 

  • throwing function 생성 ⇒ 에러를 던지는 함수
// 정상적인 상황에선 Bool을 반환하는데, 오류가 있을 경우 throw를 통해 던지겠구나!?
// 를 함수의 내용을 보지 않고 선언부만 봐도 추측할 수 있음
func validateUserInputError(text: String) throws -> Bool {
    // 입력한 값이 빈값인지 아닌지
    guard !(text.isEmpty) else {
        print("빈 값")
        throw ValidationError.emptyString
    }
    
    // 입력한 값이 숫자인지 아닌지
    guard Int(text) != nil else {
        print("숫자가 아닙니다")
        throw ValidationError.isNotInt
    }
    
    // 입력한 값이 날짜형태로 변환이 되는 숫자인지 아닌지
    guard checkDateFormat(text: text) else {
        print("날짜 형태가 잘못되었습니다")
        throw ValidationError.isNotDate
    }
    
    return true
}
  • 에러를 발생시킬 수 있다는 것을 알리기 위해 throw 키워드를 함수 선언부의 파라미터 뒤에 붙임

 

  • do try - catch구문을 사용해서 에러를 다루기
do {
    try validateUserInputError(text: "20220120")
} catch ValidationError.emptyString {
    print("빈값이유")
} catch ValidationError.isNotInt {
    print("숫자가 아니유")
} catch ValidationError.isNotDate {
    print("날짜가 아니유")
}
  • do 블럭에서 error를 발생시킬 수 있는 함수를 실행하고, 만약 error가 던져진다면 catch구문으로 넘어가게 된다
  • 그러나, error가 많아질 경우 하나의 catch구문으로 처리하는게 더 효율적일 수 있다

 

  • 하나의 catch구문과 switch문으로 처리
do {
    try validateUserInputError(text: "3454354543")
} catch {
    // Error 타입을 구체적으로 정의할 수 없어서 타입캐스팅을 활용해서 처리
    switch error as? ValidationError {
    case .emptyString:
        print("빈값이유")
    default:
        print("나머지 오류처리")
    }
}
  • 여기서 switch문으로 error를 처리할 때 Error 타입을 구체적으로 정의할 수 없어서 타입캐스팅을 통해 처리했다
  • 그 이유는, Swift5에서는 모든 에러가 Error 프로토콜을 따르는 형태여서, 만약 Error프로토콜을 따르는 에러가 여러개라면 그 중에서 어떤 타입으로 정의할지 컴파일러는 알지 못하기 때문에 발생하는 아쉬운점이었다

Typed throws - Swift6에서의 에러처리 패턴

  • 에러를 발생시킬 수 있는 함수에서 타입을 구체적으로 정의
func validateUserInputError(text: String) throws(ValidationError) -> Bool {
    guard !(text.isEmpty) else {
        print("빈 값")
        // 원래는 ValidationError.emptyString이었음
        throw .emptyString
    }
    
    guard Int(text) != nil else {
        print("숫자가 아닙니다")
        throw .isNotInt
    }
    
    guard checkDateFormat(text: text) else {
        print("날짜 형태가 잘못되었습니다")
        throw .isNotDate
    }
    
    return true
}
  • throw함수를 정의할 때 `throws(에러타입)`이런식으로 어떤 에러 타입을 다룰것인지를 정의할 수 있다
  • 이렇게 하면 각 에러가 발생하는 시기에 `에러타입.특정에러` 이렇게 매번 열거형을 지정해줬어야 됐었던 반면, 이제 어떤 에러 타입인지 위에서 정의되어있기 때문에 `.특정에러` 이렇게 축약해서 사용할 수 있다

 

  • do try - catch 구문에서의 사용
do {
    try validateUserInputError(text: "3454354543")
} catch {
    // 원래는 error는 Error 타입이었는데, 위와같이 tayped throws를 적용하면
    // 현재 error 는 ValidationError타입이 됐음
    switch error {
    // 여기도 원래는 ValidationError.emptyString으로 썼어야 헀는데 아래와같이 개선 가능
    case .emptyString:
        print("빈값이유")
    default:
        print("나머지 오류처리")
    }
}
  • do try - catch 구문에서도 에러타입을 축약해서 사용할 수 있다
  • do 블럭에서 시도하는 throw함수를 통해 어떤 에러가 발생할 수 있는지 미리 알고, catch구문에서는 그에 맞는 에러만 처리할 수 있기 때문이다

Typed throws의 이점과 한계

기존의 에러처리 문법에서 에러 타입이 지정되지 않은걸 Untyped throws라고 하는데, 이는 곧 any Error 유형의 Typed throws와 같다고 볼 수 있다.

func validateUserInputError(text: String) throws -> Bool {
	// ...
}

func validateUserInputError(text: String) throws(any Error) -> Bool {
	// ...
}

 

그러니까 이 둘은 같은 의미라는 것이다.

 

이렇게 any Error타입의 경우 런타임 시점에 어떤 타입인지 확인해야 하기 때문에 메모리 측면에서 비효율적일 수 있다.

 

그래서 Typed throws를 통해 에러 타입을 명시해주는 것은, 코드 작성시 타입을 생략할 수 있다는 이점 외에도,

컴파일 시점에 에러 타입이 결정되므로 메모리 측면에서도 이점을 얻을 수 있다!

 

 

 

그러나, 사실 대부분의 경우에는 Untyped Throws를 사용하는게 더 나을수도 있다. (아래 링크 참고)

 

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md#treat-all-uninhabited-thrown-error-types-as-nonthrowing

 

swift-evolution/proposals/0413-typed-throws.md at main · swiftlang/swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

github.com

Typed Throws를 제안한 제안서에 따르면,

throw될 수 있는 에러가 추후에 바뀔 수 있는 경우에는 오히려 Untyped Throws를 사용하는 것이 더 나을 수 있다고 한다.

 

특히 public API를 사용하거나, 라이브러리 코드의 경우에 Typed Throws를 사용하는 경우에, 에러타입을 변경할 수 없는 Typed throws를 사용하면 유연성에서 제약이 생길 수 있다.

 

그래서, 특정한 상황(상대적으로 에러 조건이 고정되어있는경우. 같은 모듈이나 패키지에서 발생하는 에러 / 독립적인 라이브러리에서 발생하는 에러를 다루는 경우)이 아니면 대부분의 경우에는 Untyped throws를 사용하는것이 더 낫다.


※참고자료