Swift

[Swift] Typed throws

Zedd0202 2024. 7. 21. 17:59
반응형

 

 Xcode 16 릴리스 노트를 보다가...새롭게 알게 된 내용이 있어서 간단히 정리!! 

 

# Typed throws

그냥 한마디로

enum MyError: Error {
    case invalid
}

func foo() ➡️throws(MyError)⬅️ -> String { ... }

이런식으로 throws할 때 에러 타입을 지정할 수 있게 되는 것 같다. 

위 foo메소드는 String을 리턴하거나, 오직 MyError타입의 에러만 throw할 수 있다

enum OtherError: Error {
    case 저쩌구
}
    
    
func foo() throws(MyError) -> String {
    do {
        try ~~~~~~
    } catch {
        throw OtherError.저쩌구 // 🔴 Thrown expression type 'ViewController.OtherError' cannot be converted to error type 'ViewController.MyError'
    }
}

만약 MyError가 아닌 타입을 throw하려고 하면 컴파일 에러가 발생하게 된다.

(위 예제에서는 MyError가 아닌 OtherError를 throw하려고함) 

 

기존에 사용하던것 처럼 타입을 지정하지 않으면 

func bar() throws -> String { }

throws(any Error)와 동일하다. (모든 에러타입을 넘기기 가능) 

 

그리고..Typed throws추가되면서 do throws문도 추가된 것 같다..?!

# do throws(에러타입) 

enum MyError: Error {
    case invalid
}

do throws(MyError) {
    if true {
        throw .invalid
    }
} catch {
    // error는 MyError로 추론됨
}

do throws(Error타입) ⬅️ 이런식으로 쓸 수 있는 것 같다. 

catch블록 내의 error는 당연히 해당 에러타입이 된다! 

 

# 에러타입 추론 

그리고 do 블록 내의 모든 throw문에서 발생하는 에러타입이 동일할경우

do /*infers throws(MyError)*/ {
    try foo()  // throws MyError
    if true {
        throw MyError.invalid  // throws MyError
    }
} catch {
    // implicit 'error' value has type MyError
}

catch블록에서 error는 해당 에러타입으로 추론된다고 한다.

(do throws(동일하게 발생하는 에러타입)으로 추론하는듯) 

⚠️ 근데 내 Xcode 16베타 && catch블록에서 error가 any Error로 나온다.. swift 6모드 켜도!! 흠 이건 수정될듯

 

만약 do 블록 내에서 발생하는 에러타입이 다를경우 catch블록내에서 error의 타입은 any Error로 추론된다. 

do /*infers throws(any Error)*/ {
  try callCat() // throws CatError
  try callKids() // throw KidError
} catch {
  // implicit 'error' variable has type 'any Error'
}

아래에는 이 Typed throws의 필요성?에 대해 Proposal에 나와있는 내용인데,

아주 간단히 왜 Typed throws가 필요하고 어떤 경우에 유용한지 정리해봤다.

Proposal에 더 정확히 나와있으니 참고하길!! 

 

아래와 같은 에러타입이 있다고 가정할때 

enum CatError: Error {
    case sleeps
    case sitsAtATree
}

기존의 throws는 Result와 Task에 비해서 더 적은 에러 정보를 전달하게 되는데,

func callCat() -> Result<Cat, CatError>
func callFutureCat() -> Task<Cat, CatError>
func callCatOrThrow() throws -> Cat

throws 메소드를 호출하는 코드에서 에러를 Result나 Task로 변환할 때 타입 정보가 손실되고,

이 손실된 정보는 명시적인 캐스팅을 통해서만 복원할 수 있기 때문이다. 

 

이게 무슨소리냐!!

enum CatError: Error {
    case sleeps
    case sitsAtATree
}

func callCatOrThrow() throws -> Cat {
    if Bool.random()  {
        return Cat()
    } else {
        throw CatError.sitsAtATree
    }
}

뭐 이런 코드가 있다고 했을때 

