IOS - Swift/Combine

[iOS/Combine] Combine - Error Handling

게게겍 2024. 12. 12. 15:40
  • Error Handling (챕터 16) - 지훈

Never

Failure 타입이 Never인 퍼블리셔는 절대 실패할 수 없다는 것을 나타냅니다.

public typealias Failure = Never// Just 등의 publisher에서 사용
  • 퍼블리셔가 절대 실패하지 않음을 컴파일 타임에 보장
  • 에러 처리 로직이 필요 없음

특징

  • 간단한 sink 구독 가능 (에러 핸들링 클로저 불필요)
Just("Hello")
    .sink(receiveValue: { print($0) })// 에러 핸들링 없이 값만 처리

Never Failure 퍼블리셔들

  • sink
  • Just
  • Empty
  • @Published 프로퍼티 래퍼
  • Timer.publish()

assign(to:on:)

func assign<Root>(
    to keyPath: ReferenceWritableKeyPath<Root, Self.Output>,
    on object: Root
) -> AnyCancellable

publisher로부터 받은 값을 object의 프로퍼티에 할당해주는 메소드.

중복이니까 이부분은 패스! 

setFailureType

실패하지 않는 publisher를 실패할 수 있는 publisher로 변환해야 하는 경우에 사용됩니다.

 

func setFailureType<E>(to failureType: E.Type) ->
Publishers.SetFailureType<Self, E> where E : Error

upstream publisher의 failure type을 바꾸기 위해 사용한다.

대신 upstream publisher의 failure type은 Never 여야 한다!

(실패할 수 있는 upstream의 에러 타입을 바꾸어야 한다면 mapError(_:) 사용)

예시

enum MyError: Error {
    case ohNo
}

let publisher = Just("안녕하세요")
    .setFailureType(to: MyError.self) // 실패 가능성 추가

publisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .failure:
            print("오류 발생!")
        case .finished:
            print("완료!")
        }
    },
    receiveValue: { text in
        print("받은 텍스트: \\(text)")
    }
)
//출력
//받은 값: 안녕하세요
//성공적으로 완료되었습니다!

두 publisher의 Failure 타입이 같아야 하기 때문에, 에러 타입을 맞추기 위해서 사용된다.

또한 setFailureType 은 타입에만 영향을 미치기 때문에 실제 에러는 발생하지 않는다.

assertNoFailure

assertNoFailure는 "이 데이터 스트림은 절대 실패하면 안됨" 이라고 말하는 메서드이다.

func assertNoFailure(
    _ prefix: String = "",
    file: StaticString = #file,
    line: UInt = #line
) -> Publishers.AssertNoFailure<Self>

upstream publisher가 실패하면 fatal error를 일으키고, 아니라면 받은 input 값을 모두 다시 publish하는 메소드

  • 테스트 중 내부 무결성을 확인하고 싶을 때 사용 → publisher가 실패로 종료될 수 없음을 확인할 때 유용!
  • 개발, 테스트뿐 아니라 배포 버전에서도 fatal error를 일으키기에 개발시에만 사용하는걸 권장

예시

public enum MyError: Error {
    case genericSubjectError
}

let subject = CurrentValueSubject<String, Error>("initial value")
subject
    .assertNoFailure()
    .sink(receiveCompletion: { print ("completion: \\($0)") },
          receiveValue: { print ("value: \\($0).") }
    )

subject.send("second value")
subject.send(completion: Subscribers.Completion<Error>.failure(MyError.genericSubjectError))

// Prints:
//  value: initial value. : 초기값
//  value: second value. : 1번쨰 보낸거
//  The process then terminates in the debugger as the assertNoFailure operator catches the genericSubjectError. : 에러받앗네? 나죽어~

subject 가 세 번째로 genericSubjectError 라는 에러를 보냈고, fatal exception이 발생해서 프로세스가 중단됩니다!


try 연산자들~

  • map / tryMap
  • scan / tryScan
  • filter / tryFilter
  • compactMap / tryCompactMap
  • removeDuplicates / tryRemoveDuplicates
  • reduce / tryReduce
  • drop / tryDrop

이 operator들은 publisher의 Failure 를 Swift의 Error 타입으로 바꿔줍니다~!

중복되는 내용이 많으니 패스!


Catching and retrying

replaceError

스트림에서 받은 에러를 특정한 값으로 대체(replace)하는 operator

func replaceError(with output: Self.Output) -> Publishers.ReplaceError<Self>

Swift의 nil-coalescing operator(??)과 비슷하게 작동합니다

예시

let publisher = someFailablePublisher
    .replaceError(with: "데이터를 불러올 수 없습니다")// 에러가 발생하면 대체 메시지 던져줌
    .sink { print($0) }

// 실패하는 네트워크 요청예시
URLSession.shared.dataTaskPublisher(for: url)
    .map { String(data: $0.data, encoding: .utf8) ?? "" }
    .replaceError(with: "네트워크 오류 발생")
    .sink { print($0) }

에러를 다시 새로운 publisher로 감싸고 downstream subscriber로 전달해줄때 catch(_:) 가 사용됩니다.

catch

func `catch`<P>(_ handler: @escaping (Self.Failure) -> P) -> Publishers.Catch<Self, P> where P : Publisher, Self.Output == P.Output

upstream publisher로부터 받은 에러를 다른 publisher로 교체하는 메소드

예시

let mainPublisher = URLSession.shared.dataTaskPublisher(for: primaryURL)
    .catch { error -> AnyPublisher<Data, Never> in
        // 주 서버가 실패하면 백업 서버로 전환
        return URLSession.shared.dataTaskPublisher(for: backupURL)
            .map { $0.data }
            .catch { _ in Just(Data()) } // 최후의 폴백
            .eraseToAnyPublisher()
    }

retry

func retry(_ retries: Int) -> Publishers.Retry<Self>

retry는 실패한 작업을 지정된 횟수만큼 재시도한다.

  • retries : 재시도할 횟수
  • publisher 반환
let SOPTPublisher = URLSession.shared.dataTaskPublisher(for: url)
    .retry(3) // 최대 3번까지 재시도
    .map { result in
        return String(data: result.data, encoding: .utf8) ?? ""
    }
    .catch { error in
        return Just("서비스를 이용할 수 없습니다. 잠시 후 다시 시도해주세요.")
    }

 

 

덧)

캐치랑 리트라이는 하나만 쓰는게 아니라 같이쓸때 더 좋을거같아요

let mainPublisher = someFailablePublisher
    .retry(2) // 먼저 재시도를 해보고
    .catch { error in 
        // 재시도가 실패하면 백업 퍼블리셔로 전환
        return backupPublisher
    }
    .replaceError(with: "모든 복구 시도 실패")