ResultType
Swift5에서부터 도입된 Result Type은 Generic Enumeration으로, success(성공) 또는 failure(실패)를 나타내는 value입니다. 또한 각 case마다 associated value(연관값)을 포함할 수 있습니다.
@frozen enum Result<Success, Failure> where Failure : Error
Result의 동작방식
Writing Failable Asynchronous APIs 의 내용을 이해한 것을 바탕으로 작성했습니다.
throws 키워드를 활용하면, 작성한 내용이 실패가능성이 있음을 나타낼 수 있습니다. 하지만 비동기적으로 return하는 작업(예를 들어, API모델링 하는 작업이 비동기적으로 리턴될 때)을 처리할 땐 throws 키워드를 사용할 수 없습니다.
Result Type이 나오기 전엔 Result와 같은 enum 타입을 직접 구현해주거나, throws 활용하여 비동기작업에 대한 처리를 진행했을텐데, 해당 문서에선 비동기적으로 return하는 작업을 처리시엔 throws키워드를 사용할 수 없다고만 나와있어 왜 안되지에 대한 고민이 있었습니다. 우선 Result Type을 비동기처리과정에서 사용하는 예시 코드를 함께 살펴보고, throws키워드를 사용할 수 없는 것에 대해선 Result Type과 비교하여 뒤에서 설명해보겠습니다.
Writing Failable Asynchronous APIs 의 예시에서 completionHandler를 비동기적으로 호출하는 과정을 보여줍니다 . EntropyError라는 Error타입을 만들고 AsyncRandomGenerator 의 fetchRemoteRandomNumber 메소드에서 EntropyError를 활용합니다.
import Foundation
let queue = DispatchQueue(label: "com.example.queue")
enum EntropyError: Error {
case entropyDepleted
}
struct AsyncRandomGenerator {
static let entropyLimit = 5
var count = 0
mutating func fetchRemoteRandomNumber(completion: @escaping (Result<Int, EntropyError>) -> Void) {
let result: Result<Int, EntropyError>
if count < AsyncRandomGenerator.entropyLimit {
result = .success(Int.random(in: 1...100))
} else {
result = .failure(.entropyDepleted)
}
count += 1
queue.asyncAfter(deadline: .now() + 2) {
completion(result)
}
}
}
var generator = AsyncRandomGenerator()
// Request one more number than the limit to trigger a failure.
(0..<AsyncRandomGenerator.entropyLimit + 1).forEach { _ in
generator.fetchRemoteRandomNumber { result in // completion에 대한 구체적 구현부
switch result {
case .success(let number):
print(number)
case .failure(let error):
print("Source of randomness failed: \(error)")
}
}
}
print("Waiting on some numbers.")
dispatchMain()
위의 예시에서 좀 더 나아가 설명하자면, 주로 URLSession을 통한 비동기 네트워킹 과정에서 네트워크 시 발생할 수 있는 에러상황들에 있어 Result Type을 통해 에러핸들링을 진행합니다.
class APIManager {
let session: URLSession.shared
// Something이라는 타입에 데이터를 매칭시키고 싶은 경우 (핸들링하고 싶은 구체적 내용에 따라 completion 이 바뀝니다.)
func fetchData(completion: @escaping (Result<Something,Error>) -> Void) {
let request = URLRequest(url: "데이터를 받아오는 url")
let task: URLSessionDataTask = session.dataTask(with: request) { (data, response, error) in
guard let response = response as? HTTPURLResponse,
(200...399).contains(response.statusCode) else {
// Error 타입인 APIError의 에러명을 completion에 전달
completion(.failure(error ?? APIError.에러명))
return
}
if let data = data,
let successResponse = try? JSONDecoder().decode(Something.self, from: data) {
//successResponse을 completion에 전달
completion(.success(successResponse))
return
}
completion(.failure(APIError.에러명))
}
task.resume()
}
}
비동기처리과정에서 throw 방식의 에러핸들링 :(
앞서 궁금증을 가졌던 부분인 throw를 통한 비동기처리과정 에러핸들링이 어려운 이유에 대해선,
-
비동기처리의 내용이 포함된 메소드에 throws를 달아주면 throws가 메소드 내의 비동기처리과정의 결과(에러인지 아닌지)를 기다려주는게 아니라 메소드 호출 시점에 시행되기에 에러발생가능시점보다 먼저 throw가 되서 에러를 못잡게 됩니다.
-
throws를 활용한 에러처리를 고수하여 1의 상황을 해결하기 위해선 completion에 throws가 담긴 function 타입을 담아야 합니다.
코드로 위의 2번에 대해 더 구체적으로 표현하자면,참고링크 의 예시코드를 기반으로 각색하여 작성해보았습니다.)
enum APIError: Error {
case defaultError
}
struct Something {
init(fromData: Data) throws { /* … */ }
/* … */
}
typealias SomethingBuilder = Void throws -> Something // throws가 담긴 function 타입
func fetchData(completion: @escaping SomethingBuilder -> Void) {
let request = URLRequest(url: "데이터를 받아오는 url")
let task: URLSessionDataTask = session.dataTask(with: request) { (data, response, error) in
completion({ SomethingBuilder in
if let error = error { throw APIError.defaultError }
guard let data = data else { throw APIError.defaultError }
return try Something(fromData: data)
})
}.resume()
}
fetchData { (somethingBuilder: SomethingBuilder) in
do {
let something = try somethingBuilder()
} catch {
print("\(error)")
}
}
ResultType을 활용할 때와 가장 큰 차이점은 completion 부분에서 즉각적으로 성공시 반환하는 타입이냐,에러냐 가 아니라 타입에 대한 try문을 반환하면서, 타입 내에서 throw를 또 진행하게 한다는 점입니다. ErrorHandling을 한 번 더 해주는 상황으로 이어집니다. 복잡해지는군요 +_+..
느낀점
Result 와 throw 의 에러핸들링 방식을 비교해봤을때 Result는 declarative(선언적), throw방식은 imperative(명령적) 하다고 생각이 들었습니다. 더불어 Result 을 활용하는것이 input에 대한 return값이 항상 동일하다, immutable하다, how(에러핸들링의 과정) 보단 what( ResultType의 연관값에 무엇을 담을건지) 에 집중하는 것 같아 함수형 프로그래밍이 지향하는 바에 가깝다고 생각하게 되었습니다. :)