[Combine] Debugging

뚝딱따리뚝딱딱 디버깅을 해보자 🛠

naljin
10 min readApr 24, 2020

비동기 프로그램에서 이벤트 흐름을 이해하는 것은 항상 어려운 일이죠! 특히 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

printTextOutputStream 객체를 파라미터로 추가해서 이곳으로 문자열을 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 이벤트를 가로채고 작업을 수행할 수 있습니다.
  • breakpointOnErrorbreakpoint 연산자를 사용하여 특정 이벤트를 중단할 수 있습니다.

이전 포스팅 👈🏻

다음 포스팅 👉🏻

--

--

No responses yet