티스토리 뷰

1. 코틀린의 코루틴(Coroutine) 이란?

안드로이드 프로그래밍을 코틀린으로 할 때 코루틴 이라는 것을 사용할 수 있는데요? 코틀린의 '코'를 따서 'Ko'루틴 인줄 알았지만 사실 코루틴의 코는 co(함께, 동시에) 라는 의미를 가지고 있습니다. 즉, 동시성 프로그래밍 개념을 코틀린에 도입한 것이 코루틴 이라고 합니다.

내용추가 21.08.24) 코루틴은 코루틴이 시작된 스레드를 중단하지 않으면서 비동기적으로 실행되는 코드입니다. 기존의 복잡한 AsyncTask 또는 다수 스레드 관리를 직접 해주지 않아도되며, 기존 다중 스레드 보다 훨씬 더 효율적으로 동작합니다.

코루틴의 특징으로는 코루틴은 스레드 위에서 실행되는데 여러가지 코루틴이 존재한다고 할때 코루틴1,2,3 이 있다고 칠 때 코루틴1 을 실행하던 중 2가 실행되도 실행중인 스레드를 정지하면서 컨텍스트 스위칭 개념으로 다른 스레드로 전환하는 것이 아니라 기존 스레드를 유지하며 기존 스레드에서 코루틴2를 실행하게 됩니다.

반응형

이후 코루틴1을 다시 실행할 때 저장해둔 코루틴1 상태를 불러와 다시 스레드에서 코루틴1을 실행하게됩니다. 한마디로 스레드의 멈춤없이 루틴을 돌릴 수 있게되며 이는 성능 향상을 기대할 수 있고 여러 스레드를 사용하는 것 보다 훨씬 적은 자원 소모를 하게됩니다.

내용추가 21.08.24) 왜냐하면 스레드 관련 이벤트 또는 결과 처리를 위한 콜백 작성이 필요없고 순차적으로 코드를 작성하면 되기 때문이다. (사실 코루틴도 내부적으로는 여전히 다중 스레드를 사용한다.)

이러한 코루틴은 코틀린에 최근 추가된 것이지만 1960년 이래로 여러 형태로 다른 언어들에 이미 코루틴이 존재했고, CSP(Communicationg Sequential Process)라는 모델에 기반한다.

https://pluu.github.io/assets/img/blog/2019/1117-droidkaigi-understanding-kotlin-coroutines/001.png

1.1. 안드로이드 스튜디오 코루틴 설정하기

안드로이드 스튜디오 최신 버전(현재 2021.06)에는 코루틴이 기본적으로 내장되어 있지만 구버전을 사용하는 경우에 기본적으로 추가가 되어있지 않을 수 있습니다. 이때는 build.gradle 파일의 dependencies에 의존성을 추가하셔서 사용해야합니다.

위와 같이 CoroutineScop를 타이핑 했을 때 자동완성이 뜨게된다면 따로 의존성을 추가하지 않으셔도됩니다.

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

1.2. 코루틴 스코프

코루틴은 코루틴 스코프 안에서 실행되며 예제는 아래와 같습니다.

내용추가 21.08.24) 모든 코루틴은 스코프 내에서 실행되어야 하는데 이를 통해서 액티비티 또는 프래그먼트의 생명주기에 따라 소멸 될 때 관련 코루틴을 한번에 취소할 수 있는데 이는 곧 메모리 누수를 방지한다. 스코프는 커스텀 또는 이미 내장된 범위를 사용할 수 있다.

(CoroutineScope 는 사실 CoroutineContext 타입 필드를 launch 등의 확장 함수 내부에서 사용하기 위한 매개체 역할만 담당한다.)

(CoroutineContext 는 실제로 코루틴이 실행중인 여러 작업(Job) 과 디스패처를 저장하는 일종의 맵이라고 보면된다. 이를 통해 코틀린 런타임은 다음에 실행할 작업을 고르고 어떤 스레드에 배정할지 결정한다.)

모든 코루틴은 항상 자신이 속한 스코프를 참조해야한다. 이후에 cancel 로 모두 취소 가능하다.

        GlobalScope.launch {
            // DO IT
        }

