[TIL-230625] Android Coroutine 알아보기

반응형

안녕하세요
오늘은 Kotlin의 Coroutine에 대해서 간략하게 포스팅 해보겠습니다.

 


Coroutine 이란?

비동기로 실행되는 코드를 단순화하기 위해 Android에서 사용할 수 있는 동시성 디자인 패턴

  • 메인 스레드를 차단하고 앱이 응답하지 않게 만들 수 있는 작업을 관리하는데 도움이 됩니다


특징

  • Lightweight : 실행 중인 스레드를 차단하지 않는 Suspend 로 인해 단일 스레드에서 많은 Coroutine 실행 가능
  • 메모리 누수 감소 : 구조화된 동시성 (Structured concurrency)을 사용하여 범위 내에서 작업을 실행
    * 구조화된 동시성
    • 새로운 Coroutine은 Coroutine의 수명을 구분하는 특정 CoroutineScope에서만 시작이 가능합니다.
    • 손실되지 않고 누출되지 않도록 합니다.
    • 코드의 모든 오류가 올바르게 보고되고 손실되지 않도록 합니다.

 

예제 살펴보기


종속성

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

 

변경 전 : 백그라운드 스레드에서 실행


메인 스레드에서 네트워크 요청은 차단으로 인해 ANR을 발생할 수 있습니다. 그러기 때문에, 백그라운드에서 스레드를 실행합니다.

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"

    // 현재 스레드를 차단하는 네트워크 요청을 생성하는 함수
    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"))
    }
}
  • LoginRepository의 makeLoginRequest가 네트워크 동기식 호출로 호출 스레드를 차단합니다.
  • 네트워크 응답 요청을 위해 Result 클래스가 사용됩니다.

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

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}
  • LoginViewModel 내에서 사용자 버튼 클릭 후 네트워크 요청을 수행합니다.
  • 네트워크 요청 시 UI 스레드를 차단합니다.

 

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

    fun login(username: String, token: String) {
        // UI thread의 실행을 옮기기 위한 새로운 Coroutine 생성
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}
  • 메인 스레드에서 실행을 이동하는 코드는 새 Coroutine을 생성하여 I/O 스레드에서 네트워크 요청을 실행하는 것입니다.
  • viewModelScope.launch : Coroutine을 생성하고, 함수 본문의 실행을 디스패처로 전달합니다
  • Dispatchers.IO : I/O 작업을 위해 예약된 스레드에서 실행되어야 한다는 의미입니다.
  • login 함수 상세
    • View 계층으로부터 login 함수는 메인 스레드에서 실행됩니다.
    • launch는 새로운 coroutine을 생성하고, 네트워크 요청이 I/O 명령을 위한 예약된 스레드위에서 독립적으로 이루어집니다.
    • Coroutine이 실행되는 동안 login 함수는 실행을 계속하고, 네트워크 요청이 완료되기 전에 리턴합니다.
      (지금은 네트워크 응답이 무시되는 코드)

  • viewModelScope에서 시작되므로, ViewModel 안에서 실행됩니다.
  • ViewModel이 Destroy 된다면, viewModelScope는 자동으로 취소되고, 모든 coroutine도 취소됩니다.
  • 현재 코드에서는 makeLoginRequest 함수를 메인스레드 실행을 옮겨야 합니다.

 

변경 후 : Main-Safety를 위한 Coroutine 사용

 

main-safe : 메인 스레드에서 UI 업데이트를 차단하지 않게 하는 것

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

        // 코루틴 실행을 Dispatchers로 이동
        return withContext(Dispatchers.IO) {
            // 네트워크 요청 코드 차단
        }
    }
}
  • 다른 스레드로 Coroutine의 실행을 이동하려면 Coroutine 라이브러리의 withContext() 을 사용합니다.
  • withContext(Dispatchers.IO) 는  I/O 스레드로 Coroutine 실행을 이동합니다. 필요에 따라 UI 업데이트도 가능합니다.
  • makeLoginRequest의 suspend 사용은 Coroutine 내에서 호출되는 함수를 강제하기 위한 Kotlin의 특징입니다.

 

makeLoginRequest를 메인스레드 실행에서 옮기면서, login 함수의 Coroutine을 메인스레드에서 실행할 수 있습니다.

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

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

        // UI thread에서 새로운 coroutine 생성
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // 작업이 끝날때 까지, suspend 실행과 네트워크 호출을 생성합니다.
            val result = loginRepository.makeLoginRequest(jsonBody)

            // 사용자에 네트워크 요청의 결과를 출력합니다.
            when (result) {
                is Result.Success<LoginResponse> ->
                else ->
            }
        }
    }
}
  • makeLoginRequest는 suspend 함수이고, Coroutine에서 실행되어야 하므로 Coroutine은 여전히 필요합니다.
  • 이전 코드와 비교
    • launch는 Dispatchers.IO 파라미터를 사용하지 않습니다.
      (launch가 Dispatcher를 전달하지 않을 때, viewModelScope에서 모든 Coroutine은 메인 스레드에서 실행됩니다.
    • 네트워크 요청 성공, 실패 결과를 UI에서 처리하게 됩니다.
  • login 함수는
    • 메인스레드 View 계층에서 실행됩니다.
    • launch는 새로운 Coroutine을 생성하고, 실행을 시작합니다.
    • Coroutine 내에서 loginRepository.makeLoginRequest()는 makeLoginRequest()의 withContext 블록 실행이 완료 될때 까지 Coroutine의 추가 실행을 일시 중단 합니다.
    • withContext 블록이 완료되면, login에서 Coroutine가 실행을 재개합니다.



이번에는 Kotlin의 Coroutine 및 사용법에 대해 알아보았습니다

잘못된 내용이 있다면 댓글 부탁드리고, 내용이 좋았다면 공감, 구독 부탁드려요!

 

반응형