한 애플리케이션에서 다양한 플랫폼의 웹툰을 볼 수 있게 해주는 애플리케이션을 개발하고 있던 도중, 비슷한 웹툰을 추천해주는 기능을 추가하기로 하였다.
firebase로 서버를 구현하였기 때문에 간단하게 Vertex AI를 사용하여 gemini 채팅 기능을 구현하였다.
activity viewModel의 init 함수에 "안녕" 이라는 메시지를 넣어 시작하려고 하였다.
private fun getMessage(text: String) {
listOfMessage.add(
Message(
message = text,
time = (if (LocalTime.now().hour < 10) "0${LocalTime.now().hour}" else LocalTime.now().hour.toString())
+ ":" + (if (LocalTime.now().minute < 10) "0${LocalTime.now().minute}" else LocalTime.now().minute.toString()),
isMe = false
)
)
_chatList.value = ArrayList(listOfMessage.reversed())
Log.d("chatList", _chatList.value.toString())
}
private fun askGemini(
text: String
) {
var response = String()
CoroutineScope(Dispatchers.IO).launch{
response = geminiModel.generateContent(text).text
}
getMessage(response)
}
init{
askGemini("안녕")
}
그러나 위의 코드로 메시지를 생성하려고 하니 생성된 메시지가 비어있었다.
로그에는 gemini api에서 받은 답변이 잘 적혀있는 것을 보아, 답변을 받는 시간과 메시지가 생성되어 화면에 보이는 시간의 차이에 문제가 있다는 것을 알 수 있었다.
따라서 나는 메시지를 받고, 화면에 보이는 작업을 동기 프로그래밍으로 코드를 작성하려고 하였다.
runBlocking과 launch 함수를 사용하여 쓰레드를 잠시 막았다가 작업이 완료되면, 메시지를 화면에 보이도록 등록하는 작업을 하려고 했다.
private fun askGemini(
text: String
){
runBlocking {
launch {
val response = getResponse(text)
getMessage(response)
}
}
}
private suspend fun getResponse(text: String) : String{
val response = geminiModel.generateContent(text).text
return response.orEmpty()
}
위의 코드는 쓰레드를 독점하여 응답을 받은 후, 메시지를 화면에 보여준다.
답변이 짧을 때는 이 코드도 잘 돌아갔지만 답변이 길 때는 ANR 에러가 발생하였다.
ANR (Application No Responding) 의 약어로 애플리케이션이 응답하지 않을 때 발생하는 에러이다. 이 에러는 안드로이드의 UI thread가 오랜 시간 동안 blocking 되었을 때 발생하는 에러이다. UI thread가 blocking 되면 사용자의 다른 입력을 처리할 수 없기 때문에 에러를 발생시킨다.
나의 코드를 보면 runBlocking을 사용하고 있는데
runBlocking은 현재 thread에서 코루틴을 만들고, 작업이 종료될 때까지 기다린다. 이러면 원래의 thread는 blocking 되기 때문에 ANR이 발생한다.
(현재 thread -> main thread)
따라서 이 문제를 해결하기 위해서 CoroutineScope을 사용했다.
CoroutineScope은 코루틴을 비동기적으로 관리한다. CoroutineScope은 현재 thread를 block하지 않는다. scope 내의 코루틴은 별도의 thread에서 실행되기 때문에, 현재 thread는 block 되지 않는다.
그리고 CoroutineScope을 사용하면 어떤 수준의 thread를 사용할지 지정할 수 있다.
위의 내용을 적용하여 아래와 같이 코드를 바꾸었다.
private fun askGemini(
text: String
) {
CoroutineScope(Dispatchers.IO).launch {
val response = getResponse(text)
withContext(Dispatchers.Main) {
getMessage(response)
}
}
}
private suspend fun getResponse(text: String): String {
val response = geminiModel.generateContent(text).text
// markdown 적용
val markwon = Markwon.create(context)
return markwon.toMarkdown(response.orEmpty()).toString()
}
init {
askGemini("안녕")
}
CoroutineScope(Dispatchers.IO) 으로 입출력 thread에서 gemini 응답을 받도록 하였다. 메시지를 화면에 보여주는 부분은 UI thread가 작업해야 하는 부분이라, Dispatchers.Main으로 thread를 변경해주었다.
withContext 함수는 코루틴 내에서 context를 변경하는데 사용하는 함수이다. 현재 코루틴을 잠시 중단하고, withContext에 정의된 명령을 수행 후 다시 원래의 context로 돌아오게 하는 역할을 한다.
이제 UI thread가 blocking 되지 않기 때문에 ANR 에러가 발생하지 않는다.