사실 코루틴 스코프의 경우에는 글로벌 스코프(GlobalScope)와 코루틴 스코프(CoroutineScope)가 존재합니다.

  • 글로벌 스코프: 앱의 생명주기와 함께 동작하기 때매 실행 도중에 별도 생명 주기 관리가 필요없음. 시작~종료 까지 긴기간 실행되는 코루틴의 경우에 적합합니다.
  • 코루틴 스코프: 버튼을 눌러 다운로드 하거나 서버에서 이미지를 열 때 등. 필요할 때만 열고 완료되면 닫아주는 코루틴스코프를 사용할 수 있습니다.
  • ViewModelScope : Jetpack 아키텍처의 뷰모델 컴포넌트 사용시 ViewModel 인스턴스에서 사용하기 위해 제공되는 스코프이다. 해당 스코프로 실행되는 코루틴은 뷰모델 인스턴스가 소멸될 때 자동으로 취소된다. (내용추가 21.08.24)
        binding.downloadButton.setOnClickListener { 
            CoroutineScope(Dispatchers.IO).launch { 
                // DO IT
            }
        }

코루틴 스코프의 경우 글로벌 스코프와 달리 디스패쳐를 지정할 수 있는데 이는 코루틴이 실행될 스레드를 지정하는 것 입니다.

2. 코틀린 코루틴의 디스패쳐

코루틴 디스패쳐의 경우에는 Default, IO, Main, Unconfined 등 이 있습니다.

  • Dispatchers.Default : 안드로이드 기본 스레드풀 사용. CPU를 많이 쓰는 작업에 최적화. (데이터 정렬, 복잡한 연산 등)
  • Dispatchers.IO : 이미지 다운로드, 파일 입출력 등 입출력에 최적화 되어있는 디스패쳐 (네트워크, 디스크, DB 작업에 적합)
  • Dispatchers.Main : 안드로이드 기본 스레드 에서 코루틴 실행. UI 와 상호작용에 최적화.
  • Dispatchers.Unconfined : 호출한 컨텍스트를 기본으로 사용하는데 중단 후 다시 실행될 때 컨텍스트가 바뀌면 바뀐 컨텍스트를 따라가는 특이한 디스패쳐

디스패처는 코루틴을 적당한 스레드에 할당하며, 코루틴 실행 도중 일시 정지 or 실행 재개를 담당한다. (다음에 어떤 코루틴을 실행 시킬지 결정) 커스텀 스레드풀을 위한 디스패처도 생성할 수도 있다.

2.1. 코루틴의 상태관리

코루틴은 launchasync로 시작이 가능합니다. launch는 상태를 관리 할 수 있고 async는 상태를 관리 + 결과 까지 반환 받을 수 있습니다. 코루틴을 생성하고 상태 관리 매서드를 호출해서 중단, 지연 할 수 있습니다.

cancel

코루틴의 동작을 멈추는 상태관리 매서드로 하나의 스코프 안에 여러 코루틴의 존재하는 경우 하위 코루틴 또한 모두 멈춥니다. 아래 코드에서 job 을 캔슬하게되면 안에 있던 job1 도 중단됩니다.

delay() 는 yield() 와 마찬가지로 다른 코루틴에 실행을 양보하게된다. (정해준 시간이 끝날때 까지 무한 양보한다. 즉 양보했던 코루틴이 다시 양보했더라도 delay 시간이 끝나지 않았다면 다시 양보했던 코루틴에게 제어권이 넘겨진다.)

        val job = CoroutineScope(Dispatchers.Default).launch { 
            val job1 = launch { 
                for (i in 0..10){
                    delay(500)
                    Log.d("코루틴", "$i")
                }
            }
        }
        
        binding.downloadButton.setOnclickListener{
            job.cancel()
        }

join

사실 코루틴 내부에 여러 launch 블록이 있는 경우 모두 새로운 코루틴으로 분기되어 동시 실행 되기 때문에 순서를 정할 수 없습니다. 순서를 정해야한다면 join() 을 사용해서 순차적으로 실행되도록 코드를 짤 수 있습니다.

        CoroutineScope(Dispatchers.Default).launch { 
            launch { 
                for (i in 0..5){
                    delay(500)
                    Log.d("코루틴", "$i")
                }
            }.join()
            
            launch {
                for (i in 6..10){
                    delay(500)
                    Log.d("코루틴", "$i")
                }
            }
        }

