1. 기본적인 채팅(리스트) 구조
채팅 리스트 구조를 들어가기 전에 간단한 채팅에서 사용할 리스트 구현에 대해서 살짝 설명해보고자 합니다. 기본적으로 채팅을 위한 리스트는 다음처럼 구현이 가능합니다.
액티비티 구현 → List 인터페이스를 구현한 자료형을 사용해서 리스트구현 → Adapter 설정 → Adapter 리스트 적용 → 리스트 아이템 추가 & Adapter 갱신
과 같은 순서로 채팅을 위한 리스트 사전작업을 합니다.
리스트 구현 참고 : https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ko
간단한 채팅을 위한 리스트 예시
val list = mutableListOf<String>()
lateinit var listAdapter: CustomListAdapterfun setRecyclerView() {
listAdapter = CustomListAdapter(list)
binding.recyclerView.run {
adapter = listAdapter
layoutManager = LinearLayoutManager(this@MainActivity)
}
}fun addData() {
list.add("아이템")
listAdapter.notifyItemInserted(list.size - 1)
}
2. 스푼 채팅 리스트 구조
먼저, 당연하게도(?) 스푼 채팅 구조는 스푼 서버에서 보내온 채팅 데이터를 리스트에 담아서 화면에 보여줍니다.
방송을 하는 라이브 화면에 많은 기능들에 대해서 얘기를 하고싶지만, 채팅 리스트 구조에 대해서만 얘기를 하겠습니다.
스푼 채팅 리스트에서는 사용자의 입장 여부, DJ 에게만 보여줘야하는 정보, 청취자에게 보여줘야하는 정보 등 세밀하게 사용자에 맞춰서 데이터를 보여주고 있습니다. 다양한 데이터와 채팅 정보를 보여주다보니 리스트에 I/O 많이 발생해서 데이터 추가에 적합한 자료구조인 LinkedList 를 사용해서 리스트를 관리합니다. 그리고 채팅 리스트는 채팅이 올때마다 아이템을 하나씩 추가하면서 화면을 갱신하고 있습니다.
fun addData() {
list.add("이것은 채팅이고, 스푼라디오 스푼라디오 짱짱이다")
listAdapter.notifyItemInserted(position)
}
여기서 채팅을 하나씩 갱신하는 부분에서 개선할 부분이 발견되었습니다.
스푼라디오 같은 실시간 방송 서비스는 일반 메신저 서비스와 다르게 청취자들의 채팅이 물밀 듯이 밀려올 수 있는 부분에 대한 처리를 생각해야 합니다.
많은 수의 채팅이 쏟아져 올 때는 채팅 하나하나를 리스트에 추가하고 레이아웃을 갱신할 경우에 성능이 빵빵한 최첨단 하이테크 스마트폰이 아니고 5년 전쯤이나 나온 성능이 부족한 스마트폰을 만나게 되면 UI가 버벅대는 렌더링 이슈를 만나게 될겁니다. 혹은 채팅이 리스트에 쌓여서 실시간으로 내용전달이 되지 못하고 다른 사람들의 채팅에 밀려서 내가 입력한 채팅을 다른 사람들은 10초 뒤에나 만나게 될지도 모릅니다.
3. 스푼 채팅 개선 구조
이번에 진행하는 채팅 개선은 서버에서 자사 서비스에 맞게 적용된 알고리즘에 의해 정제된 데이터를 보내주는것과 별개로 안드로이드에서 채팅 리스트를 원활하게 보여주기 위한 고민에 의해서 작업을 진행했습니다
채팅 리스트에서 많은 채팅이 몰린다면 UI 렌더링 이슈가 존재할 수 있기에 채팅 메세지를 처리하기 전에 채팅을 Queue 에 담아서 일정 시간 혹은 일정 양에 따른 조건에 맞춰서 데이터를 한번에 쏟아내서 보내는 형태로 변경해서 뷰를 갱신하는 회수를 줄여봤습니다.
리스트에 모든 데이터를 표시하는 시간을 측정을 했습니다. 20m/s 보다 짧은 간격으로 데이터를 보낼 경우에 리스트의 갱신이 속도를 따라가지를 못해서 20m/s 측정 했습니다.
테스트 디바이스 : SM-G973N
OS : Android 9.0
Queue 구조를 통해서 한번씩 레이아웃을 갱신하게 되는 이유는 기존 방식으로 채팅이 하나씩 추가될때마다 Adapter 의 notifyItemChanged(), notifyItemInserted() 등을 호출해서 뷰를 빈번하게 갱신시키게 될것이고 뷰의 갱신 횟수가 올라가면 당연히 렌더링에 부하가 가기때문에 예상치 못한 문제가 생깁니다. 리스트에 추가되는 데이터의 양이 적다면 문제가 발생하지 않지만, 데이터가 기하급수적으로 늘어난다면 렌더링 이슈는 필수 불가결하게 발생하게 됩니다.
즉, 갱신 회수를 줄이기 위해서 메시지의 갱신 개수와 회수에 대한 조절 작업을 했습니다.
테스트에서는 Queue 를 이용해서 데이터를 100ms 간격으로 UI 갱신했습니다. 데이터를 표시하는 간격은 20ms 부터 시작해서 100ms 까지 테스트를 하였으나 사람의 눈으로 쫓아가기에는 역시나 부담이 있는 시간입니다. 채팅의 양과 채팅이 어떻게 표현해야하는지의 정책에 따라서 UI 갱신 속도를 정해야할것 같습니다.
제가 테스트를 진행했을때는 적어도 500ms 정도의 시간이 되어야지 어느정도 눈에 인지가 되고 채팅이 눈에 들어오는 수준이 되었습니다.
동료가 추천해준 UI 성능에 관한참고 영상 : https://youtu.be/CaMTIgxCSqU
구현 예시 — 시간을 기준으로 채팅을 갱신할 경우
private val bufferMessage = LinkedList<Message>()// 리스트에 채팅을 추가합니다.
fun add(item: Message) {
bufferMessage.add(item)
}// 일정 시간 간격으로 채팅을 내보내도록 처리합니다.private fun setTimer(intervalTime: Long) {
Observable.interval(intervalTime, TimeUnit.MILLISECONDS)
.subscribe {
flush()
}
}// 리스트뷰를 관리해주는 어댑터로 로 리스트를 내보냅니다.
private fun flush() {
bufferMessageListener(bufferMessage.filter {filterCondition(it)})
bufferMesage.clear()
}private fun filterCondition(message: Message) {
// 필터가 필요하면 조건에 맞게 필터 처리를 합니다.
}
Adapter
fun addLiveChats(items: List<Message>) {
list.addAll(items)
notifyItemRangeInserted(items.size)
}
4. 끝맺음
채팅이 중요한 실시간 방송, 라디오 서비스의 경우에서는 서버쪽에서 적당한 양의 채팅을 클라이언트에게 보내줄 것입니다. 그럼에도 불구하고 실시간 방송 서비스 및 메시징을 이용하는 서비스의 경우 자사의 상황에 맞춰서 데이터 리스트를 어떻게 얼마큼 어떤 속도로 보여줄지에 대해서 고민을 할 수 밖에 없을것입니다. 이번에 채팅 리스트 개선작업을 하면서 서비스 및 엔지니어링적인 고민을 팀원들과 함께 나눌 수 있었습니다.