-
Android Retrofit2 사용을 위한 비동기 처리Android 2023. 6. 12. 16:59
들어가기 전에
Kotlin 언어로 Retrofit 라이브러리를 사용해 외부 서버 DB 에 접근하고 데이터를 가져와 화면에 노출하는 흐름을 만들어내기 위한 비동기 처리 시행착오를 기록해보려고 한다.
Retrofit
Retrofit 은 애플리케이션에서 네트워크 작업을 수행하기 위한 라이브러리이다. Square 사에서 개발하였으며, RESTful 웹 서비스와 통신하기 위해 사용된다.
간펴하고 직관적인 API 를 제공하여 개발자가 네트워크 작업을 더욱 쉽게 처리할 수 있도록 도와준다. 주요 기능으로는 HTTP 요청을 보내는 기능과 서버로부터 받은 응답을 처리하는 기능이 있다.
네트워크 요청을 정의하기 위해 인터페이스를 생성할 수 있다. 이 인터페이스는 서버와 통신하기 위한 메서드들이 정의되는데, 각 메서드는 HTTP 요청의 유형(GET, POST, PUT, DELETE 등)과 요청에 필요한 매개변수, 응답을 처리하기 위한 콜백 등을 정의할 수 있다. 이후 Retrofit 은 이 인터페이스를 기반으로 필요한 네트워크 호출 코드를 생성해준다.
또한, HTTP 요청 및 응답을 변환하기 위해 Converter 를 사용할 수 있다. 이를 통해 요청과 응답을 JSON, XML 또는 기타 형식으로 변환할 수 있다.
Main Thread 와 LifeCycleScope
companion object { const val TAG = "MainActivity" private val homeContentsRepository: HomeContentsRepository = HomeContentsRepository() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { } lifecycleScope.launch { try { val result: NetworkTest = homeContentsRepository.getTests() Log.d(TAG, "${result.title}") } catch (e: Exception) { Log.e(TAG, "${e.message}") } } }안드로이드 UI(Activity, Fragment) 레벨에서 비동기 처리되는 데이터를 확인하고 싶다면 LifecycleScope 를 사용해 확인해야 한다.
ViewModel 레벨에서 비동기 처리되는 데이터를 확인하고 싶다면 ViewModelScope 를 사용해 확인해야 한다.
여기서 확인하고 넘어갈게 있다.
Dispatcher.IO 개념은 무엇이고 코드 단에서 어떻게 사용되는지 Logcat 에서 확인해보자
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { } lifecycleScope.launch { Log.d(TAG, "코루틴: ${Thread.currentThread().name}") } Log.d(TAG, "onCreate: ${Thread.currentThread().name}") }
위 이미지를 보듯 코루틴 함수 사용한 부분과 onCreate 함수의 사용되는 Thread 는 똑같이 main Thread 에서 동작한다.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { } lifecycleScope.launch(Dispatchers.IO) { Log.d(TAG, "코루틴: ${Thread.currentThread().name}") } Log.d(TAG, "onCreate: ${Thread.currentThread().name}") }
위 이미지에서는 코루틴 함수를 실행할 때 'Dispatchers.IO' 로 명시해주었다.
Logcat 에서 확인해보니 onCreate 함수는 main Thread 에서 실행되고 코루틴 함수는 DefaultDispatcher-worker-2 라는 새로운 백그라운드 Thread 에서 실행되는걸 확인할 수 있다.
이제 Retrofit 을 사용해보자.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent {} lifecycleScope.launch { try { val result: NetworkTest = homeContentsRepository.getTests() Log.d(TAG, "서버 정보 가져 오라고 ~~~~ ${result.title}") Log.d(TAG, "코루틴: ${Thread.currentThread().name}") } catch (e: Exception) { Log.e(TAG, "${e.message}") } } Log.d(TAG, "onCreate: ${Thread.currentThread().name}") }위 코드는 간단하게 Retrofit 인터페이스 구성을 통해 외부 서버에 요청하고 응답을 받는 코드 구조이다.
homeContentsRepository.getTests() 👉 HomeContentsRepository 👉 HomeNetworkApi 흐름으로 요청이 진행되고
HomeNetworkApi 👉 HomeContentsRepository 👉 homeContentsRepository.getTests() 흐름으로 응답이 돌아온다.
위 코드 내용을 Logcat 에서 확인해보면 아래와 같다.

