ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 안드로이드 코루틴 (Coroutine)
    안드로이드 학습/Android 기술면접 대비 2024. 11. 8. 18:27

    안드로이드에서 비동기 작업을 하면 주로 다양한 라이브러리와 방법이 있겠지만 주로 3가지 방식을 사용하는 것 같다.

    • Thread 생성 
    • RxJava
    • Coroutine (코루틴)

    나는 앞서 RxJava나 Thread는 학습했고 이번에는 코루틴 차례이다.

     

    내가 학습했던 많은 안드로이드 예제 프로젝트에는 네트워크 통신 같은 것들은 거의 코루틴을 사용했었고 코루틴만의 장점이 다른 것보다 크기 때문에 그런것이라고 생각한다.

     

    그래서 코루틴에 대해서 알아보고 다른 RxJava나 Thread와 비교해 보려고 한다. 

    1. 코루틴이란?

    코루틴은 일종의 가벼운 스레드(Light-weight thread)로 Kotlin 언어에서 비동기 작업 지원하는 라이브러리다.

     

    코루틴은 코틀린 언어에서 제공되기 때문에 안드로이드를 Kotlin으로 프로젝트를 생성하던가 아니면 코틀린 라이브러리를 추가되어 있다면 별다른 라이브러리 추가 없이 사용이 가능하다. 

     

    여러 블로그에 위키피디아에 적혀있는 코루틴 정의가 포함되어 있어서 학습해봤다. 

     

    Coroutines are computer program components that allow execution to be suspended and resumed, generalizing subroutines for cooperative multitasking.

    >>> 코루틴은 실행을 일시중단 하고 재개할 수 있도록 하여 비선점형 멀티태스킹을 위한 서브루틴을 일반화하는 컴퓨터 프로그램 구성 요소이다.

     

    비선점형 멀티태스킹을 위한 서브 루틴 설명 : 

    더보기

    1) 비선점형 멀티태스킹이란 ? 

    • 선점형 : task가 cpu를 사용하고 있더라도 뺏어서 중단시킬 수 있다.
    • 비선점형 : task가 스케줄러로부터 cpu 사용권을 할당받았을 때 스케줄러가 강제로 cpu사용권을 뺏을 수 없다. 

    내가 정리한 내용으로는

    선점형은 task가 다른 코드 실행을 중지시키고 자신껄 먼저 할수 있는거고

    비선점형은 task가 다른 코드 실행을 강제로 중지시키지 못한다는 것 같다. 

     

    2) 서브루틴이란 ?

    • 메인 루틴 : 프로그램 전체의 개괄적인 동작 절차를 표시하도록 만들어진 루틴.
    • 서브 루틴 : 반복적인 특정 기능을 모아서 별도로 묶인 루틴.

    안드로이드에서는 MainActivity에서 onCreate 내부에 있는 코드가 메인 루틴일 것이고

    따로 function을 만들어서 불러온다면 그것이 서브루틴일 것이다. 

     

    메인루틴 & 서브루틴 예제 코드

    fun main() { // 메인루틴
        println("메인루틴 시작")
        val result = subroutineExample() // 서브루틴 호출
        println("Result from subroutine: $result")
        println("메인루틴 종료")
    }
    
    fun subroutineExample(): Int { // 서브루틴
        return 42
    }

     

    결과 :

    메인루틴 시작
    Result from subroutine: 42
    메인루틴 종료

     

    3) 일시 중단과 재개

    fun main() = runBlocking {
        val job = launch {
            println("작업 시작")
            delay(1000L)  // 1초 동안 일시 중단
            println("작업 완료")  // 일시 중단된 후 다시 재개
        }
    
        println("작업 중...")
        job.join()  // 코루틴이 끝날 때까지 대기
        println("모든 작업 완료")
    }

     

    코루틴에서는 delay(중단)나 join(재개)같은 것들을 사용해서 위에 예시처럼 launch{} 같은 서브루틴 안에 있는 코드가 return을 만나지 않더라도 잠시 멈추고 재개시킬 수 있다. 중단 된 동안은 같은 Thread내에서 다른 코드를 실행시킬 수 있어서 매우 효율적이다. 

    코루틴을 좀더 자세하게 이해하기 위해서는 동시성과 병렬성을 이해할 필요가 있다. 

     

    동시성과 병렬성을 이해하기 위해서는 Thread와 코루틴의 차이를 같이 보면서 이해한다면 좀더 쉽게 이해할 수 있을 것같다. Thread와 코루틴의 차이는 무엇일까?

    2. Thread vs Coroutine (동시성 & 병렬성)

     

    위에 이미지는 2개의 스레드는 2개의 작업을 동시에 하고 있고

    아래 이미지는 1개의 스레드에서 2개의 작업을 번갈아 가면서 하고 있는 것처럼 보인다. 

     

    2개의 스레드에서 2개의 작업을 하는 것을 병렬성(Thread)이라고 하고

    아래 이미지는 1개의 스레드에서 2개의 작업을 하는 것을 동시성(Coroutine)이라고 한다.

     

    참고 블로그 : 링크

     

    동시성과 병렬성

    • 동시성(Concurrency) : 논리적으로 병렬로 작업이 실행되는 것처럼 보이는 것 (코루틴)
    • 병렬성(Parallelism) : 물리적으로 병렬로 작업이 실행되는 것 (멀티스레드)
    더보기

    동시성 (Concurrency) :

    • 동시성 프로그래밍은 눈으로 보기에 동시에 실행되는 것으로 보이지만, 사실 시분할(Interleaving) 기법을 활용하여 여러 작업을 조금씩 나누어서 번갈아가며 실행하는 것이다.

     

    더 설명할 필요도 없이 위에 Task2개는 20분이 걸릴 것이다.

     

    병렬성 (Parallelism) :

    • 병렬성 프로그래밍은 여러 작업을 한 번에 동시에 수행하는 것이다. 자원(CPU 코어)이 여러 개 일 때 가능하다

    2개의 일을 각자 수행하니 총 시간이 10분 걸린다.

    Thread

    • 작업의 단위 : Thread
    • Thread 가 독립적인 Stack 메모리 영역 가진다.
    • 동시성 보장 수단 : Context Switching
      • 운영체제 커널에 의한 Context Switching 을 통해 동시성 보장
      • 블로킹 (Blocking) : Thread A 가 Thread B 의 결과가 나오기까지 기다려야 한다면, Thread A 은 블로킹되어 Thread B 의 결과가 나올 때 까지 Thread A의 자원을 사용하지 못한다.

    Coroutine

    • 작업의 단위 : Coroutine Object
      • 여러 작업 각각에 Object 를 할당함
      • Coroutine Object 도 엄연한 객체이기 때문에 JVM Heap 에 적재 된다 (코틀린 기준)
    • 동시성 보장 수단 : Programmer Switching (No-Context Switching)
      • 프로그래머의 코드를 통해 Switching 시점을 마음대로 정함 (OS 관여 X)
      • Suspend (Non-Blocking) : Object 1 이 Object 2 의 결과가 나오기까지 기다려야 한다면, Object 1 은 Suspend 되지만, Object 1 을 수행하던 Thread 는 그대로 유효하기 때문에 Object 2 도 Object 1 과 동일한 Thread 에서 실행될 수 있음

     

    • 멀티스레드 방식과는 다르게 다른 스레드의 결과를 기다리는 동안 자원을 활용 못하는게 아니라 결과가 필요한 부분만 잠시 멈추고 다른 task를 할 수 있다. 
    • 한 쓰레드에서 다수의 Coroutine 을 수행할 수 있다. 
    • Context Switching 이 필요없는 특성에 따라 Coroutine 을 Light-weight Thread 라고 부르는 것이다.

     

    RxJava는 Thread를 조금 더 편하게 사용하기 위한 대안으로 나온 것이라면, Coroutine은 그것과는 다르게 Thread를 좀 더 효율적으로 사용하기 위해서 나온 개념이라고 볼 수 있다. 

     

    3. 코루틴 활용

    3-1) 코루틴 빌더

     

    코루틴을 만드는 함수를 코루틴 빌더라고 한다. 코루틴 빌더에는 아래 4가지가 대표적이다. blocking은 thread를 정지시키고 block내의 코드를 실행시키고, non-blocking은 thread를 정지시키지 않는다. 

    • launch (non-blocking)
    • async (non-blocking)
    • runBlocking (blocking)
    • withContext (non-blocking)

    a) launch :

    • launch  결과 반환이 없다. 다른 async나 runBlocking은 있다. 
    • 자체/자식 코루틴 실행을 취소할 수 있는 Job 반환
    • 메소드들 : start(), join(), cancel(), cancelAndJoin(), cancelChildren()
    private fun buildByUsingLaunch() {
        Log.d("MainActivity-a", "buildByUsingLaunch - buildByUsingLaunch 시작")
        coroutineScope = CoroutineScope(Dispatchers.Main)
        val job = coroutineScope.launch {
            Log.d("MainActivity-a", "buildByUsingLaunch - coroutineScope 영역 시작")
            delay(3000)
            Log.d("MainActivity-a", "buildByUsingLaunch - coroutineScope 영역 끝")
        }
        Log.d("MainActivity-a", "buildByUsingLaunch - buildByUsingLaunch 끝")
    
        // 1 - buildByUsingLaunch - buildByUsingLaunch 시작
        // 2 - buildByUsingLaunch - buildByUsingLaunch 끝
        // 3 - buildByUsingLaunch - coroutineScope 영역 시작
        // 4 - buildByUsingLaunch - coroutineScope 영역 끝
    
        // 상태 확인 방법 :
        job.isActive
        job.isCancelled
        job.isCompleted
    }

     

    • 새로운 코루틴을 실행하고, 비동기(non-blocking) 작업을 수행한다. 
    • 반환값이 없고, Job을 반환하여 이를 통해 코루틴의 상태를 추적하거나 취소할 수 있습니다. 
    • 보통 CoroutineScope 내에서 사용됩니다.

    start 메소드 

    • Job의 start 메소드는 코루틴의 실행 완료 여부와는 상관없이 나머지 코드들이 계속 진행된다.
    더보기
    private fun jobStart() {
        Log.d("MyTag", "jobStart - start")
    
        val activeJob: Job = coroutineScope.launch(start = CoroutineStart.LAZY) {
            Log.d("MyTag", "jobStart inside - start")
            delay(2000)
            Log.d("MyTag", "jobStart inside - end")
        }
    
        //  1-1. Job
        Log.d("MyTag", "$activeJob")
        activeJob.start()    // 1초 대기
        Log.d("MyTag", "$activeJob")
        Log.d("MyTag", "jobStart - end")
    
        // jobStart - start
        // StandaloneCoroutine{Active}@fd97f26
        // StandaloneCoroutine{Active}@fd97f26
        // jobStart - end
        // jobStart inside - start
        // jobStart inside - end
    }
    

    join 메소드

    더보기
    private suspend fun jobJoin() {
        Log.d("MyTag", "jobJoin - start")
        val activeJob: Job = coroutineScope.launch {
            Log.d("MyTag", "jobJoin - start")
            delay(3000)
            Log.d("MyTag", "jobJoin - end")
        }
        Log.d("MyTag", "$activeJob")
        activeJob.join()    // 1초 대기
        Log.d("MyTag", "$activeJob")
        delay(3000)
        Log.d("MyTag", "jobJoin - end")
    
        // jobJoin - start
        // StandaloneCoroutine{Active}@cc6cfe5
        // jobJoin - start
        // jobJoin - end
        // StandaloneCoroutine{Completed}@cc6cfe5
        // jobJoin - end
    }

    start, join 차이점

    b)   async :

    CoroutineScope(Dispatchers.Main).launch {
        asyncFunction()
    }
    private suspend fun asyncFunction() {
        Log.d("asyncFunction", "1. start asyncFunction")
    
        val deferred:Deferred<String> = CoroutineScope(Dispatchers.Default).async {
            Log.d("asyncFunction", "2. start deferred")
            sleep(2000)
            Log.d("asyncFunction", "3. end deferred")
            "abcd"
        }
    
        Log.d("asyncFunction", "4")
        val num = deferred.await()
        Log.d("asyncFunction", "5. num : $num")
        Log.d("asyncFunction", "6. end asyncFunction")
    
        // 1. start asyncFunction
        // 4
        // 2. start deferred
        // 3. end deferred
        // 5. num : 31
        // 6. end asyncFunction
    }

     

    • async는 launch와 다르게 코루틴 내부에서의 결과 값을 받을 수 있다.
    • async와 await을 같이 사용한다면 코루틴에서 마지막 줄의 결과를 받고 나머지 코드를 진행시킬 수 있다. 
    • 나머지 코드 의미는 await() 이후 부터의 코드를 의미한다 

    c)  runBlocking:

    private fun runBlockingFunction() {
        Log.d("runBlockingFunction", "1. start runBlockingFunction")
    
        val result = runBlocking {
            Log.d("runBlockingFunction", "2. start deferred")
            Thread.sleep(2000)
            Log.d("runBlockingFunction", "3. end deferred")
        }
    
        Log.d("runBlockingFunction", "4")
        Log.d("runBlockingFunction", "5. result : $result")
        Log.d("runBlockingFunction", "6. end runBlockingFunction")
    
        // 1. start runBlockingFunction
        // 2. start deferred
        // 3. end deferred
        // 4
        // 5. result : 37
        // 6. end runBlockingFunction
    }

     

    • launch와 async와는 다르게 runBlocking이 끝나야 진행된다.
    • launch, async는 join, await를 사용해야 코루틴 영역 코드를 완료하고 나머지 코드를 진행, runBlocking은 바로 코루틴이 종료될때까지 현재 스레드를 blocking하고 영역 내의 코드를 실행시킨다. 
    • 주로 테스트나 main 함수 내에서 코루틴을 사용하기 위해 코루틴 스코프를 만들 때 사용합니다.

    d) withContext

    CoroutineScope(Dispatchers.Default).launch {
        val result1 = withContext(Dispatchers.IO) {
            // 첫 번째 작업: 네트워크 호출 등
            Log.d("WithContextFragment", "withContextExample - result1 inside")
            delay(500) // 1초 대기
            "Result 1"
        }
        Log.d("WithContextFragment", "withContextExample - result1 $result1")
    
        val result2 = withContext(Dispatchers.IO) {
            // 두 번째 작업: 다른 네트워크 호출 등
            Log.d("WithContextFragment", "withContextExample - result2 inside")
    
            delay(2000) // 1초 대기
            "Result 2"
        }
        Log.d("WithContextFragment", "withContextExample - result2 $result2")
        Log.d("WithContextFragment", "--- End ---")
    }
    
    D/WithContextFragment: withContextExample - result1 inside
    D/WithContextFragment: withContextExample - result1 Result 1
    D/WithContextFragment: withContextExample - result2 inside
    D/WithContextFragment: withContextExample - result2 Result 2
    D/WithContextFragment: --- End ---
    • 현재 코루틴이 실행 중인 컨텍스트에서 다른 컨텍스트로 잠깐 전환해 중괄호 블록 안의 코드를 실행할 수 있다.
    • 시간이 오래 걸리는 작업을 메인 쓰레드에서 분리해 UI를 차단하지 않게 하거나, 다른 쓰레드 or 디스패처에서 실행해야 하는 코드가 있을 경우 사용한다.

    3-2) 코루틴 스코프 (Coroutine Scope)

    • 코루틴의 제어 범위, 실행 범위를 지정할 수 있다. 
    • 동일한 Scope안의 코루틴을 취소하는 경우, 모든 자식 코루틴들도 취소된다. 하지만, 다른 스코프를 지정하여 사용하는 코루틴은 취소되지 않는다. 

    1. CoroutineScope

    • 특정한 Scope 및 Dispatcher를 지정할 수 있는 기본 코루틴 Scope
    • Dispatcher 종류 :  
      1. Main : 메인 스레드, 화면 ui 작업 등을 하는 곳
      2. IO : 네트워크, DB 등 백그라운드에서 필요한 작업을 하는 곳
      3. Default : 정렬이나 무거운 계산 작업 등을 하는 곳
    • 안드로이드 생명주기에 맞게 설계된 viewmodelScope, LifecycleScope등이 제공된다.

    CoroutineScope

    val scopeMain = CoroutineScope(Dispatchers.Main)
    scopeMain.launch {
        // 메인 쓰레드 작업
    }
    
    CoroutineScope(Dispatchers.IO).launch {
        // 백그라운드 작업
    }
    

     

    viewModelScope는 ViewModel안에서 사용된다. 

    class CoroutineExampleViewModel : ViewModel() {
    
        private val _userDataLiveData: MutableLiveData<UserData> = MutableLiveData()
        val userDataLiveData: LiveData<UserData> = _userDataLiveData
    
        fun fetchData() {
            viewModelScope.launch {
                val result = fetchUserData()
                _userDataLiveData.postValue(result)
            }
        }
    
        private suspend fun fetchUserData(): UserData {
            delay(1000) // 예시로 1초 지연
            return UserData("John Doe", 30) // 예시 데이터
        }
    }
    
    data class UserData(val name: String, val age: Int)
    • ViewModel이 제거되면 코루틴 작업이 자동으로 취소됩니다.
    • 추가로 build.gradle에 아래 내용이 추가되어 있어야 한다. 
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    

     

     

    lifecycleScope

    lifecycleScope.launch(Dispatchers.IO) {
        delay(1000)
    }

     

    lifecycleScope를 사용하면 수명 주기와 연관된 안드로이드 구성 요소(예: 액티비티, 프래그먼트)의 수명 주기를 기반으로 코루틴을 실행 가능

     

    이는 구성 요소의 수명 주기가 활성 상태인 동안에만 코루틴이 실행되고, 구성 요소가 파괴되거나 비활성 상태가 되면 자동으로 해당 코루틴이 취소됩니다.

     

    2. GlobalScope

    • GlobalScope는 CoroutineScope의 한 종류로써 가장 큰 특징은 Application이 시작하고 종료될 때까지 계속 유지
    • Singletone 이기 때문에 따로 생성하지 않아도 되며 어디에서든 바로 접근이 가능하여 간단하게 사용하기 쉽다
    • 그러나 메모리 누수의 원인이 될 수 있기 때문에 신중히 사용해야 한다. 즉 다시 말해 앱이 실행된 이 후 계속 수행이 되어야 한다면 GlobalScope 를 사용해야 하는 것이고 특정 Activity나 Service 에서만 잠깐 사용하는 것이라면 GlobalScope를 사용하면 안된다.
    GlobalScope.launch {
       //coroutine
    }

     

Designed by Tistory.