Swift의 새로운 기능: Typed throws 완벽 가이드

안녕하세요! 오늘은 Xcode 16과 함께 등장한 Swift의 새로운 강력한 기능인 Typed throws에 대해 자세히 알아보려고 합니다. 이 기능은 Swift의 에러 처리 메커니즘을 더욱 정교하게 만들어주는 중요한 발전입니다.

Typed throws란 무엇인가?

기존의 Swift에서는 함수가 에러를 던질 때 에러의 타입을 명시적으로 지정할 수 없었습니다. 단순히 throws 키워드만 사용해 “이 함수는 어떤 에러든 던질 수 있다”라고 표현했습니다. 하지만 Xcode 16부터는 함수가 던질 수 있는 에러 타입을 명시적으로 지정할 수 있게 되었습니다.

간단한 예를 통해 살펴보겠습니다:

enum MyError: Error {
    case invalid
}

// 새로운 문법: throws(MyError)를 사용하여 에러 타입을 지정
func foo() throws(MyError) -> String {
    if Bool.random() {
        return "Success"
    } else {
        throw MyError.invalid // MyError 타입의 에러만 던질 수 있음
    }
}

foo() 함수는 String을 반환하거나, 오직 MyError 타입의 에러만 던질 수 있습니다. 만약 다른 타입의 에러를 던지려고 하면 컴파일 에러가 발생합니다:

enum OtherError: Error {
    case somethingElse
}
    
func foo() throws(MyError) -> String {
    do {
        try someOtherFunction()
    } catch {
        // 컴파일 에러: MyError가 아닌 OtherError를 던지려고 함
        throw OtherError.somethingElse // 🔴 Thrown expression type 'OtherError' cannot be converted to error type 'MyError'
    }
}

기존 throws와의 호환성

기존처럼 에러 타입을 지정하지 않고 단순히 throws만 사용하는 것도 여전히 가능합니다:

func bar() throws -> String { /* ... */ }

이 경우 throws(any Error)와 동일하게 취급되며, 모든 에러 타입을 던질 수 있습니다.

새로운 do throws 구문

Typed throws와 함께 do throws 구문도 추가되었습니다:

enum MyError: Error {
    case invalid
}

// 새로운 do throws 구문
do throws(MyError) {
    if Bool.random() {
        throw MyError.invalid
    }
} catch {
    // error는 MyError 타입으로 추론됨
    print(error) // 타입이 MyError임이 보장됨
}

do throws(ErrorType) 형태로 사용하면, catch 블록 내의 error 변수는 자동으로 해당 에러 타입으로 추론됩니다.

에러 타입 추론

do 블록 내에서 발생하는 모든 에러 타입이 동일할 경우, Swift는 자동으로 해당 에러 타입을 추론합니다:

do /*컴파일러가 throws(MyError)로 추론*/ {
    try foo()  // throws MyError
    if true {
        throw MyError.invalid  // throws MyError
    }
} catch {
    // error 변수의 타입은 MyError로 추론됨
}

하지만 do 블록 내에서 서로 다른 에러 타입이 발생할 수 있는 경우, error 변수의 타입은 any Error로 추론됩니다:

do /*컴파일러가 throws(any Error)로 추론*/ {
    try callCat() // throws CatError
    try callKids() // throws KidError
} catch {
    // error 변수의 타입은 any Error
}

⚠️ 참고: 현재 Xcode 16 베타 버전에서는 동일한 에러 타입이 발생하는 경우에도 errorany Error로 나오는 문제가 있을 수 있습니다. 이는 향후 업데이트에서 수정될 것으로 예상됩니다.

Typed throws의 필요성

왜 Typed throws가 필요할까요? 몇 가지 중요한 이유를 살펴보겠습니다.

1. 에러 정보 손실 방지

기존의 throwsResultTask와 달리 에러 타입 정보를 명시적으로 전달하지 않았습니다:

func callCat() -> Result<Cat, CatError> // 에러 타입이 CatError로 명시됨
func callFutureCat() -> Task<Cat, CatError> // 에러 타입이 CatError로 명시됨
func callCatOrThrow() throws -> Cat // 에러 타입이 명시되지 않음 (any Error)

이로 인해 throws 함수의 결과를 Result로 변환할 때 타입 정보가 손실되는 문제가 있었습니다:

enum CatError: Error {
    case sleeps
    case sitsAtATree
}

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

func callAndFeedCat() -> Result<Cat, CatError> {
    do {
        return .success(try callCatOrThrow())
    } catch {
        // 컴파일 에러: error의 타입이 any Error이므로 CatError로 변환할 수 없음
        return .failure(error) // 🔴 won't compile
    }
}

이 문제를 해결하기 위해 명시적인 타입 캐스팅이 필요했습니다:

func callAndFeedCat() -> Result<Cat, CatError> {
    do {
        return .success(try callCatOrThrow())
    } catch let error as CatError {
        return .failure(error)
    } catch {
        // 여전히 문제: CatError가 아닌 에러는 어떻게 처리해야 할까?
        // 🔴 컴파일 에러
    }
}

2. Typed throws를 사용한 해결책

Typed throws를 사용하면 이 문제가 깔끔하게 해결됩니다:

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

func callAndFeedCat() -> Result<Cat, CatError> {
    do {
        return .success(try callCatOrThrow())
    } catch {
        // 컴파일 성공: error의 타입이 CatError로 보장됨
        return .failure(error) // ✅ 성공
    }
}

Typed throws의 장점

  1. 에러 처리의 명확성: 함수가 어떤 종류의 에러를 던질 수 있는지 명확히 알 수 있어 코드의 가독성과 안정성이 향상됩니다.
  2. 성능 향상: any Error는 런타임에 타입 정보를 확인해야 하므로 오버헤드가 발생할 수 있습니다. Typed throws는 컴파일 타임에 에러 타입이 결정되므로 이러한 오버헤드가 감소합니다.
  3. 타입 안전성: 컴파일러가 에러 타입을 체크하므로 런타임 에러의 가능성이 줄어듭니다.

언제 Typed throws를 사용해야 할까?

Swift 공식 제안에 따르면, 대부분의 상황에서는 여전히 기존의 throws(즉, throws(any Error))가 더 적합할 수 있습니다. Typed throws는 다음과 같은 특정 상황에서 유용합니다:

  1. 에러 조건이 고정되어 있는 경우: 함수가 던질 수 있는 에러 유형이 명확하게 정해져 있을 때
  2. 같은 모듈이나 패키지 내에서 발생하는 에러를 다룰 때: 모든 관련 코드를 제어할 수 있는 경우
  3. 독립적인 라이브러리에서 발생하는 에러를 다룰 때: API 경계를 명확히 하고 싶을 때

결론

Typed throws는 Swift의 에러 처리 메커니즘을 크게 개선한 중요한 기능입니다. 이를 통해 더 명확하고 안전한 코드를 작성할 수 있으며, 특히 에러 처리가 중요한 복잡한 애플리케이션에서 큰 도움이 될 것입니다.

하지만 모든 상황에서 Typed throws를 사용해야 하는 것은 아니며, 프로젝트의 특성과 요구사항에 따라 적절히 선택하는 것이 중요합니다. 때로는 기존의 throws가 더 간결하고 유연한 해결책이 될 수 있습니다.

Swift의 발전과 함께 우리의 코드도 더욱 견고해지길 바랍니다! 궁금한 점이나 추가 정보가 필요하시면 언제든지 댓글로 남겨주세요.


참고 자료:

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다