ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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,
                    )
                } // 모든 아티스트 버튼 UI

     

    StateFlow 를 사용하는 방법은 아래와 같다.

        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
Designed by Tistory.