에러와 Observable 존재
Observable에서 에러가 발생하면 해당 시퀀스는 즉시 종료됩니다.
// 에러 발생 시 Observable의 동작
someObservable
.subscribe(
onNext: { value in
print("값: \(value)")
},
onError: { error in
print("에러 발생: \(error)")
// 이 시점에서 Observable은 종료됨
},
onCompleted: {
print("완료") // 에러 발생 시 이 부분은 실행되지 않음
}
)
실무에서 흔히 발생하는 에러 상황은 뭐가 있을까요?
- 네트워크 연결 실패
- 잘못된 사용자 입력
- 서버 에러
- 파싱 에러 등등 ................
Catch 연산자를 활용한 에러 처리
1. catchError
func catchError(_ handler: @escaping (Error) throws -> Observable<Element>) -> Observable<Element>
에러가 발생했을 때 대체 Observable을 반환하는 클로저를 인자로 받습니다. 이를 통해 에러 유형에 따라 다른 처리를 할 수 있습니다.
apiClient.fetchData()
.catchError { error in
if let networkError = error as? NetworkError, networkError == .connectionLost {
return Observable.just(cachedData) // 캐시된 데이터 반환
} else {
return Observable.just(emptyData) // 빈 데이터 반환
}
}
.subscribe(onNext: { data in
// UI 업데이트
})
.disposed(by: disposeBag)
catchError는 다음과 같은 상황에 유용합니다:
- 특정 에러에 대해 캐시된 데이터 또는 기본값 반환
- 다른 API 엔드포인트 또는 데이터 소스로 전환
- 에러를 로깅하면서 대체 데이터 제공
2. CatchErrorJustReturn
func catchErrorJustReturn(_ element: Element) -> Observable<Element>
더 단순한 형태로, 어떤 에러가 발생하든 상관없이 미리 정의된 값을 반환합니다.
Retry
네트워크 불안정 등의 일시적인 문제로 인한 에러는 재시도를 통해 해결될수 있습니다.
1. retry
apiClient.fetchData()
.retry() // 주의: 무한 재시도 가능성
.subscribe(onNext: { data in
// 처리 로직
})
.disposed(by: disposeBag)
가장 단순한 형태는 에러가 발생할때마다 무제한으로 재시도
2. 제한된 재시도
retry (3) -> 최대 3번까지만 할께.
3. retryWhen
let maxAttempts = 4
apiClient.fetchData()
.retryWhen { errors in
return errors.enumerated().flatMap { (attempt, error) -> Observable<Int> in
// 최대 시도 횟수 초과 시 에러 전달
if attempt >= maxAttempts - 1 {
return Observable.error(error)
}
// 1초, 3초, 5초, 10초 간격으로 재시도
let delay = Double(attempt + 1) * 2
print("재시도 \(attempt + 1)회 - \(delay)초 후")
return Observable<Int>.timer(delay, scheduler: MainScheduler.instance).take(1)
}
}
.subscribe(onNext: { data in
// 처리 로직
})
.disposed(by: disposeBag)
retryWhen은 네트워크 요청과 같이 일시적인 실패가 예상되는 경우에 유용합니다.
고급 에러 처리 패턴?
materialize/demateriaze
materialize는 Observable의 이벤트를 Event<T>로 변환하여 에러를 포함한 모든 이벤트를 다른 Observable로 처리할 수 있게 합니다. 이를 통해 에러 처리를 더 세밀하게 제어할 수 있습니다. dematerialize는 반대로 Event<T>를 원래의 Observable로 되돌립니다.
// 특정 에러 타입을 필터링하고 다른 값으로 대체하는 패턴
apiClient.fetchData()
.materialize()
.map { event -> Event<Data> in
switch event {
case .error(let error):
if let apiError = error as? ApiError {
switch apiError {
case .rateLimitExceeded:
// 사용 제한 초과 에러를 완료 이벤트로 변환
analytics.log(event: "RateLimitHandled")
return .completed
case .resourceNotFound:
// 리소스 없음 에러를 빈 데이터로 변환
analytics.log(event: "ResourceNotFoundHandled")
return .next(Data())
case .serverOverloaded:
// 서버 과부하는 특별 처리 없이 그대로 전파
return event
default:
// 기타 API 에러는 공통 에러로 매핑
return .error(AppError.apiFailure(original: apiError))
}
}
return event
default:
return event
}
}
.dematerialize()
에러를 변환하는데 사용할수 있다.
enum ErrorStrategy {
case ignore
case replaceWithDefault
case transformToCompletion
case rethrow
}
func applyErrorStrategy<T>(_ strategy: ErrorStrategy, defaultValue: T? = nil) -> (Event<T>) -> Event<T> {
return { event in
if case .error = event {
switch strategy {
case .ignore:
return .completed
case .replaceWithDefault:
if let value = defaultValue {
return .next(value)
}
return .completed
case .transformToCompletion:
return .completed
case .rethrow:
return event
}
}
return event
}
}
// 사용 예시
apiObservable
.materialize()
.map(applyErrorStrategy(.replaceWithDefault, defaultValue: Data()))
.dematerialize()
언제쓸까?
: 특정 에러를 다른 이벤트로 변환할때, 에러를 필터링하거나 로깅 후 다른값으로 대체할때, 복잡한 에러처리 로직을 명시적으로 정의할때
flatMap과 에러 전파 제어
flatMap을 사용하면 내부 Observable의 에러를 외부 Observable로 전파하지 않도록 제어할 수 있습니다.
Observable.just(userIds)
.flatMap { ids in
apiClient.fetchUserData(ids)
.catchErrorJustReturn(defaultUserData) // 내부 에러 처리
}
.subscribe(onNext: { userData in
// 외부 Observable은 계속 진행
print("유저 데이터: \(userData)")
})
.disposed(by: disposeBag)
에러 로깅과 사용자 알림
apiClient.fetchData()
.catchError { error in
// 에러 로깅
analytics.log(event: "FetchDataError", parameters: ["error": error.localizedDescription])
// 사용자 알림
notificationManager.showError(message: "데이터를 불러오지 못했습니다. 다시 시도해주세요.")
return Observable.just(emptyData)
}
.subscribe(onNext: { data in
print("데이터: \(data)")
})
.disposed(by: disposeBag)
이런식으로 에러처리시 로깅과 사용자 알림을 추가하면 디버깅과 사용자 경험이 향상됩니다.
'반응형프로그래밍' 카테고리의 다른 글
RxSwift(9) - Scheduler (0) | 2025.05.09 |
---|---|
RxSwift(7)-BehaviorRelay (0) | 2025.05.06 |
RxSwift(6)-RxCocoa(bind, drive, DelegateProxy) (0) | 2025.05.06 |
RxSwift(5)-TimeBasedOperators (0) | 2025.05.04 |
RxSwift(4)-Combining Operators (0) | 2025.05.02 |