3. async 로 결과값 처리

async로 코루틴 스코프의 결과를 받아서 쓸 수 있습니다. 특히 연산시간이 오래걸리는 2개의 네트워크 작업의 경우를 예를들면 2개의 작업이 모두 완료되고 나서 이를 처리하려면 await() 을 사용할 수 있습니다. 이때는 async 작업이 모두 완료되고 나서야 await() 호출줄 코드가 실행됩니다.

        CoroutineScope(Dispatchers.Default).async {
            val deferred1 = async { 
                delay(500)
                350
            }
            val deferred2 = async { 
                delay(1000)
                200
            }
            Log.d("coroutine", "${deferred1.await() + deferred2.await()}")
        }

3.1. suspend 를 보고 놀라다

코루틴의 가장 큰 특징이라고 할 수 있는 suspend 키워드는 코루틴 안에서 사용되면 suspend 함수가 호출될 경우 이전까지의 코드의 실행이 멈추며 suspend 함수가 처리가 완료된 후 멈춰 있던 원래 스코프의 다음 코드가 실행됩니다. (코드를 보는게 이해가 빠릅니다.)

        suspend fun subRoutine(){
            for(i in 0..10){
                Log.d("subRoutine", "$i")
            }
        }

        CoroutineScope(Dispatchers.Main).launch {
            // 선 처리 코드
            subRoutine()
            // 후 처리 코드
        }

위에서 보시면 알겠지만 suspend 함수를 코루틴 내에서 사용할 때 호출 이전까지 코드가 실행되고 호출이 된 순간은 해당 suspend 함수가 처리가 모두 완료되어야 후처리 코드가 실행되게 되는 것이죠. 이때 suspend 키워드를 사용했기 때문에 코루틴스코프 안에서 자동으로 백그라운드 스레드 처럼 동작하게 됩니다.

가장 큰 특징이라고 한 이유는 바로 이 suspend 키워드를 붙인 함수가 실행되면서 호출한 쪽의 코드를 잠시 멈추게 되지만 스레드의 중단이 없기 때문입니다. (이번에 안드로이드 앱 개발을 하면서 이런게 정말 필요하다고 느꼈는데 답은 코루틴에 있었네요..🤭)

코루틴이 실행되다가 일시 정지하는 경우 (일정 시간 대기 등) 코틀린 런타임은 해당 코투틴이 실행되던 스레드에 다른 코루틴을 할당하여 실행되게 한다. 그리고 다시 이전 코루틴이 재개할 때 사용 가능한 스레드를 코틀린 런타임이 할당해준다. 이런 것은 다 효율적인 스레드 활용을 위한 것인데, 이런 매커니즘에 맞게 실행되게 하는 함수가 바로 suspend 함수다.

만약 스레드에서 해당 코드를 사용했다면 선처리 코드가 동작하는 스레드를 멈춰야지 서브루틴 호출이 가능한데 코루틴은 해당 부모 루틴 상태를 저장. 서브루틴 실행. 부모루틴 복원 하는 식으로 동작하여 스레드 영향을 주지 않게됩니다.

4. withContext 디스패쳐 분리 사용

suspend 함수를 코루틴 스코프에서 사용할 때 호출한 스코프와 다른 디스패쳐를 사용할 때가 있는데, 호출쪽 코루틴은 Main 디스페쳐로 UI를 제어, suspend 함수는 파일io 를 하는 경우 withContext 를 사용하여 suspend 함수의 디스패쳐를 IO 로 변경사용할 수 있습니다.

기본적으로 부모의 코루틴 디스패쳐를 사용하지만 withContext 로 디스패쳐를 달리 사용할 수 있게되는 것이죠.

        CoroutineScope(Dispatchers.Main).launch { 
            // ui 처리 코드
            // ...
            val result = withContext(Dispatchers.IO){
                readFile()
            }
            Log.d("코루틴", "$result")
        }

다음에는 코루틴을 이용하여 특정 서비스 api 로 이미지 url 리스트를 받아와서 비트맵으로 변환한뒤 이미지를 이미지뷰에 나타내주는 예제 글로 찾아 뵙도록 하겠습니다.

(내용추가 21.08.24)

