코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 코루틴 은 버전 1.3에서 Kotlin에 추가되었으며 다른 언어의 개념을 살펴보겠습니다.
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
함수에서 코루틴 코드를 분석해 보겠습니다.
viewModelScope
는ViewModel
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
}
}
}
}
makeLoginRequest
가 suspend
함수이므로 코루틴은 여전히 필요하며, 모든 suspend
함수는 코루틴에서 실행되어야 합니다.
이 코드는 위의 login
예와 몇 가지 차이점이 있습니다.
launch
가Dispatchers.IO
매개변수를 사용하지 않습니다.Dispatcher
를launch
에 전달하지 않으면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 코루틴으로 앱 성능 향상을 참조하세요.
코루틴 리소스를 더 보려면 다음 링크를 참조하세요.