Coroutine

BongKu·2023년 6월 16일
0

Android

목록 보기
3/30
post-thumbnail

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

코루틴이란

코루틴은 비동기 프로그래밍을 위한 개념입니다. 코루틴은 스레드와 유사한 동작 방식을 가지며, 스레보다 가볍고 효율적인 비동기 작업 처리를 가능하게 해줍니다. 또한 비동기 작업을 필요한 시점에서 일시 중단하거나 재개할 수 있도록 해줍니다.

주로 비동기 작업이 필요한 네트워크 요청, 파일 입출력, 데이터베이스 액세스 등에서 사용하며, UI 스레드를 차단하지 않고 작업을 수행할 수 있습니다.


코루틴 사용 예시

관련 함수

  • launch: launch 함수는 가장 간단한 형태의 코루틴 빌더입니다. 비동기 작업을 코루틴으로 실행할 때 사용되며, 반환값이 없습니다.
  • suspend: suspend 키워드는 함수를 코루틴으로 사용할 수 있도록 만들어줍니다. suspend 함수는 일시 중단 가능하며, 다른 코루틴에서 호출될 수 있습니다.
  • withContext: withContext 함수는 코루틴 컨텍스트를 변경하고, 지정된 블록 내에서 함수를 실행합니다. 다른 스레드에서 실행하거나 코루틴 컨텍스트를 변경할 때 사용됩니다.
  • CoroutineScope: CoroutineScope 인터페이스는 코루틴 범위를 정의하고 관리하는 데 사용됩니다. 코루틴 범위 내에서 launch나 async 함수를 호출할 수 있습니다.

백그라운 스레드에서 실행하는 예시를 보겠습니다.

우선 다음과 같이 동기식으로 LoginRepository.ktLoginViewModel.kt 가 구현되었을 때의 문제점을 확인해 보겠습니다.

LoginReository.kt

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가 동기식이며, 호출하는 스레드를 차단합니다.

LoginViewModel.kt

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 스레드에서 네트워크 요청을 하게 할 수 있습니다. 이를 위해서 아래와 같이 수정할 수 있습니다.

우선 build.gradle 파일에 다음 종속 항목을 추가합니다.

build.gradle 추가

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

LoginViewModel 수정

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

viewModelScope는 KTX 확장 프로그램에 포함된 사전 정의된 CoroutineScope 입니다.
launch는 코루틴을 만들고 함수 본문의 실행을 해당하는 디스패처에 전달하는 함수입니다.
Dispatchers.IO 는 코루틴을 I/O 작엉용으로 예약된 스레드에서 실행해야 함을 나타냅니다.

하지만 위와 같이 구현을 하면 makeLoginRequest 를 호출하는 모든 항목이 명시적으로 실행을 기본 스레드 외부로 이동해야 한다는 문제점이 있습니다. 이를 위해 Respository를 다음과 같이 수정합니다.

LoginRepository 수정

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를 업데이트 합니다.
suspend 는 코루틴 내에서 함수가 호출되도록 하는 kotlin의 방법 입니다.

이렇게 되면 makeLoginRequest 가 실행을 기본 스레드 외부로 이동하게 되므로, login 함수의 코루틴이 기본 스레드에서 실행될 수 있습니다. 아래는 이를 반영하여LoginViewModel을 수정한 코드 입니다.

LoginViewModel 수정

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

launch가 Dispatchers.IO 매개변수를 사용하지 않습니다. Dispatcher를 launch에 전달하지 않으면 viewModelScope에서 실행된 코루틴은 기본 스레드에서 실행됩니다.

login 함수의 동작은 다음과 같이 실행됩니다.

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

0개의 댓글