시배's Android

Hi Jack Mocker | 개선기 (5) feat.SharedFlow 본문

Android/Hi Jack Mocker

Hi Jack Mocker | 개선기 (5) feat.SharedFlow

si8ae 2024. 7. 25. 21:49

Hi Jack Mocker란?

 

GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net

Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker

github.com

Hi Jack Mocker는 비개발자도 UI 엣지 케이스를 쉽게 테스트할 수 있도록 돕는 라이브러리입니다. 이 프로젝트는 OkHttp3 인터셉터를 활용하여 네트워크 요청과 응답을 가로채고 수정할 수 있게 합니다. 이를 통해 개발자뿐만 아니라 다양한 사용자들이 다양한 시나리오를 테스트할 수 있게 합니다.

소개 

응답을 가로채고 수정한 후 다시 전달하는 과정에서 흥미로운 문제에 직면했고, 이를 해결하는 과정에서 코틀린의 Channel에서 SharedFlow로 전환하게 되었습니다. 이 포스트에서는 그 과정과 학습한 내용을 공유하고자 합니다.

초기 구현 : Channel 사용

처음에는 다음과 같은 방식으로 구현했습니다.

override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
    var response = chain.proceed(chain.request())

    if (hjmDataStore.getHjmMode()) {
        interceptorManager.sendWithInterceptorChannel(response)

        startHjmActivityIfNeeded()
        response = interceptorManager.receiveWithResultChannel()
    }

    return@runBlocking response
}

suspend fun receiveWithResultChannel(): Response {
    if (resultChannel.isClosedForReceive) {
        resultChannel = Channel(UNLIMITED)
    }

    return resultChannel.receive()
}

이 구현에서는 Channel을 사용하여 HjmActivity에서 수정된 응답을 다시 Interceptor로 전달했습니다.

문제점 인식

이 접근 방식은 단일 요청에 대해서는 잘 작동했지만, 여러 개의 동시 요청을 처리할 때 문제가 발생했습니다. Channel의 특성상 첫 번째로 도착한 응답이 첫 번째로 대기 중인 Interceptor에 전달되는 문제가 있었습니다.

해결책 : SharedFlow 도입

이 문제를 해결하기 위해 SharedFlow를 도입했습니다. SharedFlow는 여러 소비자가 동시에 값을 관찰할 수 있어, 우리의 사용 사례에 더 적합했습니다.

override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
    var response = chain.proceed(chain.request())

    if (hjmDataStore.getHjmMode()) {
        val uuid = UUID.randomUUID().toString()
        interceptorManager.sendEventAtInterceptorEvent(uuid, response)

        startHjmActivityIfNeeded()

        response = interceptorManager.resultEvent.filter { it.first == uuid }.first().second
    }

    return@runBlocking response
}

class InterceptorManager {
    val isHjmActivityRunning = AtomicBoolean(false)

    private val _interceptorEvent = MutableSharedFlow<Pair<String, Response>>(replay = 1)
    val interceptorEvent = _interceptorEvent.asSharedFlow()

    private val _resultEvent = MutableSharedFlow<Pair<String, Response>>()
    val resultEvent = _resultEvent.asSharedFlow()

    suspend fun sendEventAtInterceptorEvent(uuid: String, response: Response) {
        _interceptorEvent.emit(Pair(uuid, response))
    }

    suspend fun sendEventAtResultEvent(uuid: String, response: Response) {
        _resultEvent.emit(Pair(uuid, response))
    }
}

Channel과 SharedFlow의 주요 차이점

 1. 데이터 공유 방식:

  • Channel: point-to-point 통신 방식으로, 하나의 sender와 하나의 receiver가 연결되어 데이터를 주고받습니다. 즉, 각각의 collector는 독립적인 데이터 스트림을 받습니다.
    SharedFlow: broadcast 통신 방식으로, 하나의 sender가 여러 receiver에게 동일한 데이터를 전송합니다. 즉, 모든 collector가 동일한 데이터 스트림을 공유합니다. 

2. 데이터 버퍼링:

  • Channel: 버퍼 크기를 지정할 수 있으며, 버퍼가 가득 차면 sender는 receiver가 데이터를 가져갈 때까지 대기합니다. (blocking)
  • SharedFlow: replay, extraBufferCapacity 설정을 통해 버퍼링 동작을 제어할 수 있습니다. 기본적으로 최신 데이터만 유지하며, 이전 데이터는 버려집니다. (non-blocking) 

3. 사용 용도:

  • Channel: 주로 coroutine 간의 직접적인 통신에 사용됩니다. 예를 들어, producer coroutine에서 생성한 데이터를 consumer coroutine으로 전달하는 데 사용할 수 있습니다.
  • SharedFlow: 여러 coroutine 또는 컴포넌트가 동일한 데이터를 관찰해야 하는 경우에 유용합니다. 예를 들어, 앱의 상태 변경을 여러 화면에서 관찰해야 하는 경우에 사용할 수 있습니다. 

4. Cold vs. Hot:

  • Channel: cold stream으로, collector가 collect를 호출할 때마다 새로운 스트림이 생성됩니다.
  • SharedFlow: hot stream으로, collector의 존재 여부와 관계없이 데이터를 방출합니다