Combine 스터디 (5) - Error Handling

Seoyoung Lee·2024년 4월 28일
0

Combine 스터디

목록 보기
5/5
post-thumbnail

Combine에서 에러 핸들링 특징

지금까지 봤던 다양한 Publisher들에서 알 수 있듯, Combine은 스트림에서 에러 타입을 꼭 정의해주어야 한다.

struct AnyPublisher<Output, Failure> where Failure : Error

Publisher에서 Operator, Subscriber로 흘러가는 동안 에러는 언제 어디서든 발생할 수 있다. 따라서 Combine을 효과적이고 안전하게 사용하기 위해서 에러를 적절하게 처리하는 것은 필수적이다! (사실 Combine뿐 아니라 모든 reactive programming에 다 해당되는 일,,)

Combine에서의 에러 핸들링 방법을 알아보자!✨

Never

본격적인 에러 핸들링 방법을 공부하기 전, Never 에 대해 잠깐 짚고 넘어가보자.

Failure 타입이 Never 인 Publisher는 절대 실패하지 않는다.

따라서 Publisher가 값을 소비하는 데에만 집중할 수 있다.

절대 실패하지 않는 Publisher를 위한 메소드

Combine에서는 절대 실패하지 않을 때만 사용할 수 있는 메소드들이 있다.

sink(receiveValue:)

func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
let integers = (0...3)
integers.publisher
    .sink { print("Received \($0)") }

// Prints:
//  Received 0
//  Received 1
//  Received 2
//  Received 3

이 메소드는 subscriber를 생성하고, subscriber를 리턴하기 전에 바로 값을 무한대로 요청한다.

리턴되는 값은 유지되어야 하고, 그렇지 않으면 스트림이 취소된다.

assign(to:on:)

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

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

이전 스터디 주차에서 공부했었던 메소드라 자세한 설명은 생략!!

setFailureType

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

실패하지 않는 publisher를 실패할 수 있는 publisher로 변환해야 하는 경우가 있을 것이다.

setFailureType operator를 사용하면 upstream publisher의 failure type을 바꿀 수 있다.

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

(실패할 수 있는 upstream의 에러 타입을 바꾸고 싶다면 mapError(_:) 를 사용하자)

let pub1 = [0, 1, 2, 3, 4, 5].publisher
let pub2 = CurrentValueSubject<Int, Error>(0)
let cancellable = pub1
    .setFailureType(to: Error.self)
    .combineLatest(pub2)
    .sink(
        receiveCompletion: { print ("completed: \($0)") },
        receiveValue: { print ("value: \($0)")}
     )

// Prints: "value: (5, 0)".

combineLatest 는 두 publisher의 Failure 타입이 같아야 하기 때문에, 위 예시에서는 setFailureType 으로 에러 타입을 맞춰주었다.

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

assertNoFailure

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

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

  • prefix : fatal error 메시지 앞에 적을 string
  • file : 에러 메시지에서 사용할 파일명
  • line : 에러 메시지에서 사용할 라인 넘버
  • 테스트 중 내부 무결성을 확인하고 싶을 때 사용 → publisher가 실패로 종료될 수 없음을 확인할 때 유용!
  • 개발, 테스트뿐 아니라 배포 버전에서도 fatal error를 일으키니 주의!!
public enum SubjectError: 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(SubjectError.genericSubjectError))

// Prints:
//  value: initial value.
//  value: second value.
//  The process then terminates in the debugger as the assertNoFailure operator catches the genericSubjectError.

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

try~ 친구들

Combine 은 에러를 발생시킬 수 있는 operator 와 그렇지 않은 operator 사이에 구분을 제공한다.

이 operator들은 publisher의 Failure 를 Swift의 Error 타입으로 바꾼다.

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

그외 너무 많다.. 그때그때 try operator가 있는지 찾아보면서 개발하자

Mapping errors

map 은 에러를 던질 수 없는 non-throwing 메소드이다. 에러를 던지고 싶을 때는 tryMap 을 사용하자.

tryMap

func tryMap<T>(_ transform: @escaping (Self.Output) throws -> T) -> Publishers.TryMap<Self, T>
struct ParseError: Error {}
func romanNumeral(from:Int) throws -> String {
    let romanNumeralDict: [Int : String] =
        [1:"I", 2:"II", 3:"III", 4:"IV", 5:"V"]
    guard let numeral = romanNumeralDict[from] else {
        throw ParseError()
    }
    return numeral
}
let numbers = [5, 4, 3, 2, 1, 0]
cancellable = numbers.publisher
    .tryMap { try romanNumeral(from: $0) }
    .sink(
        receiveCompletion: { print ("completion: \($0)") },
        receiveValue: { print ("\($0)", terminator: " ") }
     )

// Prints: "V IV III II I completion: failure(ParseError())"

mapError

func mapError<E>(_ transform: @escaping (Self.Failure) -> E) -> Publishers.MapError<Self, E> where E : Error
struct DivisionByZeroError: Error {}
struct MyGenericError: Error { var wrappedError: Error }

func myDivide(_ dividend: Double, _ divisor: Double) throws -> Double {
       guard divisor != 0 else { throw DivisionByZeroError() }
       return dividend / divisor
   }

let divisors: [Double] = [5, 4, 3, 2, 1, 0]
divisors.publisher
    .tryMap { try myDivide(1, $0) }
    .mapError { MyGenericError(wrappedError: $0) }
    .sink(
        receiveCompletion: { print ("completion: \($0)") ,
        receiveValue: { print ("value: \($0)", terminator: " ") }
     )

// Prints: "0.2 0.25 0.3333333333333333 0.5 1.0 completion: failure(MyGenericError(wrappedError: DivisionByZeroError()))"

Catching and retrying

replaceError

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

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

struct MyError: Error {}
let fail = Fail<String, MyError>(error: MyError())
cancellable = fail
    .replaceError(with: "(replacement element)")
    .sink(
        receiveCompletion: { print ("\($0)") },
        receiveValue: { print ("\($0)", terminator: " ") }
    )

// Prints: "(replacement element) finished".

에러를 다시 감싸고 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로 교체하는 메소드

catch vs tryCatch

catch vs replaceError

Publishers.Catch가 상위 Publisher가 에러를 낼 때 다른 Publisher로 교체하는 동작을 제공한다면, Publishers.ReplaceError는 상위 Publisher가 에러를 낼 때 특정 요소로 교체하는 동작을 제공한다.

retry

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

upstream publisher를 이용해서 인자로 전달한 값까지 failed subscription을 다시 만든다.

  • retries : 재시도할 횟수
  • publisher 반환
struct WebSiteData: Codable {
    var rawHTML: String
}

let myURL = URL(string: "https://www.example.com")

cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
    .retry(3)
    .map({ (page) -> WebSiteData in
        return WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
    })
    .catch { error in
        return Just(WebSiteData(rawHTML: "<HTML>Unable to load page - timed out.</HTML>"))
}
.sink(receiveCompletion: { print ("completion: \($0)") },
      receiveValue: { print ("value: \($0)") }
 )

// Prints: The HTML content from the remote URL upon a successful connection,
//         or returns "<HTML>Unable to load page - timed out.</HTML>" if the number of retries exceeds the specified value.
profile
나의 내일은 파래 🐳

0개의 댓글