오늘도 더 나은 코드를 작성하였습니까?

Android의 Kotlin Coroutine 본문

Coroutine

Android의 Kotlin Coroutine

hik14 2021. 4. 7. 12:17

 

코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 

코루틴은 Kotlin 버전 1.3에 추가되었으며 다른 언어에서 확립된 개념을 기반으로 합니다.

 

Android에서 코루틴은 Main 스레드(UI)를 차단하여 앱이 응답하지 않게 만들 수도 있는 장기 실행 작업을 관리하는 데 도움이 된다.

 

코루틴은 Android의 비동기 프로그래밍에 권장되는 솔루션입니다. 주목할 만한 기능은 다음과 같습니다.

 

경량: 코루틴을 실행 중인 스레드를 차단하지 않는 suspension를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있습니다. suspension는 많은 동시 작업을 지원하면서도 차단보다 메모리를 절약합니다.

 

메모리 누수 감소: 구조화된 동시 실행{코루틴안에 코루틴을 넣고 결과를 받아서 실행등이 가능함}을 사용하여 범위 내에서 작업을 실행.

 

기본으로 제공되는 취소 지원: 실행 중인 코루틴 계층 구조를 통해 자동으로 취소가 전달됩니다.( 코루틴안에 코루틴을 만들었다면, 최상위 코루틴 스코프가 취소되면 그안에 있는 모든 코루틴들은 취소된다는 소리다. )

 

Jetpack 통합: 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있습니다.

일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공합니다.

 

예시

 

*Android App 아키텍쳐 가이드에 따르면 ViewModel에서 Repository의 네트워크 요청을 하는 함수를 트리거 하는것이 일반적인다. 

 

의존성. 

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

백그라운드 스레드에서 실행

Main Thread 에서 네트워크 요청을 하면 스레드가 네트워크의 응답 결과를 받기 위해 스레드가 대기 및 정지 상태가 된다.

이는 OS가  View의 onDraw()를 호출할 수 있는 메인 스레드를 사용할수 없게되고 앱은 정지되고,  ANR에러가 발생한다.

 

 

아래 코드를 그대로 실행시키면 동기식 호출이기 때문에 메인 스레드가 차단된다.

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

//Repository
class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

//viewModel
class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

 

코루틴을 이용한 비동기식 처리로 변경.

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

login 함수에서 코루틴 코드를 분석

 

viewModelScope- ViewModel KTX 확장 프로그램에 포함된 사전 정의된 CoroutineScope입니다.

*모든 코루틴은 범위 내에서 실행해야 합니다. CoroutineScope는 하나 이상의 관련 코루틴을 관리합니다.

 

launch - 코루틴을 생성하고 함수 본문의 실행을 담당하는 디스패처에 전달하는 함수

 

Dispatchers.IO  코루틴을 I/O 작업용으로 예약된 스레드에서 실행해야 함을 알려줌. 

 

viewModelScope로 시작되므로 ViewModel 범위에서 실행됩니다.

사용자가 화면 밖으로 이동하는 것으로 인해 ViewModel이 소멸되는 경우 viewModelScope가 자동으로 취소되고 실행 중인 모든 코루틴도 취소됩니다

 

한 가지 문제는 makeLoginRequest를 처리를 비동기적으로 으로 실행을 하지만, Main 스레드에서 실행이되기 때문에 옳지 못하다. 

 

Use coroutines for main-safety

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO) 

코루틴 실행을 I/O 스레드로 이동하여 호출 함수를 기본 안전 함수로 만들고 필요에 따라 UI를 업데이트하도록 설정합니다.

 

makeLoginRequest에는 suspend 키워드도 표시됩니다. 이 키워드는 코루틴 내에서 함수가 호출되도록 강제하는 Kotlin의 방법

즉, suspend키워드가 달린 함수는 코루틴 범위내에서 실행되어야 하는 함수이다.

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // 현재까지 UI Thread에서 실행된다.
        
        // UI Thread가 코루틴의 범위를 생성한다
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // 로그인을 요청하고 결과값을 받는다. 
            val result = loginRepository.makeLoginRequest(jsonBody)

            // 사용자에게 요청 결과를 보여준다.
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

 

launch가 Dispatchers.IO 매개변수를 넣어주지 않았음

---> Dispatcher를 launch에 전달하지 않으면 viewModelScope에서 실행된 코루틴은 기본 스레드에서 실행됩니다.

네트워크 요청의 결과가 이제 성공 또는 실패 UI표기..

 

이제 로그인 함수가 다음과 같이 실행됩니다.

 

앱이 기본 스레드의 View 레이어에서 login() 함수를 호출합니다.l

 

launch가 Main스레드에서 네트워크 요청을 보낼 새 코루틴을 만들며, 코루틴이 실행을 시작합니다.

코루틴 내에서 이제 loginRepository.makeLoginRequest() 호출은 makeLoginRequest()의 withContext 블록 실행이 끝날 때까지 코루틴의 추가 실행을 정지한다.

 

withContext 블록이 완료되면 login()의 코루틴이 네트워크 요청의 결과와 함께 기본 스레드에서 실행을 재개합니다.

 

예외처리

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun makeLoginRequest(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

 

'Coroutine' 카테고리의 다른 글

Android의 코루틴 권장사항  (1) 2021.04.07
코루틴 시작  (0) 2021.04.07
Kotlin 코루틴으로 앱 성능 향상  (0) 2021.04.07