Android의 Kotlin 코루틴

코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 코루틴은 Kotlin 버전 1.3에 추가되었으며 다른 언어에서 확립된 개념을 기반으로 합니다.

Android에서 코루틴은 기본 스레드를 차단하여 앱이 응답하지 않게 만들 수도 있는 장기 실행 작업을 관리하는 데 도움이 됩니다. 코루틴을 사용하는 전문 개발자 중 50% 이상이 생산성이 향상되었다고 보고했습니다. 이 주제에서는 Kotlin 코루틴을 사용하여 이러한 문제를 해결해 더 명확하고 간결한 앱 코드를 작성하는 방법을 설명합니다.

기능

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

  • 경량: 코루틴을 실행 중인 스레드를 차단하지 않는 정지를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있습니다. 정지는 많은 동시 작업을 지원하면서도 차단보다 메모리를 절약합니다.
  • 메모리 누수 감소: 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행합니다.
  • 기본 제공 취소 지원: 취소는 실행 중인 코루틴 계층 구조를 통해 자동으로 전파됩니다.
  • Jetpack 통합: 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있습니다. 일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공합니다.

예시 개요

이 주제의 예에서는 앱 아키텍처 가이드에 따라 네트워크 요청을 보내고 결과를 기본 스레드로 반환합니다. 그러면 앱에서 결과를 사용자에게 표시할 수 있습니다.

특히 ViewModel 아키텍처 구성요소는 기본 스레드의 저장소 레이어를 호출하여 네트워크 요청을 트리거합니다. 이 가이드에서는 코루틴을 사용하는 다양한 솔루션을 반복하여 기본 스레드를 차단 해제 상태로 유지합니다.

ViewModel에는 코루틴과 직접 연동되는 KTX 확장 프로그램 집합이 포함됩니다. 이러한 확장 프로그램은 lifecycle-viewmodel-ktx 라이브러리이며, 이 가이드에서 사용됩니다.

종속 항목 정보

Android 프로젝트에서 코루틴을 사용하려면 앱의 build.gradle 파일에 다음 종속 항목을 추가합니다.

Groovy

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

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

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

기본 스레드에서 네트워크 요청을 보내면 응답을 받을 때까지 스레드가 대기하거나 차단됩니다. 스레드가 차단되는 경우 이로 인해 OS는 onDraw()를 호출할 수 없으므로 앱이 정지되고 애플리케이션 응답 없음(ANR) 대화상자가 표시될 수 있습니다. 사용자 환경을 개선하기 위해 백그라운드 스레드에서 이 작업을 실행해 보겠습니다.

먼저 Repository 클래스를 살펴보고 네트워크 요청 방식을 확인하겠습니다.

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

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"))
    }
}

makeLoginRequest가 동기식이며 호출 스레드를 차단합니다. 네트워크 요청의 응답을 모델링하기 위해 자체 Result 클래스를 사용합니다.

ViewModel은 사용자가 예를 들어 버튼을 클릭할 때 네트워크 요청을 트리거합니다.

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

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

위의 코드에서 LoginViewModel은 네트워크 요청을 보낼 때 UI 스레드를 차단합니다. 이 실행을 기본 스레드 외부로 이동하는 가장 간단한 방법은 새로운 코루틴을 만들고 I/O 스레드에서 네트워크 요청을 실행하는 것입니다.

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 함수에서 코루틴 코드를 분석해 보겠습니다.

  • viewModelScopeViewModel KTX 확장 프로그램에 포함된 사전 정의된 CoroutineScope입니다. 모든 코루틴은 범위 내에서 실행해야 합니다. CoroutineScope는 하나 이상의 관련 코루틴을 관리합니다.
  • launch는 코루틴을 만들고 함수 본문의 실행을 해당하는 디스패처에 전달하는 함수입니다.
  • Dispatchers.IO는 이 코루틴을 I/O 작업용으로 예약된 스레드에서 실행해야 함을 나타냅니다.

login 함수는 다음과 같이 실행됩니다.

  • 앱이 기본 스레드의 View 레이어에서 login 함수를 호출합니다.
  • launch가 새 코루틴을 만들며, I/O 작업용으로 예약된 스레드에서 독립적으로 네트워크 요청이 이루어집니다.
  • 코루틴이 실행되는 동안 네트워크 요청이 완료되기 전에 login 함수가 계속 실행되어 결과를 반환합니다. 편의를 위해 지금은 네트워크 응답이 무시됩니다.

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

위의 예에서 한 가지 문제는 makeLoginRequest를 호출하는 모든 항목이 명시적으로 실행을 기본 스레드 외부로 이동해야 한다는 점입니다. 이 문제를 해결하기 위해 Repository를 수정하는 방법을 알아보겠습니다.

기본 안전을 위해 코루틴 사용

기본 스레드에서 UI 업데이트를 차단하지 않는 함수를 기본 안전 함수로 간주합니다. 기본 스레드에서 makeLoginRequest를 호출하면 UI가 차단되므로 makeLoginRequest 함수는 기본 안전 함수가 아닙니다. 코루틴 라이브러리의 withContext() 함수를 사용하여 코루틴 실행을 다른 스레드로 이동합니다.

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의 방법입니다.

다음 예에서는 LoginViewModel에 코루틴을 만듭니다. makeLoginRequest가 실행을 기본 스레드 외부로 이동하므로, 이제 login 함수의 코루틴이 기본 스레드에서 실행될 수 있습니다.

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

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

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

makeLoginRequestsuspend 함수이므로 코루틴은 여전히 필요하며, 모든 suspend 함수는 코루틴에서 실행되어야 합니다.

이 코드는 위의 login 예와 몇 가지 차이점이 있습니다.

  • launchDispatchers.IO 매개변수를 사용하지 않습니다. Dispatcherlaunch에 전달하지 않으면 viewModelScope에서 실행된 코루틴은 기본 스레드에서 실행됩니다.
  • 네트워크 요청의 결과가 이제 성공 또는 실패 UI를 표시하도록 처리됩니다.

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

  • 앱이 기본 스레드의 View 레이어에서 login() 함수를 호출합니다.
  • launch가 기본 스레드에 새 코루틴을 만들고 코루틴이 실행을 시작합니다.
  • 코루틴 내에서 이제 loginRepository.makeLoginRequest() 호출은 makeLoginRequest()withContext 블록 실행이 끝날 때까지 코루틴의 추가 실행을 정지합니다.
  • withContext 블록이 완료되면 login()의 코루틴이 네트워크 요청의 결과와 함께 기본 스레드에서 실행을 재개합니다.

예외 처리

Repository 레이어에서 발생할 수 있는 예외를 처리하려면 Kotlin에서 기본으로 제공되는 예외 지원을 사용하세요. 다음 예에서는 try-catch 블록을 사용합니다.

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

    fun login(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
            }
        }
    }
}

이 예에서는 makeLoginRequest() 호출에 의해 발생한 예기치 않은 예외가 UI에서 오류로 처리됩니다.

추가 코루틴 리소스

Android의 코루틴에 관한 자세한 내용은 Kotlin 코루틴으로 앱 성능 향상을 참조하세요.

코루틴 리소스를 더 보려면 다음 링크를 참조하세요.