[Combine] Debugging
비동기 프로그램에서 이벤트 흐름을 이해하는 것은 항상 어려운 일이죠! 특히 throttle
연산자처럼 수신하는 모든 이벤트를 내보내지 않을 때 무슨 일이 일어나고 있는지 더더욱 알아내기 어렵습니다.
방출하는 이벤트는 많은데 throttle
을 거치고 나서는 많이 줄어들었죠? throttle
에 대한 자세한 정보는 여기를 확인합시다.
어쨌든 Combine은 흐름을 디버깅하는 데 도움이 되는 연산자를 제공합니다!
Printing events
print(_:to:)
연산자는 publisher를 통해 전달되는 것이 확실하지 않을 때 사용해야 하는 첫 번째 연산자입니다. 이것은 passthrough publisher
로서 아래와 같이 많은 정보를 인쇄해준답니다!
- subscription을 받은 시점과 upstream publisher에 대한 설명
- subscriber의 demand request (요청되는 개수를 알 수 있게 한다)
- upstream publisher가 방출하는 모든 값
- 완료 이벤트
간단한 코드를 통해 확인해볼까요?
let subscription = (1...3).publisher
.print("publisher")
.sink { _ in }
위와 같이 간단한 경우라도 결과는 이렇게 자세하게 출력됩니다.
publisher: receive subscription: (1...3)
publisher: request unlimited
publisher: receive value: (1)
publisher: receive value: (2)
publisher: receive value: (3)
publisher: receive finished
print
는 TextOutputStream
객체를 파라미터로 추가해서 이곳으로 문자열을 redirection 시킬 수 있어요! 이렇게 들어온 기존의 정보에 현재 날짜나 시간을 추가하는 등 다양한 방식으로 프린트 할 수 있습니다.
아래의 예시는 문자열 사이의 시간 간격을 표시하는 간단한 logger입니다. 이를 통해 publisher가 값을 얼마나 빨리 방출하는지 알 수 있겠죠?
class TimeLogger: TextOutputStream {
private var previous = Date()
private let formatter = NumberFormatter()init() {
formatter.maximumFractionDigits = 5
formatter.minimumFractionDigits = 5
}// TextOutputStream 프로토콜에서 기본으로 구현해야하는 write 함수. string 파라미터는 redirection 된 기존의 정보
func write(_ string: String) {
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
}
}
만들어진 TimeLogger
는 매우 간단하게 사용할 수 있습니다.
let subscription = (1...3).publisher
.print("publisher", to: TimeLogger())
.sink { _ in }
콘솔에는 각 라인 사이의 시간이 표시됩니다.
+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
Acting on events — performing side effects
정보를 출력하는 것 외에도, 특정 사건에 대한 조치를 취하는 것이 유용할 수 있습니다. handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)
를 통해 publisher의 라이프사이클에 있는 모든 이벤트를 가로채고, 각 단계에서 조치를 취할 수 있습니다.
publisher가 네트워크 요청을 수행하고 데이터를 방출해야 하는 아주 일반적인 상황을 가정해봅시다. 그런데 아무런 데이터도 받지 못한다면? 온갖 생각이 들기 시작하겠죠? ‘무슨 일이 일어난거지..?’ ‘요청이 가긴 한건가?’ ‘요청은 갔는데 들어오는 데이터들을 못 듣고 있는건가?’🤔🤯🤮😱
let request = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://medium.com/@rkdthd0403")!)request
.sink(receiveCompletion: { completion in
print("Sink received completion: \(completion)")
}) { (data, _) in
print("Sink received data: \(data)")
}
이 코드를 수행하면 아무 것도 프린트되지 않는데..! 두둥 코드만 보고 이슈를 찾을 수 있으신가요? 그렇다면 뒤로 가기를 눌러주십셔.. 총총…
흠.. 저의 이전 포스팅을 유심히 봤다면 알 수도 있겠지만 사실 모르는게..ㅎㅎ 삐빅 정상입니다.
자 어찌됐던간에 뭐가 문젠지 모르겠다! 할때 handleEvents
를 사용해서 무슨 일이 일어나고 있는지 추적할 수 있습니다. 이 연산자를 publisher와 sink
사이에 삽입해봅시다.
.handleEvents(receiveSubscription: { _ in
print("Network request will start")
}, receiveOutput: { _ in
print("Network request data received")
}, receiveCancel: {
print("Network request cancelled")
})
다시 실행하면? 일부 디버깅 출력을 확인할 수 있습니다.
Network request will start
Network request cancelled
오호라.. 구독은 시작되지만 즉시 취소된다? Cancellable
을 저장하지 않을때 일어나는 일이군..!
이제 Cancellable
을 변수에 담아 유지하는 것으로 코드를 수정할 수 있습니다.
let subscription = request
.handleEvents...
코드를 다시 실행하면 올바르게 작동하는 것을 볼 수 있습니다.
Network request will start
Network request data received
Sink received data: 153253 bytes
Sink received completion: finished
사실 이 정도는 위에서 설명된 print
함수를 써서 해결할 수 있을 듯 합니다. 프린트를 써도 이정도는 나오기 때문이죠.
receive subscription: (DataTaskPublisher)
request unlimited
receive cancel
참고로 여기의 소제목이 Acting on events — performing side effects 인 이유는 “on the side” 에서 수행하는 행동이 이후의 publisher에 (stream의 down에 있는 publisher) 직접적인 영향을 미치지는 않지만, 외부 변수를 수정하는 등의 효과를 줄 수 있기 때문에 이것을 “performing side effects”라고 부르기 때문입니다. 번역이 너무 허접해서리 이렇게 맨 마지막에 붙입니다..ㅎㅎ
Using the debugger as a last resort
위에서 소개된 그 무엇도 문제를 알아내는데 도움이 되지 않는다면 어떻게 할까요? 그럴때 사용하는 최후의 수단들을 알아봅시다.
첫 번째 연산자는 breakpointOnError()
입니다. 이름에서 알 수 있듯이, upstream publisher에서 오류가 발생하면 break 됩니다. 스택을 보면서 publisher가 오류를 일으키는 이유와 장소를 찾을 수 있겠죠?
보다 완전한 변형은 breakpoint(receiveSubscription:receiveOutput:receiveCompletion:)
입니다. 다양한 이벤트를 가로채고, 디버거를 일시 중지할지 여부를 사례별로 결정할 수 있습니다.
이렇게 특정 값이 publisher를 통과하는 경우에만 중단할 수 있습니다.
.breakpoint(receiveOutput: { value in
return value > 10 && value < 15
})
참고: playground 에서는 어떤 breakpoint publisher도 동작하지 않을 것입니다. 실행이 중단되었지만 debugger에 들어가지 않는다는 오류를 볼 수 있습니다.
Key points
print
연산자와 함께 publisher의 lifecycle을 추적할 수 있습니다.- 고유한
TextOutputStream
을 생성하여 출력 문자열을 customize 할 수 있습니다. handleEvents
연산자를 사용하여 lifecycle 이벤트를 가로채고 작업을 수행할 수 있습니다.breakpointOnError
및breakpoint
연산자를 사용하여 특정 이벤트를 중단할 수 있습니다.