| Combine | 11. Mapping Errors
이전까지 컴바인 프레임워크를 이용하면서 해왔던 에러처리들은 모두 업스트림에서 에러를 던져주면 다운스트임에서 .sink 를 통해 받기만 하는 수동적인 방법이였다. 하지만 만약 에러를 throw하는 메서드를 sink받기전에 쓰고 sink에서 catch하고 싶다면? 혹은 OOP개념과 함께 컴바인을 쓰면서 인터페이스로부터 나온 Stream을 받을 때 보통 추상화된 에러를 받기 때문에 sink로 받기전에 구체 에러타입으로 바꿔야한다면? .tryMap 연산자를 통해 에러처리를 할 수 있다.
enum APIError: Error {
case networkError
case dataCorrupted
case invalidFormat
}
struct User {
let id: Int
let name: String
}
// 예제 JSON 데이터
let jsonData = """
{
"id": 123,
"name": "Alice"
}
""".data(using: .utf8)!
// 추상화된 에러를 처리하고 데이터를 파싱하는 함수
func fetchData() -> AnyPublisher<Data, Error> {
// API에서 데이터를 가져오는 상황을 시뮬레이션
Just(jsonData)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
private func parseUser(from data: Data) throws -> User {
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let dictionary = jsonObject as? [String: Any],
let id = dictionary["id"] as? Int,
let name = dictionary["nnnaaammme"] as? String else { // 일부러 에러발생
throw APIError.dataCorrupted
}
return User(id: id, name: name)
}
// 다운스트림에서 데이터 처리
func handleData() {
var subscriptions = Set<AnyCancellable>()
fetchData()
.tryMap { data -> User in
try parseUser(from: data)
}
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Data processing completed successfully.")
case .failure(let error):
print("An error occurred: \(error)")
}
}, receiveValue: { user in
print("Received user: \(user)")
})
.store(in: &subscriptions)
}
handleData()
.tryMap 을 활용한 예제 코드이다. 살펴보면 fetachData() 메서드는 AnyPublisher<Data, Error> 라는 추상화된 Error 타입을 던져준다. 다운스트림에서는 업스트림에서 던져주는 Error를 구체화해서 처리해야한다. 이때 parseUser() 메서드는 구체환된 에러를 throw하고 있다. 이렇게 추상화된 에러와 구체화된 에러처리의 가교역할 및 throw 메서드를 sink로 받기전에 처리할 수 있는 연산자가 바로 tryMap 연산자이다. 소비자 입장에서 sink로 업스트림 밸류로 받기전에 먼저 tryMap을 통해 에러를 구체화해서 던져준뒤 sink를 통한 다운스트림에서 효과적으로 에러처리를 할 수 있다.
앞서 tryMap은 upstream에서 받은 데이터를 특정한 형식으로 변환하려고 시도할 때 사용된다면 mapError는 upstream에서 발생한 에러를 다운스트림에서 다룰 수 있는 다른 형태의 에러로 매핑(변환)할 때 사용된다. tryMap이 에러를 "바꾸는" 느낌이었다면 mapError는 말그대로 에러를 "맵핑"한다. 아래 예제코드를 들여다보자
// 네트워크 요청의 결과를 나타내는 enum
enum NetworkRequestError: Error {
case connectionError
case invalidData
case unknown
}
// 사용자에게 보여줄 에러
enum UserFriendlyError: Error {
case unableToConnect
case corruptedData
case somethingWentWrong
}
// 네트워크 요청을 시뮬레이션하는 함수
func performNetworkRequest() -> AnyPublisher<String, NetworkRequestError> {
// 실제 애플리케이션에서는 네트워크 요청 코드를 여기에 작성
Fail(error: NetworkRequestError.connectionError)
.eraseToAnyPublisher()
}
// 데이터 처리 및 에러 매핑
func handleRequest() {
let cancellable = performNetworkRequest()
.map { data in
// 데이터 처리 로직을 여기에 추가
print("Data received: \(data)")
}
.mapError { error -> UserFriendlyError in
// 네트워크 에러를 사용자 친화적인 에러로 매핑
switch error {
case .connectionError:
return .unableToConnect
case .invalidData:
return .corruptedData
case .unknown:
return .somethingWentWrong
}
}
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed successfully.")
case .failure(let error):
print("An error occurred: \(error)")
}
}, receiveValue: { _ in
// 데이터를 성공적으로 처리한 경우 실행되는 블록
})
}
위 예제코드를 보면 tryMap은 에러를 throw하거나 추상화된 에러타입을 구체화된 에러타입으로 "변환"을 해주었다면 mapError는 에러 케이스별로 또 다른 에러와 "맵핑"을 해주고 있음을 확인할 수 있다. 이런 에러 맵핑을 보며 든 생각은 클린아키텍쳐 기준으로 각 레이어간 서로 다른 에러처리를 해주어야할 때 유용할 것 같다는 생각이 들었다.
24.05.15 추가
import Combine
let jsonData = """
{
"name": "John",
"age": 30
}
""".data(using: .utf8)!
let publisher = Just(jsonData)
.tryMap { data -> [String: Any] in
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
throw URLError(.badServerResponse)
}
return json
}
.sink(receiveCompletion: { completion in
if case let .failure(error) = completion {
print("Error occurred: \(error)")
}
}, receiveValue: { json in
print("Received JSON: \(json)")
})
컴바인의 에러처리에서 tryMap 연산자를 소개했지만 tryMap은 데이터 변환 작업에 중점을 두며, 에러 처리보다는 데이터 처리를 목적으로 사용된다. 이 과정에서 에러가 발생할 경우 throw로 처리해줄 수 있다. 만약 직접 이미 발생한 에러를 처리하고 스트림의 흐름을 변경하거나 대체 데이터를 제공하고자 할 때에는 cacth 연산자를 사용하자
https://www.kodeco.com/21773708-intermediate-combine/lessons/4
Intermediate Combine, Episode 4: Mapping Errors
Operators such as tryMap erase any error types that are encountered during execution of the code within the operator. The mapError operator lets developers maintain knowledge of errors encountered, passing them down the Combine pipeline.
www.kodeco.com