시배's Android

Hi Jack Mocker | 개선기 (3) feat.channel data loss 본문

Android/Hi Jack Mocker

Hi Jack Mocker | 개선기 (3) feat.channel data loss

si8ae 2024. 7. 13. 19:00

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의 send와 consumeEach 동시 호출 시 데이터 유실 문제

문제 상황

private var interceptorChannel = Channel<Response>(UNLIMITED)
private var resultChannel = Channel<Response>(UNLIMITED)
val isHjmActivityRunning = AtomicBoolean(false)

@OptIn(DelicateCoroutinesApi::class)
suspend fun sendWithInterceptorChannel(response: Response) {
    if (interceptorChannel.isClosedForSend) {
        interceptorChannel = Channel(UNLIMITED)
    }
    interceptorChannel.send(response)
}

@OptIn(DelicateCoroutinesApi::class)
suspend fun consumeEachInterceptorChannel(action: (Response) -> Unit) {
    if (interceptorChannel.isClosedForReceive) {
        interceptorChannel = Channel(UNLIMITED)
    }

    interceptorChannel.consumeEach {
        action(it)
    }
}

Hi Jack Mocker 라이브러리는 OkHttp3를 통해 네트워크 응답을 가로채어 이를 모킹하고 UI에 전달합니다. 하지만 간헐적으로 네트워크 요청이 3개가 send 되었지만 1~2개만 receive 되는 문제가 발생하였습니다.

데이터 유실의 원인

초기에는 send와 receive 과정에서 동시성 문제라고 생각하여 mutex를 추가하였습니다.

@OptIn(DelicateCoroutinesApi::class)
suspend fun sendWithInterceptorChannel(response: Response) {
    mutex.withLock {
        if (interceptorChannel.isClosedForSend) {
            interceptorChannel = Channel(UNLIMITED)
        }

        interceptorChannel.send(response)
    }
}

@OptIn(DelicateCoroutinesApi::class)
suspend fun receiveAllWithInterceptorChannel(action: (Response) -> Unit) {
    mutex.withLock {
        if (interceptorChannel.isClosedForReceive) {
            interceptorChannel = Channel(UNLIMITED)
        }

        for (response in interceptorChannel) {
            action(response)
        }
    }
}

이는 문제를 해결하는 것처럼 보였습니다.

실제 원인은 이벤트 순서 문제였으며, 액티비티 종료send와 receive 이벤트 이후에 호출되어 데이터가 손실되는 것이었습니다.

  1. 모킹된 응답을 UI로 전송.
  2. 액티비티 종료 요청.
  3. 새로운 네트워크 요청 발생.

비동기 특성상 이벤트가 1, 2, 3 순서대로 처리되지 않고 1, 3, 2 순서로 처리될 수 있었기 때문입니다.

실제로 1, 3, 2 순서로 처리가되어 HjmActivity에서는 빈 리스트를 보여주는 것을 확인할 수 있습니다.

해결책 

모든 모킹된 데이터를 전송한 후, onFinishEvent라는 SharedFlow를 사용하여 true를 방출하고 이를 통해 액티비티를 종료했습니다.

그러나 ViewModel에서 인터셉터를 통해 새로운 데이터를 수신할 경우, onFinishEvent에 false를 방출하여 액티비티 종료를 막았습니다. 이때 300밀리초의 debounce를 추가하여 새로운 이벤트가 없고 true가 collect 될때만 액티비티가 종료되도록 했습니다.

LaunchedEffect(Unit) {
    hjmViewModel.onFinishEvent
        .distinctUntilChanged()
        .debounce(300)
        .collectLatest { isFinish ->
            if (isFinish) {
                interceptorManager.isHjmActivityRunning.set(false)
                finish()
            }
        }
}