Error vs LocalizedError: Swift 에러 처리, 어떤 걸 선택할까?
네트워크 에러를 타입으로 관리할 때는 아래와 같이 Error 타입을 채택하고 description 프로퍼티를 만들어 사용했었다
이전 프로젝트에서 네트워크 쪽을 담당해주셨던 팀원분이 Error 대신 LocalizedError를 채택해 에러 타입을 정의하고 해당 프로토콜의 errorDescription 프로퍼티를 활용하신 것을 보고 어떤 차이가 있는지 알아보고자 한다
지금까지 내가 사용했던 방식의 코드이다
enum NetworkError: Error {
case invalidURL(url: String)
case invalidResponse
case decodingError(message: String)
case serverError(statusCode: Int)
case networkFailure(message: String)
var description: String {
switch self {
case .invalidURL(let url):
"invalidURL: \\(url)"
case .invalidResponse:
"invalidResponse"
case .decodingError(let message):
"decodingError: \\(message)"
case .serverError(let statusCode):
"serverError - StatusCode: \\(statusCode)"
case .networkFailure(let message):
"networkFailure: \\(message)"
}
}
}
Error 프로토콜을 채택하고 각 케이스별로 에러를 정의하고 description 프로퍼티에 에러 설명을 넣는 방식이었다
지난 프로젝트에서 팀원분이 사용하셨던 방식이다
enum FSError: LocalizedError {
case invalidURL(urlString: String)
case httpError(statusCode: Int)
case noData
case encodingFailed(message: String)
case decodingFailed(message: String)
case unknownError(message: String)
var errorDescription: String? {
switch self {
case .invalidURL(let urlString):
return "invalidURL: \\(urlString)"
case .httpError(let statusCode):
return "HTTPError: \\(statusCode)"
case .noData:
return "NoData"
case .encodingFailed(let message):
return "EncodingFailed: \\(message)"
case .decodingFailed(let message):
return "DecodingFailed: \\(message)"
case .unknownError(let message):
return "UnknownError: \\(message)"
}
}
}
여기서 궁금했던 부분은 두 가지이다
- Error 프로토콜을 채택하는 것이 아니라 LocalizedError 프로토콜을 채택했다. 어떤 차이가 있을까?
- errorDescription의 타입이 옵셔널이다?
그럼 우선 LocalizedError가 무엇인지 살펴보자
LocalizedError
LocalizedError는 사용자에게 보여줄 수 있는 친숙한 에러 메시지를 제공하기 위해 사용하는 프로토콜이다. 디버깅 목적보단 사용자 친화적인 메시지를 만들기 위한 용도로 사용한다
LocalizedError는 Error 프로토콜을 채택하고 있으며 네 가지의 인스턴트 프로퍼티를 가진다
- errorDescription: 일반적인 에러 메시지로 사용한다(ex. “서버에 연결할 수 없습니다.”)
- failureReason: 에러가 발생한 원인을 보여준다 (ex. “네트워크 연결이 끊겼습니다”)
- recoverySuggestion: 사용자에게 에러를 어떻게 해결하면 좋은지 권유한다 (ex. “Wi-Fi를 확인하세요”)
- helpAnchor: 도움말 링크 등 사용자가 도움을 받을 수 있는 방법을 알려준다
대부분의 경우 일반적인 에러 메시지를 표시하는 errorDescription 프로퍼티만 사용하고 나머지 프로퍼티는 상황에 따라 선택적으로 사용한다
errorDescription의 타입이 옵셔널인 이유?
LocalizedError 프로토콜 내부에서 errorDescription이 옵셔널(String?)로 선언되어 있기 때문에, 구현하지 않아도 에러가 발생하지 않는다. 필요한 경우에만 해당 프로퍼티를 구현해줄 수 있게 설계된 것이다.
즉, 내가 직접 만든 description 프로퍼티와는 달리, Swift의 에러 처리 흐름에 자연스럽게 통합되도록 설계된 공식적인 방식이었다
그럼 LocalizedError 프로토콜의 errorDescription 프로퍼티를 사용할 때 어떤 장점이 있나?
1. error.localizedDescription 사용 시 자동으로 메시지 출력
LocalizedError를 채택하고 errorDescription을 구현하면, error.localizedDescription을 호출할 때 자동으로 해당 errorDescription 값이 반환된다. 이는 Swift의 Error 타입이 Objective-C의 NSError와 호환되도록 Error가 내부적으로 NSError로 브리징되며, 이 과정에서 errorDescription이 localizedDescription으로 매핑되기 때문이다
Swift의 localizedDescription 호출 흐름을 살펴보자
https://forums.swift.org/t/confusing-error-localizeddescription-output/5337/6
- print(error.localizedDescription) → 내부적으로 NSError로 브리징 시도.
- Swift는 Error를 NSError로 자동 변환.
- 이때 LocalizedError를 채택한 경우, errorDescription을 NSError.localizedDescription의 값으로 사용함.
LocalizedError 을 채택하여 errorDescription을 구현한 경우, NSError 내부적으로 NSLocalizedDescriptionKey에 해당하는 value로 localizedError.errorDescription의 값을 넣는 것을 확인할 수 있다
따라서, LocalizedError를 채택하고 errorDescription 을 구현하면, error.localizedDescription 에서 동일한 값을 사용할 수 있는 것이다
✅ 확인 실험
2. 국제화 및 지역화 지원
LocalizedError의 errorDescription을 NSLocalizedString과 함께 사용하면, 다국어 지원이 용이해진다. 다양한 언어를 사용하는 사용자들에게 적절한 에러 메시지를 쉽게 제공할 수 있다.
이 부분은 다국어 지원을 직접 지원해보진 않아서 공감되지는 않았지만, 기회가 되면 사용해볼 예정이다.
var errorDescription: String? {
return NSLocalizedString("SERVER_ERROR", comment: "서버 오류")
}
3. 추가적인 에러 정보 제공
위 설명과 같이 LocalizedError는 errorDescription 외에도 failureReason, recoverySuggestion, helpAnchor 등의 프로퍼티를 제공하기 때문에 사용자에게 더 풍부한 에러 정보를 제공할 수 있다 → UX 향상 가능
var failureReason: String? {
return "서버에서 잘못된 응답을 보냈습니다."
}
var recoverySuggestion: String? {
return "잠시 후 다시 시도해주세요."
}
Error vs LocalizedError 어떤 것을 채택할까?
Error는 Swift에서 모든 에러 타입이 채택해야 하는 기본 프로토콜로 에러 메시지를 사용자에게 보여주지 않고, 로직 분기로만 쓰는 경우, 디버깅만을 목적으로 둔 경우는 Error만으로도 충분하다
LocalizedError는 Error 프로토콜에서 사용자가 이해할 수 있는 에러 메시지를 제공하는 기능을 추가한 프로토콜이기 때문에 사용자에게 메시지를 명확하게 보여줘야 할 때는 Error 프로토콜 보단, LocalizedError 프로토콜을 채택하는 게 올바른 방법이다
ETC) 디버깅용 에러 메시지
현재 진행 중인 프로젝트에서는 사용자에게 에러 메시지를 출력하기 때문에 아래와 같이 LocalizedError를 채택하여 errorDescription을 구현해줬다. 이는 사용자에게 보여주는 에러메시지이기 때문에 내부적으로 어떤 에러가 발생했는지를 구체적으로 담지 않는다. 따라서, 디버깅용 에러 메시지도 함께 정의해줘야 한다
enum NetworkError: LocalizedError {
case invalidURL(url: String)
case invalidResponse
case decodingError(error: Error)
case serverError(statusCode: Int)
case networkFailure(error: Error)
}
// 사용자 메시지 정의
extension NetworkError {
var errorDescription: String? {
switch self {
case .invalidURL:
return "요청에 문제가 발생했어요.\\n 다시 시도해주세요."
case .invalidResponse:
return "서버로부터 올바른 응답을 받지 못했어요.\\n 잠시 후 다시 시도해주세요."
case .decodingError:
return "데이터 처리 중 오류가 발생했어요.\\n 다시 시도해주세요."
case .serverError(let statusCode):
return "서버 오류가 발생했어요.\\n (오류 코드: \\(statusCode))"
case .networkFailure:
return "네트워크 연결에 실패했어요.\\n 인터넷 상태를 확인해주세요."
}
}
}
디버깅용 에러 메시지
debugDescription이라는 프로퍼티를 정의해 디버깅을 위한 메시지를 따로 정의했다
사용자를 위한 에러 메시지는 localizedDescription를 사용하고 로그는 debugDescription 프로퍼티를 사용했다
// 디버깅용 메시지 정의
extension NetworkError {
var debugDescription: String {
switch self {
case .invalidURL(let url):
"invalidURL: \\(url)"
case .invalidResponse:
"invalidResponse"
case .decodingError(let message):
"decodingError: \\(message)"
case .serverError(let statusCode):
"serverError - StatusCode: \\(statusCode)"
case .networkFailure(let message):
"networkFailure: \\(message)"
}
}
}
self.showNetworkErrorAlert(message: error.localizedDescription)
os_log("%@", type: .error, error.debugDescription)
사실 debugDescription 프로퍼티는 CustomDebugStringConvertible 프로토콜을 채택하여 구현하지만, 채택하여 구현할만큼 필요성을 느끼지 못해서 직접 선언해서 사용했다
CustomDebugStringConvertible 프로토콜을 채택해서 사용하면 debugPrint() 또는 po 명령어 사용 시 자동으로 debugDescription이 호출된다고는 한다
참고 자료)
https://developer.apple.com/documentation/swift/error/localizeddescription
https://developer.apple.com/documentation/foundation/nserror/localizeddescription
https://forums.swift.org/t/confusing-error-localizeddescription-output/5337/6
https://medium.com/%40ziminny/improving-error-handling-in-swift-why-use-localizederror-in-enums-instead-of-errorintrodução-a-549cf0f930af
https://developer.apple.com/documentation/objectivec/nsobjectprotocol/debugdescription