ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter 10: Debugging
    Raywenderlich/Combine: Asynchronous Programming 2020. 8. 13. 01:37

    Version

    Swift 5.3, iOS 14, Xcode 12

     

    비동기(asynchronous) 코드의 이벤트(event) 흐름(flow)을 이해하는 것은 항상 어려운 일이었다. publisher의 연산자(operators) 체인(chains)이 이벤트(events)를 즉시 내보내지(emit) 않을 수 있기 때문에 Combine의 컨텍스트(context)에서 특히 그러하다. 예를 들어 throttle(for:scheduler:latest:)와 같은 연산자(operators)는 수신하는 모든 이벤트(events)를 내보내지(emit) 않으므로, 무슨 일이 일어나고 있는지(what’s going on) 이해해야 한다. Combine은 반응형(reactive) 흐름(flows)을 디버깅(debugging)하는 데 도움이 되는 몇 가지 연산자(operators)를 제공한다. 이를 알면 곤혹스러운(puzzling) 상황을 해결하는(troubleshoot) 데 도움이 될 것이다.

     

    Printing events

    print(_:to:) 연산자(operator)는 publishers에서 어떤 일이 일어나고 있는지 확실하지 않을 때(unsure) 가장 먼저 사용해야 하는 연산자(operator)이다. 이는 무슨 일이 일어나고 있는지에 대한 많은 정보를 출력(prints)하는 passthrough publisher이다.

    다음과 같은 간단한(simple) 경우(cases)에도:

    let subscription = (1...3).publisher
        .print("publisher")
        .sink { _ in }

    매우 상세한 출력(output)을 보여준다:

    publisher: receive subscription: (1...3)
    publisher: request unlimited
    publisher: receive value: (1)
    publisher: receive value: (2)
    publisher: receive value: (3)
    publisher: receive finished

    여기에서 print(_:to:) 연산자(operators)는 다음과 같이 많은 정보(information)를 보여준다:

    • 구독(subscription)을 받으면 출력(prints)하고, 업 스트림(upstream) publisher에 대한 설명(description)을 표시한다.
    • 요청(requested) 중인 항목(items) 수를 확인할 수 있도록 subscriber의 수요(demand) 요청(requests)을 출력(prints)한다.
    • 업 스트림(upstream) publisher가 내보낸(emits) 모든 값(value)을 출력(prints)한다.
    • 마지막으로, 완료(completion) 이벤트(event)를 출력(prints)한다.

    TextOutputStream 객체를 받는 추가(additional) 매개 변수(parameter)가 있다. 이를 사용하여 로거(logger)에 출력할 문자열(strings)을 리디렉션(redirect)할 수 있다. 현재 날짜(date) 및 시간(time) 등과 같은 정보(information)를 로그에 추가할 수도 있다. 가능성은 무한하다(The possibilities are endless).

    예를 들어, 각 문자열(string) 사이의 시간 간격(interval)을 표시하는 간단한 로거(logger)를 만들어, publisher가 값(values)을 얼마나 빨리 내보내(emits)는지 파악할 수 있다:

    class TimeLogger: TextOutputStream {
        private var previous = Date()
        private let formatter = NumberFormatter()
        
        init() {
            formatter.maximumFractionDigits = 5
            formatter.minimumFractionDigits = 5
        }
        
        func write(_ string: String) { //TextOutputStream 프로토콜은 해당 함수를 구현해야 한다.
            let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
            
            guard !trimmed.isEmpty else { return }
            
            let now = Date()
            print("+\(formatter.string(for: now.timeIntervalSince(previous))!)s: \(string)")
            previous = now
        }
    }

    코드에서 사용하는 것은 매우 간단하다:

    let subscription = (1...3).publisher
        .print("publisher", to: TimeLogger())
        .sink { _ in }

    그리고 결과는 출력 줄(line) 사이의 시간을 표시한다:

    +0.00111s: publisher: receive subscription: (1...3)
    +0.03485s: publisher: request unlimited
    +0.00035s: publisher: receive value: (1)
    +0.00025s: publisher: receive value: (2)
    +0.00027s: publisher: receive value: (3)
    +0.00024s: publisher: receive finished

    위에서 언급했듯이, 가능성은 무궁무진하다(the possibilities are quite endless here).

     

    Acting on events — performing side effects

    정보(information)를 출력(printing out)하는 것 외에도, 특정 이벤트(events)에 대해 해당 연산자 작업을 수행(perform)하는 것이 유용한 경우가 많다. 이를 performing side effects이라고 부르는데, 다른(further) publishers에게 직접적으로(directly) 영향(impact)을 미치지는 않지만 외부(external) 변수(variable)를 수정(modifying)하는 것과 같은 영향을 미칠(effect) 수 있기 때문이다.

    handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)를 사용하면 publisher의 생명 주기(lifecycle)에서 모든 이벤트(events)에서(intercept) 각 단계(step)의 조치를 취할 수 있다.

    publisher가 네트워크(network) 요청(request)을 수행(perform)한 다음, 일부 데이터를 내보내는(emit) 작업(issue)을 추적(tracking)한다고 가정해 본다. 실행(run)시에 데이터가 수신(receives)되지 않는다면 무슨 일이 일어났는지 확인해 보기 위해 다음과 같은 코드를 고려(consider)해 볼 수 있다:

    let request = URLSession.shared
        .dataTaskPublisher(for: URL(string: "https://www.raywenderlich.com/")!)
    
    request
        .sink(receiveCompletion: { completion in
            print("Sink received completion: \(completion)")
        }) { (data, _) in
            print("Sink received data: \(data)")
        }

    이를 실행(run)해도 아무것도 출력(print)되지 않는다면, 코드만 보고 문제(issue)를 해결하기는 쉽지 않다.

    handleEvents를 사용하여 상황(happening)을 추적한다. publisher와 sink 사이에 다음 연산자(operator)를 삽입(insert)할 수 있다:

    .handleEvents(receiveSubscription: { _ in
        print("Network request will start")
    }, receiveOutput: { _ in
        print("Network request data received")
    }, receiveCancel: {
        print("Network request cancelled")
    })

    그런 다음 코드를 다시 실행(run)한다. 이번에는 몇 가지 디버깅(debugging) 출력(output)이 표시된다:

    Network request will start
    Network request cancelled

    이를 확인하면, Cancellable을 유지(keep)하는 것을 잊었다는 것을 알 수 있다. 따라서 구독(subscription)이 시작(starts)되지만, 즉시(immediately) 취소(canceled)된다. 이제 Cancellable을 유지하도록(retaining) 코드를 수정한다:

     let subscription = request 
         .handleEvents...

    코드를 다시 실행(running)하면, 올바르게 작동하는 것을 확인할 수 있다:

    Network request will start
    Network request data received
    Sink received data: 153253 bytes
    Sink received completion: finished

     

    Using the debugger as a last resort

    다른 어떤 방법도 무엇이 잘못되었는지 파악(figure out)하는 데 도움이 되지 않아서, 디버거(debugger)의 실제 항목을 특정 시간에서(at certain times) 조사할 필요가 있는 경우에 최후의 조치(last resort)로 사용할 수 있는 연산자(operator)가 있다. 

    첫 번째 연산자(operator)는 breakpointOnError() 이다. 이름에서 알 수 있듯이, 이 연산자(operator)를 사용할 때 업 스트림(upstream) publishers 중 하나에서 오류(error)가 발생(emits)하면 Xcode가 디버거(debugger)를 중단(break)시켜 스택(stack)을 살펴 볼 수 있게 되고, publisher가 오류(errors)를 발생한 이유(why)와 위치(where)를 찾을 수 있게 된다.

    보다 완전한 형식(variant)은 breakpoint(receiveSubscription:receiveOutput:receiveCompletion:) 이다. 다양한 이벤트(events)에서, 디버거(debugger)의 일시 중지(pause) 여부를 사례별로(case-by-case) 결정할 수 있다.

    예를 들어, 다음과 같이 특정(certain) 값(values)이 publisher를 통과하는 경우에만 중단하도록 할 수 있다:

    .breakpoint(receiveOutput: { value in
        return value > 10 && value < 15
    })

    업 스트림(upstream) publisher가 정수(integer) 값(values)을 내보내(emits)지만, 값이 11 ~ 14 이어서는 안 된다고 가정하면, 이 경우에만 중단(break)되도록 breakpoint을 구성(configure)하고 조사(investigate)할 수 있다.

    조건부(conditionally)로 구독(subscription) 및 완료(completion) 시에 중단(break)할 수도 있지만, handleEvents 연산자(operator)처럼 취소(cancelations)에는 사용할 수 없다.

    Note: 중단점(breakpoint) publishers는 playgrounds에서 작동하지 않는다. 실행(execution)이 중단되었다(interrupted)는 오류(error)가 표시되지만, 디버거(debugger)에 표시되지는 않는다.

     

    Key points

    • print 연산자(operator)를 사용해 publisher의 생명 주기(lifecycle)를 추적할 수 있다.
    • 자신만(own)의 TextOutputStream을 생성하여, 사용자 지정(customize) 출력(output) 문자열(strings)을 정의할 수 있다.
    • handleEvents 연산자(operator)를 사용하여 수명 주기(lifecycle) 이벤트(events)를 가져오고(intercept) 작업을 수행(perform)한다.
    • breakpointOnErrorbreakpoint 연산자(operators)를 사용하여 특정(specific) 이벤트(events)를 중단(break)한다.

    'Raywenderlich > Combine: Asynchronous Programming' 카테고리의 다른 글

    Chapter 12: Key-Value Observing  (0) 2020.08.14
    Chapter 11: Timers  (0) 2020.08.13
    Chapter 9: Networking  (0) 2020.08.13
    Chapter 8: In Practice: Project "Collage"  (0) 2020.08.11
    Chapter 7: Sequence Operators  (0) 2020.08.09
Designed by Tistory.