안녕하세요! 오늘은 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 베타 버전에서는 동일한 에러 타입이 발생하는 경우에도
error
가any Error
로 나오는 문제가 있을 수 있습니다. 이는 향후 업데이트에서 수정될 것으로 예상됩니다.
Typed throws의 필요성
왜 Typed throws가 필요할까요? 몇 가지 중요한 이유를 살펴보겠습니다.
1. 에러 정보 손실 방지
기존의 throws
는 Result
나 Task
와 달리 에러 타입 정보를 명시적으로 전달하지 않았습니다:
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의 장점
- 에러 처리의 명확성: 함수가 어떤 종류의 에러를 던질 수 있는지 명확히 알 수 있어 코드의 가독성과 안정성이 향상됩니다.
- 성능 향상:
any Error
는 런타임에 타입 정보를 확인해야 하므로 오버헤드가 발생할 수 있습니다. Typed throws는 컴파일 타임에 에러 타입이 결정되므로 이러한 오버헤드가 감소합니다. - 타입 안전성: 컴파일러가 에러 타입을 체크하므로 런타임 에러의 가능성이 줄어듭니다.
언제 Typed throws를 사용해야 할까?
Swift 공식 제안에 따르면, 대부분의 상황에서는 여전히 기존의 throws
(즉, throws(any Error)
)가 더 적합할 수 있습니다. Typed throws는 다음과 같은 특정 상황에서 유용합니다:
- 에러 조건이 고정되어 있는 경우: 함수가 던질 수 있는 에러 유형이 명확하게 정해져 있을 때
- 같은 모듈이나 패키지 내에서 발생하는 에러를 다룰 때: 모든 관련 코드를 제어할 수 있는 경우
- 독립적인 라이브러리에서 발생하는 에러를 다룰 때: API 경계를 명확히 하고 싶을 때
결론
Typed throws는 Swift의 에러 처리 메커니즘을 크게 개선한 중요한 기능입니다. 이를 통해 더 명확하고 안전한 코드를 작성할 수 있으며, 특히 에러 처리가 중요한 복잡한 애플리케이션에서 큰 도움이 될 것입니다.
하지만 모든 상황에서 Typed throws를 사용해야 하는 것은 아니며, 프로젝트의 특성과 요구사항에 따라 적절히 선택하는 것이 중요합니다. 때로는 기존의 throws
가 더 간결하고 유연한 해결책이 될 수 있습니다.
Swift의 발전과 함께 우리의 코드도 더욱 견고해지길 바랍니다! 궁금한 점이나 추가 정보가 필요하시면 언제든지 댓글로 남겨주세요.
참고 자료: