Android(+ Kotlin)

코루틴(Coroutine) - 기본개념

Charko 2020. 1. 16. 16:17

[2.18 수정] 코루틴은 Kotlin언어를 개발한 jetbrains에서 만들어졌다.(해당 원론 개념은을 뜻한바는 아니였습니다.)

ko.wikipedia.org/wiki/%EC%BD%94%EB%A3%A8%ED%8B%B4 

"Kotlin 1.3에서 추가되었으며 다른 언어에서 확립된 개념을 기반으로 합니다"(developer.android.com/kotlin/coroutines?hl=ko)

Kotlin언어에서 사용할 수 있는 coroutine 비동기 솔루션입니다.(Java에서는 사용할 수 없다.)

 

서브루틴(subroutine)

먼저 서브루틴 개념이 필요하다, 

하나의 함수를 예를 들어 파라미터를 받고 시작해서 끝 지점에서 종료되는 방식이 서브루틴이다.

 

코루틴(Cooutine)

서브루틴가 유사하게 단일지점에서 시작해서 끝 지점에서 종료가 된다.

하지만 중간에 함수 실행을 중단되었다 중단 지점 지점에서 재 진행한다.(임의 지점에서 멈춤, 해당 지점에서 재개)

결국 비동기형 프로그래밍으로 보아도 무방할 것 같다.

 

- 그럼 코루틴의 장점은 무엇일까?

- 비동기 코드를 작성할 때 asynctask와 같이 별도의 클래스 또는 스레드를 생성할 필요 없이 순서대로 코드를 작성할 수 있다. 

- light-weight thread이다. 기존 비동기 코드(asynctask, thread) 보다 소모비용이 작다,

- google에서 AsynckTask가 deprecate 되며 코루틴을 추천하고 있고 있다.

- 현재 코틀린 1.3 버전부터 공식적으로 추가되었으며 mvvm등에 구조에 맞는 scope 제공한다.(대세는 corutine!!)

rxjava 보다 러닝 커브는 낮다고 생각이 든다. 하지만 rx가 많은 기능을 갖고 있기 때문에 앱 기능에 대해 더 체크해보고 비동기 라이브러리를 선택해야 한다. (서버와의 api통신 정도의 어플은 coroutine정도도 충분할 것이다.)

 

https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

 

Coroutines Guide - Kotlin Programming Language

 

kotlinlang.org

공식 문서 기반으로 정리해 보았다.

 

기본 사용방법

GlobalScope.launch {
    delay(1000)
    println("World!")
}

println("Hello, ")
Thread.sleep(2000)

sleep을 한 이유는? - 코루틴은 비동기이라 실행 중 메인 프로세스가 종료되면 해당 작업이 함께 종료된다.

 

runBlocking

해당 runBloking은 기본적으로 CoroutineScope를 내장하고 있다. (GlobalScope.launch 도 해당 스코프를 내장)

runBlocking {
    delay(2000L)
}

메인 스레드 진행을 막고 해당 범위가 종료된 후 다음 프로세스를 진행한다. 위 sleep(2000)을 위 코드로 대처할 수 있다.

 

Job

코루틴의 동작을 컨트롤할 수 있다. launch를 통해 Job 타입으로 반환 값이 발생된다.

* Job에서 사용할 수 있는 메서드

  1. start -  동작 상태 반환(동작중 - true)
  2. join - 동작이 끝날 때까지 대기한다. (메모리에서 종료가 될 때까지 대기한다.)
  3. cancel - 종료 요청한다.
  4. cancelAndJoin - 종료 요청과 함께 메모리에서 종료가 될때까지 대기한다.
  5. cnacelChileren - 부모를 제외한 하뤼 루틴들을 종료한다.

 

 

Job관련 상세 설명 사이트 - https://thdev.tech/kotlin/2019/04/08/Init-Coroutines-Job/

더 자세한 내용은 위 링크를 참조하자!

val job = GlobalScope.launch {
    delay(1000L)
    println("World!")
}

println("Hello, ")
job.join()

위 sleep, runBlocking을 join으로 대처

 

리팩토링 함수 추출

launch 내부를 보다 깔끔한 코드로 리팩토링 할 때 아래와 같이 함수를 만들어서 사용이 가능하다.

suspend라는 키워드를 사용하여 언제든 지연 및 재개될 수 있는 함수로 정의할 수 있다.(IDE에서 빨간 밑줄이 표시되며 자동완성을 통해 조정된다.)

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

doWorld() 함수 내부의 delay동안 코루틴이 일시 중단된다. 그동안 다른 작업이 진행되며 다시 print문이 실행된다.

 

취소(Cancel)

fun main() = runBlocking {
    var job = launch {
        repeat(1000) {i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancel()
    job.join() // 위 처럼 종료, 대기로 작업
    job.cancelAndJoin() // 간단하게 한줄로 작업 가능하다.
    println("main: Now I can quit.")
}

진행 중 취소가 가능하며 1~1000까지 반복되나 중간에서 cancel을 통해 종료 요청이 들어가 종료가 된다.

join or cancelAndJoin 사용하여 보다 안정적으로 프로세스를 종료하자!

 

delay(1300L)
println("1300 waiting end time : ${Date().time - nowTime}")
println("main: I'm tired of waiting!")
job.cancel()
job.join() // job.cancelAndJoin()
println("call by cancel time : ${Date().time - nowTime}")
println("main: Now I can quit.")

0.004초 만에 종료가 되었다.(성능에 따라 상이)

join or cancelAndJoin 메서드를 사용하면 0.010초 만에 종료되었다. 0.006초 가량 차이가 났는데, 이 이유는 join에서 메모리 해제까지의 작업에 소요되는 시간이 포함되어 있을 것이라고 추측할 수 있다.

 

타임아웃(TimeOut)

코루틴이 실행이 중일 때 join을 통해 프로세스를 마냥 기다릴 수 없다 android에서는 ANR 가능성을 배제할 수 없다. 이럴 때를 대비한 기능인 듯하다.

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

위 코드와 같이 실행하면 TimeoutCancellationException 이 나타난다. 1.3초 동안 진행 후 타임아웃을 걸었기 때문이다.

exception을 throws 혹은 try/catch를 통해 예외처리를 진행하여도 되지만 좋은 방식이 아니다.(개인적 생각)

 

withTimeoutOrNull을 활용하면 해당 시간이 초과되었을 때 null을 return 한다. 아래 방식이 더 안정적이라 생각이 든다.

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        println("Done") // 해당 프로세스가 제시간에 완료되었을때 나타남.
    }
    println("Result is $result")
}

 

끝.