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를 사용하는게 더 나을수도 있다. (아래 링크 참고)
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를 사용하는것이 더 낫다.
※참고자료
'iOS > Swift' 카테고리의 다른 글
[iOS/Swift] 책임분리를 위한 설계 - DTO와 Entity에 대해 (0) | 2025.03.15 |
---|---|
[iOS/Swift] Realm의 데이터 삭제하기 (0) | 2025.03.12 |
[iOS/Swift] MVVM의 한계에 대한 고찰 - Massive View Model 해결법 (0) | 2025.02.11 |
[iOS/Swift] ARC - Swift의 메모리 관리 기법 그런데 이제 weak을 곁들인 (0) | 2025.02.08 |
[iOS/Swift] 커스텀 Observable와 bind메서드에 대한 고찰 (0) | 2025.02.04 |