kotlin - Suspension functions can be called only within coroutine body 코루틴
0. 서론
특정 화면에서 이런저런 API를 사용하고, 두개의 데이터를 받아올 때 까지 기다린다음 해야하는 기능들도 있어서, 구현중 비동기 처리를 하면서 왜 이건 되고 이건 안될까? 하는
의문이 있었음..
왜 이건 안될까의 하나는 ⬇️
android.os.NetworkOnMainThreadException
Suspension functions can be called only within coroutine body
withContext(Dispatchers.Main) {} 했잖아!!!!
그래도 백스레드에서 메인스레드 동작들을 하지말라는 오류가 계속 나옴.
근데 아에 CoroutineScope로 빼서 Main에서 동작하면 또 됨. 근데 이 부분에서 동기처리로 되는건지는 잘 모르겠음
한 마디로 기초 쌓아서 배운게 아니라 그냥 기능 구현하면서 짤막하게 아는 토막상식 밖에 없는 상태라서 배워보는 코루틴
거의 대부분이 suspend fun의 결과를 어떻게 처리하냐 부터 시작했었기 때문에 이 부분에서 부터 시작함.
1. 본론
(1) 내가 아는 것들의 개념들을 정리해보기
1) lifecycleScope
얘는 내가 알기로 Activity의 생명주기를 따른다고 했음. 여기서 시작한 코드들은 뭐 유지된다거나 시작된다거나 해도, 액티비티가 Destroy되면 같이 종료됨.
2) viewLifecycleScope
이건 액티비티가 아니라 특정 view의 라이프싸이클인것 같은데 얘부터 어떤 시점인지가 좀 애매했음
3) CoroutineScope
얘는 코루틴스코프라는 액티비티의 생명주기를 따르지 않는? 그니까 별개의 생명주기를 가지고 동작을 하게끔 한다고 이해했음.
4) withContext(Dispatchers)
요 놈은 Scope안에서 백->메인, 메인->백으로 옮겨가고 싶을 때 사용한다고 이해함. 근데 여기서 내가 예상과 다르게 동작하는 부분이 있었음.
5 - 1) 스레드
Dispatchers.IO -> 백스레드 Dispatchers.Main -> 메인 , Dispatchers.Default 디폴트였나? 얘는 잘 안쓰는데, 그냥 시스템 설정에 따르는 거였나? 기억이 안남.
아무튼 이렇게 가 전분데
(2) 내가 맏딱트린 상황들
1) callback 사용 상황
suspend fun fetchDataWithCallback(callback: (String) -> Unit) {
withContext(Dispatchers.IO) {
val data = api.getData() // IO에서 실행
callback(data)
}
}
suspend fun 그니까 함수를 쓸 때 이부분에서 callback(data)로 넘겨줬을 때
fun updateUI() {
lifecycleScope.launch(Dispatchers.IO) {
fetchDataWithCallback() { data ->
binding.exampleText.text = data
}
}
}
이렇게 하면 왜 오류가 나오는지 의문이었음.
여기서 이렇게 withContext를 하면 오류가 나옴. 이유는 코루틴 바디 안에서 써라는 말.
fun updateUI() {
lifecycleScope.launch(Dispatchers.IO) {
fetchDataWithCallback() { data ->
withContext(Dispathers.Main) {
binding.exampleText.text = data
}
}
}
}
여기서 왜 사용이 되지 않는걸까.. 🤔 했는데
알아봤더니
이 fragment든 이 suspend fun을 부르는 곳에서 먼저
suspend fun fetchDataWithCallback(callback: (String) -> Unit) {
withContext(Dispatchers.IO) {
val data = api.getData() // IO에서 실행
withContext(Dispatchers.Main) {
callback(data)
}
}
}
이렇게 반환할 때 데이터야 ~ Main에 가서 잘 살아라~ (동물농장 엔딩 멘트 짤) 라고 해주면 해결... 아니 이 간단한 걸 왜 몰랐을까
callback이 일어나는 곳에서는 이 스레드가 바뀌나? 이런 의문이 있었는데 해결 !
2) 코루틴이 있는데 lifecycleScope, viewLifecycleScope의 용도
개념 | 끝나는 시점 | |
CoroutineScope | Coroutine이라는 비동기 작업을 쉽게 처리할 수 있도록 도와주는 기능인데, 이 기능의 범위를 통제하는 것 | 직접 끝나는 곳 |
lifecycleScope | Activity나 Fragment의 lifecycle과 연결된 Coroutine임 | onDestroy() |
viewLifeCycleScope | Fragment에서 그 View가 생성된 동안에만 코루틴이 유지됨. | onDestroyView() |
gpt가 보통 fragment나 activity에서는 lifecycleScope나 viewlifecyclerScope를 사용해서 자동으로 메모리 누수를 방지하는게 좋다고 함. 맞는 말임.
코루틴에 들어가 있는 함수가 특정 상황에서 무한 요청을 보내버린다면? 걍 똥개발자임 ㅋㅋ 그렇기 때문에 activity가 종료될 때 (빡종) 자동으로 캔슬되게끔 하는 게 중요하긴함
근데 여기서 예외가 ViewModel에 관련된 것임.
VM은 Activity가 finish되지 않는이상 해당 액티비티위에서 공유되는데 특정 작업이 액티비티가 종료되도 즉시 종료되지 않아야한다면?
ViewModel의 생명주기를 따르는 ViewModelScope를 사용해야 함
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { api.getData() }
_uiState.value = data
}
}
}
3) Coroutine의 launch와 async차이
여기서 내가 많이 안 써본 (launch만 있는 줄 알았음) async가 나오는데 나는 okhttp3를 주로 쓰다보니 await async라는 단어에 익숙치 않는데,, 프론트엔드 에서 많이쓰지 않나..? 아무튼 잘 모름
근데 이 async라는 건 이 코루틴에서 결과를 기다리는 기능을 제공한다고 함. launch는 결과를 기다리지 않음.
lifecycleScope.launch {
val result = async { fetchData() }
withContext(Dispatchers.Main) {
binding.tvExampleText.text = "${result.await()}"
}
}
이렇게 하면 tvExampleText라는 view의 텍스트를 result가 반환될 때 까지 기다렸다가 사용할 수 있다는 점.
fun main() = runBlocking {
val time = measureTimeMillis {
val deferred1 = async { getData1() }
val deferred2 = async { getData2() }
val result1 = deferred1.await()
val result2 = deferred2.await()
}
}
해당 방식처럼 동시에 두가지 함수를 실행할 수도 있다는 설명.
함수 getData1, getData2 가 동시에 실행됨
그리고 마지막
4) execute와 enqueue의 차이
enqueue를 쓸 때는 withContext(Dispatchers.IO)를 안씀. 왜 그럴까 봤는데
메소드 | 특징 | 스레드 | 비동기/동기 | 콜백 |
execute | 동기 호출(응답을 받을 때 까지 대기) | 현재 스레드 그대로 | 동기 | ❌ |
enqueue | 비동기 호출(백그라운드에서 실행) | OkHttp의 내부 스레드 | 비동기 | ⭕ |
execute는 호출한 스레드를 막음 (Default) 그렇기 때문에 Dispatchers.IO와 함께 써야 백그라운드에서 실행하고 응답을 기다림. Dispatchers.IO를 쓰지 않으면 앱이 멈춰있음 ㅋㅋ
enqueue는 내부적으로 알아서 백그라운드에서 실행됨. 결과는 콜백으로 받아옴. 그렇기 때문에 UI 업데이트는 무조건 runonUiThread나 뭐 코루틴을 사용해서 MainThread에서 사용해야함.
대부분 기능 구현하면서 이해했다고 생각했는데 특정 상황에서 내가 생각했던 것과 다르게 동작하면 진짜 개빡이었는데 드디어 해결
다음은 LiveData에 대해서 좀 다뤄봐야 겠다.. 특히 Observe
'개발 > android_kotlin' 카테고리의 다른 글
kotlin FFmpeg 종료에 대응하는 Media3 다뤄보기 (2) | 2025.04.13 |
---|---|
kotlin jetpack compose 컴포즈 기본 활용하기 (0) | 2025.03.09 |
kotlin OKHttp3 토큰 자동 갱신 요청 (0) | 2025.01.31 |
kotlin Error occurred while mirroring video: ffmpeg version v4.5-dev-2008-g90da43557f / FFMpeg 안드로이드 동영상 좌우 반전처리 (0) | 2025.01.30 |
kotlin 안드로이드 File 손상된 파일 다운로드 받기 (0) | 2025.01.29 |