코드의 순서는 lifecycleScope 가 먼저이지만 비동기 처리이기 때문에 서버가 응답을 전달했을 때 로그가 확인되고
onCreate 함수는 main Thread 에서 동작하고 있기 때문에 먼저 로그가 확인되는걸 알 수 있다.
하지만, main Thread 에서 동작하는걸 볼 수 있는데 이렇게 되면 지금이야 아주 간단한 샘플 코드를 작성해서 문제가 없겠지만 비동기 처리가 늘어나고 main Thread 에서 다른 로직이 추가되었을 때 오류가 발생할 것이다.
그래서 비동기 처리를 수행하는 lifecycleScope 는 sub Thread 로 수행되어야 한다.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent {} lifecycleScope.launch(Dispatchers.IO) { try { val result: NetworkTest = homeContentsRepository.getTests() Log.d(TAG, "서버 정보 가져 오라고 ~~~~ ${result.title}") Log.d(TAG, "코루틴: ${Thread.currentThread().name}") } catch (e: Exception) { Log.e(TAG, "${e.message}") } } Log.d(TAG, "onCreate: ${Thread.currentThread().name}") }위 코드 내용을 Logcat 에서 확인해보면 아래와 같다.

이전과는 다르게 main Thread 에서 동작하는게 아닌 코루틴 함수를 위한 새로운 Thread name 을 가지고 동작하는걸 볼 수 있다. 그리고 main Thread 가 동작이 완료되기 전에 서버 응답 값을 로그에서 확인할 수 있다.
Dispatchers.IO 란 무엇인가.
Kotlin 의 코루틴 라이브러리에서 제공하는 디스패처(dispatcher) 중 하나이다.
코루틴은 비동기 작업을 효율적으로 처리하기 위한 Kotlin의 기능이다. 코루틴을 사용하면 비동기 코드를 동기식으로 작성할 수 있으며, 콜백 헬(callback hell)과 같은 문제를 피할 수 있다.
Dispatchers.IO 디스패처는 I/O 작업에 최적화된 디스패처이다. 주로 네트워크 요청이나 파일 입출력과 같은 I/O 작업에 사용된다. 이 디스패처는 백그라운드 Thread 풀에서 작업을 실행하며, I/O 작업에 적합한 Thread 개수를 유지한다.
Dispatchers.IO 를 사용하면 I/O 작업을 수행하는 동안 Main Thread 를 차단하지 않고 비동기적으로 작업을 처리할 수 있다. 이는 애플리케이션의 Responsiveness(응답성)을 유지하면서도 I/O 작업의 성능을 향상시킬 수 있다.
viewModelScope 사용
UI 레벨(Activity, Fragment) 에서 lifecycleScope 를 사용해 Main Thread 를 차단하지 않고 비동기 처리를 수행한 것과 같이 viewModel 단에서는 viewModelScope 를 사용해 Main Thread 를 차단하지 않고 비동기 처리를 수행해보겠다.
Button( onClick = { viewModel.getNetworkTest() }, modifier = Modifier .padding(bottom = 40.dp) .fillMaxWidth(), shape = RoundedCornerShape(8.dp) ) fun getNetworkTest() { viewModelScope.launch(Dispatchers.IO) { try { val result: NetworkTest = homeContentsRepository.getTests() Log.d(TAG, "서버 정보 가져 오라고 ~~~~ ${result.title}") Log.d(TAG, "코루틴: ${Thread.currentThread().name}") } catch (e: Exception) { Log.e(TAG, "${e.message}") } } }위 코드를 작성하고 Button 을 클릭했을 때, 백그라운드 Thread 에서 실행될 것 같았다.
하지만 Logcat 에서 아무런 로그도 확인할 수 없었다. 이유는 Button 클릭 이벤트 핸들러('onClick')가 메인(UI) Thread 에서 실행되기 때문이다. 그래서 Button 클릭 이벤트 핸들러에서 'viewModel.getNetworkTest()' 를 호출하면, 'getNetworkTest()' 함수의 코루틴 블록도 메인(UI) Thread 에서 실행된다.
방법이야 여럿있겠지만 내가 오류를 해결한 방법은 아래와 같다.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val viewModel: HomeViewModel = viewModel( factory = HomeAndroidViewModelFactory(application), ) HomeView(viewModel = viewModel) } }Activity 를 실행할 때 Application Context 값을 viewModel 에 전달하는 것이다.
그렇게 되면 Activity 와 ViewModel 단에서 Application Context 를 공유할 수 있고 이제 'viewModelScope.launch'를 호출하여 'getNetworkTest()' 함수를 백그라운드 Thread 에서 실행할 수 있다. 이렇게 하면 I/O 작업이 메인(UI) Thread를 차단하지 않고 비동기적으로 처리될 수 있다.
아래는 위 코드로 수정 후 동작한 Logcat 화면이다.

