Gemstone's Devlog

Coroutine 정리 본문

Kotlin (Android)

Coroutine 정리

Gemstone 2022. 3. 28. 18:05

https://developer.android.com/kotlin/coroutines?hl=ko 

 

Android의 Kotlin 코루틴  |  Android 개발자  |  Android Developers

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

developer.android.com

 

우선 코루틴의 정의를 알아보기 위해 안드로이드 개발자 홈페이지에 들렸다.

코루틴에 대한 대략적인 설명이다...

이젠 기능에 대해서 알아보자.

1) 경량 : 실행 중인 스레드를 차단하지 않는 정지를 지원, 단일 스레드에서 많은 코루틴을 실행 가능. 정지는 많은 동시 작업을 지원하면서도 차단보다 메모리를 절약.

2) 메모리 누수 감소 : 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행

3) 기본으로 제공되는 취소 지원 : 실행 중인 코루틴 계층 구조를 통해 자동으로 취소가 전달

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

 

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

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

 

예시를 보자.

 

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 범위에서 실행된다.

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

위의 예에서 한 가지 문제는 makeLoginRequest를 호출하는 모든 항목이 명시적으로 실행을 기본 스레드 외부로 이동해야 한다는 점이다.

이 문제를 해결하기 위해 Repository를 수정하는 방법을 알아보겠다.

 

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 키워드도 표시가 된다. 이 키워드는 코루틴 내에서 함수가 호출되도록 강제하는

코틀린의 방법이다.

 

다음 예에서는 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
            }
        }
    }
}

 

다음은 예외처리에 대해서 알아보겠다.

 

Repository 레이어에서 발생할 수 있는 예외를 처리하려면 Kotlin에서 기본으로 제공되는 예외 지원을 사용해야 한다.

다음 예에서는 try-catch 블록을 사용하겠다.

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

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