5. 코루틴 빌더

코틀린은 코루틴 빌더에 원하는 동작을 람다로 넘겨 코루틴을 생성하여 실행하는 방식을 사용한다. (21.08.26 추가) 

코루틴에서 제공하는 빌더들

  • launch - 현재 스레드 중단 없이 코루틴을 즉시 시작 시킨다. 결과를 호출한 쪽에 반환하지 않는다. suspend 함수가 아닌 일반 함수 안에서 suspend 함수를 호출할 때 와 코루틴의 결과 처리가 필요없을 때 사용하자. (실행 후 망각 코루틴)
  • async - 현재 스레드 중단 없이 코루틴을 즉시 시작 시킨다. 호출 쪽에서 await() 을 통해 코루틴 결과를 기다릴 수 있다. 병행으로 실행될 필요가 있는 다수의 코루틴을 사용할 때 쓰자. async 빌더는 suspend 함수 내부에서만 사용가능하다.
  • withContext - 부모 코루틴에 의해 사용되던 컨텍스트와 다른 컨텍스트에서 코루틴을 실행시킬 수 있다. 코루틴에서 결과를 반환할 때 async 대신 유용하게 쓸 수 있다.
반응형
  • coroutineScope - 병행으로 실행될 다수의 코루틴을 suspend 함수가 시작시키고 모든 코루틴이 완료될 때만 어떤 처리가 필요할 때 이상적이다. 만약 이런 코루틴이 coroutineScope 빌더로 실행되면 호출 함수는 모든 자식 코루틴이 완료되어야 실행이 끝나고 복귀된다. 여러 코루틴중 하나라도 실행에 실패하면 모든 다른 코루틴이 취소된다.
  • supervisorScope - croutineScope 빌더와 비슷하다. 그러나 한 코루틴이 실패해도 다른 코루틴이 취소되지 않는다는 차이점이 있다.
  • runBlocking - 코루틴을 시작시키고 완료될 때 까지 현재 스레드를 중단시킨다. 코루틴의 취지와 정반대다. 그러나 코드 테스트, 레거시 코드 및 라이브러리 통합시 유용하다. 그외 경우엔 되도록 사용하지 말자. (CoroutineScope 의 확장 함수가 아니여서 코루틴 스코프 없이도 실행 가능하다.)

5.1. 그외 코루틴 빌더 : produce, actor

  • produce : 정해진 채널로 데이터를 스트림으로 보내는 코루틴을 빌드한다. ReceiveChannel<> 을 반환하며 해당 채널로 부터 메세지를 전달 받아 사용가능하다.
  • actor : 정해진 채널로 메세지를 받아 처리하는 액터를 코루틴으로 빌드한다. SendChannel<> 을 반환하며 해당 채널의 send() 매서드를 통해서 액터에세 메세지를 보낼 수 있다.

5.2. 코루틴 모듈 최상위에 정의된 suspend function

delay() 및 yield() 같은 미리 정의된 suspend 함수를 살펴보자.

  • withContext : 다른 컨텐스트로 코루틴을 전환한다.
  • withTimeout : 코루틴이 정해진 시간 안에 실행되지 않으면 예외를 발생시킨다.
  • withTimeoutOrNull : 정해진 시간 안에 실행되지 않는다면, null을 반환한다.
  • awaitAll : 모든 작업의 성공을 기다린다. 작업 중 하나라도 예외로 실패하면 awaitAll() 또한 해당 예외로 실패한다.
  • joinAll : 모든 작업이 끝날 때 까지 현재 작업을 일시 중단시킨다.

6. Job

launch, async 등 빌더를 호출하면 Job 인스턴스가 반환된다. (launch 는 Job 을, async 는 Deffered 를 반환한다. Deffered 는 어차피 Job을 상속한 녀석이다.)코루틴 생명주기를 Job으로 관리할 수 있고. 내부에서 다시 빌더를 호출하면 자식 Job 으로 생성된다. 부모 Job 취소시 자식 Job도 취소되며 그 반대는 취소되지 않는다.

 

그러나 launch 빌더로 생성한 자식 Job은 예외 발생시 부모 Job 을 취소시킨다. async 빌더로 생성한 자식 Job은 예외가 발생해도 부모 Job이 취소되지 않는데 이는 반환 결과에 예외도 포함시켜버리기 때문이다.