func callAndFeedCat1() -> Result<Cat, CatError> {
    do {
        return Result.success(try callCatOrThrow())
    } catch {
        return Result.failure(error) // 🔴 won't compile, because error type guarantee is missing in the first place

    }
}

do 에서 callCatOrThrow()를 호출해도 callCatOrThrow는

func callCatOrThrow() throws -> Cat { ... }

에러타입이 정해져있지 않고 모든 에러를 throw할 수 있기 때문에 (any Error) 

return Result.success(try callCatOrThrow())

이 시점에서 에러 타입의 정보가 손실되게 된다. 

근데!! 

func callAndFeedCat1() -> Result<Cat, CatError> {

callAndFeedCat1는 Error타입이 CatError로 지정되어있기 때문에 

func callAndFeedCat1() -> Result<Cat, CatError> {
    do {
        return Result.success(try callCatOrThrow())
    } catch {
        return Result.failure(error) // 🔴 won't compile, because error type guarantee is missing in the first place

    }
}

CatError에 대한 정보가 손실된 상태(any Error) 캐스팅 없이 error를 던지면 컴파일에러가 나게 된다.

 

아 ㅇㅋ....;;; 하고 CatError로 캐스팅을 아래와 같이 했다고 해보자 

func callAndFeedCat2() -> Result<Cat, CatError> {
    do {
        return Result.success(try callCatOrThrow())
    } catch let error as CatError {
        // compiles
        return Result.failure(error)
    } catch {
        return Result.failure(error)
        // 🔴 won't compile, because exhaustiveness can't be checked by the compiler
        //  so what should we return here?
    }
}

이렇게 되도 마지막 catch문에서 컴파일에러가 나게 된다(any Error타입이기 때문)

 

마지막 catch문에서 CatError중 하나를 아무거나 리턴하게 해도 되고 

enum CatError: Error {
    case sleeps
    case sitsAtATree
    case unknownError(Error)
}

이런식으로 case를 하나 추가해서.....처리해도 되지만

아무튼 any Error를 throw하는 메소드의 에러타입이 손실된다는 점이 포인트!!!

 

하지만 Typed throws를 사용하면 

enum CatError: Error {
    case sleeps
    case sitsAtATree
}

func callCatOrThrow() throws(CatError) -> Cat {  // ⭐️ throws(CatError)로 명시함
    if Bool.random()  {
        return Cat()
    } else {
        throw CatError.sitsAtATree
    }
}

func callAndFeedCat2() -> Result<Cat, CatError> {
    do {
        return Result.success(try callCatOrThrow())
    } catch {
        return Result.failure(error) // ✅ 
    }
}

 

에러타입의 정보가 손실되지 않게 되며  Result로 변환하는 과정에서 별도의 캐스팅 없이 컴파일이 잘 된다. 

아무튼 이렇게 구체적인 에러타입을 지정할 수 있게 되면서

1. 에러 처리의 명확성을 높히고, 특정 에러 타입에 대해 철저히 처리할 수 있는 이점이 생김

2. 기존의 any Error는 런타임에 타입 정보를 확인해야하기 떄문에 메모리/성능측면에서 오버헤드가 발생할 수 있는데, Typed throws는 컴파일타임에 에러타입이 결정되므로 이러한 오버헤드가 개선될 수 있다고 한다.

Typed throws가 도입되더라도 대부분의 Swift코드에서는 기존의 throws가(any Error) 여전히 더 나은 기본 에러 처리 메커니즘으로 여겨지므로 Typed throws는 특정상황에서 사용될 때 가장 적합하다고 한다. 

 e.g. 상대적으로 에러 조건이 고정되어있는경우. 같은 모듈이나 패키지에서 발생하는 에러 / 독립적인 라이브러리에서 발생하는 에러를 다루는 경우에 유용

 

 

# 참고

Xcode 16 Beta 3 Release Notes

Typed throws

 

반응형