Android/트러블슈팅

Channel을 이용하여 팝업 메세지 구현하기 ( send & trySend )

Seoplee 2022. 4. 13. 21:43

서버를 통해 FCM으로 데이터를 보내면, 클라이언트에서 해당 데이터를 Channel에 넣고, view에서 Channel에서 데이터를 꺼내와 팝업 레이아웃을 보여주는 기능을 구현하려고 하였다.

그러나 데이터가 들어오는것을 전부 팝업으로 띄울 수는 없다고 생각했고, (데이터가 계속해서 넘어오면 처리속도보다 발행속도가 빨라 매우 오래 처리하게 되므로) Channel에 버퍼를 할당하면 되는 것이라 생각했다.

일단 팝업 메세지를 관리할 Channel을 만들고 capacity를 할당하였다.

class MessageChannel {

    val channel = Channel<PopupMessage>(capacity = 3)

    suspend fun sendToChannel(title: String?, message: String?) {
        val data = PopupMessage(title = title, message = message)
        data.title?.let { channel.send(data) }
    }

    data class PopupMessage(
        val title : String?,
        val message : String?
    )
}

그리고 FCM으로 데이터가 들어오면 sendToChannel을 통해 데이터를 넘겨주고, 다음과 같이 view에서 Channel의 값을
불러와 팝업 애니메이션(motion layout)을 실행하였다.

override fun onMessageReceived(remoteMessage: RemoteMessage) {
   val title = remoteMessage.data["articleTitle"]
   val description = remoteMessage.data["description"]
   val roomId = remoteMessage.data["roomId"]

   // 해당 채팅방에 접속중이라면 push 알림을 받지 않는다
   if(myPreferenceManager.getSocketStatus() && myPreferenceManager.getRoomId() == roomId) { 
       Log.i(TAG,"Room Connected: RoomId = $roomId")
   } else {
       Log.i(TAG,"Room Unconnected")
        GlobalScope.launch {
            messageChannel.sendToChannel(title, message)
        }
   }.
}
lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            for (data in messageChannel.channel) {
                binding.popupArticleTitleTextview.text = data.title
                binding.popupMessageTextView.text = data.message

                showAniamtion() // 팝업을 보여주는 애니메이션
            }
        }
    }
  • repeatOnLifeCycle은 해당 뷰가 onDestroy가 호출되지 않아도 중단시키기 위해 (백그라운드 동작 X) 사용하였다.

처음 의도는 capacity를 3으로 설정했으므로 채널에 데이터가 3개까지만 쌓이고, 나머지는 버리는 방식으로 동작할 줄 알았으나..
위 코드는 Channel에 데이터를 넣는쪽과 쓰는쪽이 모두 suspend 함수에서 동작하므로 그럴 수 없다.

위 코드의 흐름은 다음과 같다

  1. Channel에 데이터를 넣을 때 1번 suspend 함수 안에서 데이터를 넣는다.
  2. view에서 Channel에 데이터가 들어오자 마자 바로 2번 suspend 함수로 데이터를 꺼내온다.
  3. 2번 suspend 함수가 끝날 때 까지 1번의 suspend는 말 그대로 '일시중단' 상태가 되므로 데이터를 넣지 않고 대기한다.
  4. 2번 suspend 함수가 끝나면 다시 1번 suspend 함수가 깨어나 데이터를 다시 넣고, 이를 반복한다.

데이터 발행도 ,처리도 별개의 suspend 내부에서 동작하기 때문에, 의도한 데이터 처리중에 들어오는 데이터를 일부분 버린다. 가 동작하지 않는다.

trySend

이럴 떄 사용할 수 있는게 Channel의 trySend 메서드다. (기존에는 offer라는 이름으로 제공되었으나 deprecated 됨)

이 메서드는 Channel에 데이터를 넣을 때 suspend 하지 않는다. 즉 앞선 과정에서 2번 suspend의 종료를 기다리지 않고, 데이터를 설정한 capacitry만큼 계속 넣고, capacity가 초과되면 버린다.

suspend fun trySendToChannel(title: String?, message: String?) {
    val data = PopupMessage(title = title, message= message)

    data.title?.let { channel.trySend(data).isSuccess }
}

trySend를 활용하면 애니메이션을 보여주는 suspend 함수가 종료되기 전에 capacity가 차게되면 해당 값들을 버릴 수 있으므로, 처음 원했던 대로 동작한다.

(+ 별개로 메세지를 버퍼에 넣고 일부만 보여주는게 아니라, 팝업이 떠있는 상태에서 새로운 데이터가 올 경우 바로 해당 내용을 변경하려면 그냥 showAnimation을 별도의 코루틴스코프에서 실행하면 된다.)