사용할 수 있는 속성 : isActive, isCompleted, isCancelled

사용할 수 있는 함수 : cancel(), cancelChildren(), join(), cancelAndJoin()

join() 은 모든 자식 job 이 완료될 때까지 특정 Job과 연관된 코루틴을 정지한다.

cancelAndJoin() 은 특정 Job을 취소한다.

이러한 구조는 Structured concurrency 의 기반을 마련한다. 각 코루틴의 참조를 직접 관리할 필요없이 코루틴이 필요 이상으로 더오래 실행되지 않게 할 수 있다는 뜻이다.

6.1. 코루틴의 정지와 재개

위 예제에서 메인 엑티비티의 버튼을 누르면 startTask 가 호출 될 때 UI가 정지할 것 같지만 정지하지 않습니다! 왜냐하면 delay 또한 suspend 함수이기 때문에 호출시 코틀린 런타임에 코루틴으로 시작됩니다. performSlowTask 가 suspend 되고 제어권은 다시 Main 스레드로 넘어가는 갑니다! 그래서 UI의 멈춤이 없고 delay 7.777초가 모두 지나고 나서 resume 되어 다시 코루틴이 제어권을 가져가 (사용가능한 스레드풀 스레드에 복원) after 로그를 출력합니다.

코루틴 정지점 표시

6.2. 코루틴에서 결과 받기

performSlowTaskAsync 함수가 Deferred 객체를 반환하도록 한다. Deferred 객체는 향후 언젠가 값을 제공한다는 의미이다. 해당 객체에 await() 을 호출하면 값이 반환될 때 코틀린 런타임에 전달해준다.

코루틴에서 결과를 받아오려면 async 빌더로 코루틴을 실행시켜야하는데 startTask 는 suspend 함수가 아닌 클릭 리스너이기 때문에 performSlowTaskAsync 함수에서 async 빌더를 사용해서 또 다른 코루틴(Deferred 를 반환하는 코루틴)을 시작하게 해야한다.

그러면 결과를 await 할 때 메인 스레드 중지가 아닌 백그라운드에서 작업을 수행할 수 있게된다.

또는

반응형

withContext 로 async, Deferred, await() 호출을 대신할 수도 있다.

6.3. 코루틴 간에 채널 통신

Channel 로 데이터 스트림을 비롯한 코틀린 간의 통신을 간단하게 구현할 수 있다.

send() 로 데이터를 전송하고 receive() 로 데이터를 수신한다.

Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
Received: 6

6.4. 기타 예제

class MapActivity : AppCompatActivity(), ..., CoroutineScope { // 👈

    private lateinit var job: Job // 👈

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job // 👈
        
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        job = Job() // 👈

        //...
    }
    
    private fun loadReverseGeoInformation(locationLatLngEntity: LocationLatLngEntity) {
        // 코루틴 사용
        launch(coroutineContext) { // 👈
            try {
                //...
                withContext(Dispatchers.IO) { // 👈
                    //...
                    if (response.isSuccessful) {
                        //...
                        withContext(Dispatchers.Main) { // 👈
                            //...
                        }
                    }
                }
            } catch (e: Exception) {
                //...
            } finally {
                //...
            }
        }
    }

7. 결론

다중 스레드 방식 보다 효율적인 코루틴을 사용하자. 비동기 작업을 구조화된 방법으로 구현할 수 있고 더 간단하게 구현할 수 있다.

Reference.
이것이 안드로이드다 with 코틀린 (고돈호 저)
안드로이드 스튜디오 Arctiv Fox & 프로그래밍 (닐 스미스 지음, 심재철 옮김)
Kotlin in action (드미트리 제메로프/스베트라나 이사코바 지음, 오현석 옮김)

2021.06.21 - [Android] - [Android] webView 에 html String Data 넣어서 불러오기

2021.06.18 - [Android] - [Android] 코드에서 drawable 자원으로 컴포넌트 속성 설정하기

2021.06.18 - [Android] - [Android] Intent 에 데이터(값) 넣어서 엑티비티 전환하기

댓글
댓글쓰기 폼
네이버 이웃추가
«   2021/11   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        
글 보관함