ViewModel 👉 View 로 데이터를 전달
ViewModel 에서 비동기적으로 처리한 데이터를 View 로 전달하는 방법은 Android Jetpack 의 일부로 제공되는 LiveData와 Kotlin의 StateFlow, RxJava, EventBus 등 다양한 라이브러리를 사용하여 비동기적으로 처리된 데이터를 View 로 전달할 수 있다. 선택한 라이브러리에 따라 사용 방법이 다를 수 있으니 해당 라이브러리의 문서나 예제를 참고하면 좋다.
데이터 관찰 기능을 사용하면 데이터의 변경을 관찰하고 해당 변경을 View 에 자동으로 알려주는 메커니즘을 제공한다.
viewModelScope 와 Compose State(상태변수) 를 사용하는 방법은 아래와 같다.
private val _data = mutableStateOf<String>("") val data: State<String> = _data fun getNetworkTest() { viewModelScope.launch(Dispatchers.IO) { val result: NetworkTest = homeContentsRepository.getTests() _data.value = result.title Log.d(TAG, "getNetworkTest: ${result.title}") } } // viewModel 에서 비동기 처리를 수행하고 가져온 데이터를 View 에 변수로 저장 val textState = viewModel.data.value Button( onClick = { viewModel.getNetworkTest() // 비동기 처리 }, modifier = Modifier .padding(bottom = 40.dp) .fillMaxWidth(), shape = RoundedCornerShape(8.dp) ) { Text( text = textState, // 비동기 처리 후 가져온 데이터 세팅 color = Title, fontWeight = FontWeight(500), fontSize = 16.sp, textAlign = TextAlign.Center, ) } // 모든 아티스트 버튼 UIStateFlow 를 사용하는 방법은 아래와 같다.
private val _data = MutableStateFlow(NetworkTest("")) val data: StateFlow<NetworkTest> = _data.asStateFlow() companion object { const val TAG = "HomeViewModel" } fun getNetworkTest(): Flow<Result<NetworkTest>> = flow { kotlin.runCatching { Log.d(TAG, "서버 통신 Flow 로 성공했어!!!!!!!!!!!!!!!!") val result: NetworkTest = homeContentsRepository.getTests() emit(Result.Success(result)) }.onFailure { throwable -> Log.e(TAG, "서버 통신 Flow 로 실패했어!!!!!!!!!!!!!!!!!!", throwable) emit(Result.Failure(throwable)) } } // viewModel 에서 비동기 처리를 수행하고 가져온 데이터를 View 에 변수로 저장 val textState = viewModel.getNetworkTest().collectAsState(initial = "") Button( onClick = { viewModel.getNetworkTest() // 비동기 처리 }, modifier = Modifier .padding(bottom = 40.dp) .fillMaxWidth(), shape = RoundedCornerShape(8.dp) ) { Text( text = "${textState.value}", // 비동기 처리 후 가져온 데이터 세팅 color = Title, fontWeight = FontWeight(500), fontSize = 16.sp, textAlign = TextAlign.Center, ) } // 모든 아티스트 버튼 UI아래는 Logcat 에서 확인한 Flow 통신 로그와 View


데이터를 래핑해서 사용했기 때문에 래핑을 풀고 실제 값만 사용하게 변경하는 작업이 필요하다.
'Android' 카테고리의 다른 글
Android Studio 멀티 모듈과 Gradle Wrapper (0) 2023.05.15 Jetpack compose 레이아웃 - ② (0) 2023.05.12 Jetpack compose 레이아웃 - ① (0) 2023.05.09 Jetpack compose State 심화 (0) 2023.05.08 Jetpack compose Navigation (0) 2023.05.04