ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin - Coroutine
    개발/안드로이드 2020. 4. 15. 22:47

    Kotlin의 Coroutine은 비동기 작업을 지원하는 "lightweight threads" 인 컴포넌트다. 이미 안드로이드에 있는 AsyncTask와 비슷한 역할을 수행하지만 Coroutine은 특별한 오버라이드 함 수 없이 간단하게 구현이 가능하고 깊게 들어가면 세부 동작 방식과 구현 철학은 다르다. 이번 포스트에서는 Kotlin의 Coroutine에 대해서 전반적인 소개와 사용 방법을 소개하려고 한다.

     

     

     

    먼저 Coroutine은 하나의 task가 아니라, 여러 개 순서가 정해진 sub-tasks의 집합이다. Coroutine에서는 여러 개의 sub-task가 존재하는데 이들의 실행 순서는 보장된(guaranteed) 순서로 실행이 된다. 즉 코드 상에서는 얘네들이 sequential 하게 짜여져 있어도 코드를 어떻게 짜느냐에 따라서 실행 순서는 다를 수 있다. 이게 무슨 뚱딴지 같은 소리인가 싶을 수도 있지만 이건 asynchronous 작업을 좀더 유연하게 지원하기 위한 철학 정도로 이해하면 될 것 같다. 글로 보면 이해가 잘 되지 않을 수 있는데 예제 코드를 본다면 감이 어느정도 잡힐 것이다.

     

    0. Quick Example

     

    GlobalScope.launch {
        async { withContext(this.coroutineContext) { printLog("1") } }
        async { withContext(this.coroutineContext) { printLog("2") } }
        async { withContext(this.coroutineContext) { printLog("3") } }
    }

     

    안드로이드에서 Coroutine을 사용한 예제 코드다. 아직 Coroutine에 대해서 잘 모르더라도 느낌상으로는 로그를 찍는 함수 호출로 미뤄 볼 때 로그는 1 -> 2 -> 3 의 순서로 찍혀야 할 것 같다. 하지만 실제 출력 결과 매번 실행할 때마다 순서가 다르게 나온다.

     

    2020-04-15 20:38:06.190 24595-24791/kwony.study D/CoroutineSample: 1
    2020-04-15 20:38:06.191 24595-24790/kwony.study D/CoroutineSample: 3
    2020-04-15 20:38:06.191 24595-24791/kwony.study D/CoroutineSample: 2

     

    그런데 이번에는 로그 출력용 객체에 await 함수를 붙여주면

     

    GlobalScope.launch {
        async { withContext(this.coroutineContext) { printLog("1") } }.await()
        async { withContext(this.coroutineContext) { printLog("2") } }.await()
        async { withContext(this.coroutineContext) { printLog("3") } }.await()
    }

     

     이렇게 결과가 달라지고 값도 고정되게 나온다.

     

    2020-04-15 20:40:12.687 25004-25078/kwony.study D/CoroutineSample: 1
    2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 2
    2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 3

     

    Coroutine을 사용하면 작업의 순서를 요리조리 조정할 수 있다. 맛보기로 이정도면 좋을 것 같다.

     

    1. Coroutine Builder 

     

    Coroutine을 사용하려면 Coroutine 객체를 생성하는 작업을 먼저해야한다. Coroutine Builder는 Coroutine을 생성하기 위한 팩토리 함수다. Kotlin에서는 Coroutine을 생성하기위한 기본 빌더 함수를 제공한다.

     

    runBlocking

     

    현재 시작중인 thread를 block 시키고 Coroutine 작업을 시작하게 하는 Coroutine 빌더다. Main Thread에서 이 함수가 호출 됐으면 UI가 잠시 멈추고 coroutine 함수가 실행된다. 테스트를 위해 onCreate 함수에서 runBlocking Coroutine을 생성하고 3초간 딜레이를 주었다. 그 결과 화면이 초기화되는 시간이 3초간 딜레이되는 현상이 발생한다.

     

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    
        runBlocking {
            delay(3000)
        }

     

    synchronous하게 처리해야하는 작업에 사용되는데 현재 실행중인 thread를 block시키는 문제가 꽤 골치 아프기 때문에 가능하면 쓰지 않을 것을 추천하는 문서도 있다. 정말로 필요한 경우가 아니면 가급적 사용하지 않는 것이 좋을 수도.

     

    GlobalScope 

     

    runBlocking 빌더와 달리 현재 실행중인 Thread를 block 시키지 않고 백그라운드에서 작업을 실행하는 Coroutine이다. 최상위 Coroutine을 생성 할 때 사용하는데 이 Coroutine은 애플리케이션과 동일한 생명주기를 갖게 된다. 이에 대한 자세한 내용은 CoroutineScope 에서 소개하는 것이 좋을 것 같다. 예제 코드를 한번 보자.

     

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        GlobalScope.launch {
            delay(3000)
            Log.d(coroutineSampleTag(), "World")
        }
        Log.d(coroutineSampleTag(), "Hello")
    }

     

    GlobalScope 으로 만들어진 Coroutine 에서는 3초간 딜레이를 주고 World를 출력하고 생성 다음에는 Hello를 출력하는 코드다. 실행 결과는 아래와 같다. Hello가 먼저 출력되고 Coroutine 내부의 World 문자열은 3초뒤에 출력되는 것을 확인 할 수 있다.

     

    2020-04-15 21:21:45.224 30001-30001/kwony.study D/CoroutineSample: Hello
    2020-04-15 21:21:48.234 30001-30069/kwony.study D/CoroutineSample: World

     

    CoroutineScope(context: CoroutineContext)

     

    Coroutine이 동작할 context를 지정해서 Coroutine을 만드는 함수다. 저기 안에 인자에 특정 CoroutineContext를 넣을 수 있는데, 이에 대해서 자세하게 설명하는 것보다는 어떤 Thread에서 동작할 것인지 지정하는 것 정도로만 보면 좋을 것 같다. 대표적으로 Dispatchers.Main과 Dispatchers.IO가 있다.

     

    CoroutineScope(Dispatchers.Main).launch {}
    CoroutineScope(Dispatchers.IO).launch {}

     

    Dispatchers.Main으로 넣은 경우는 해당 Coroutine 작업이 MainThread 즉 UI를 관장하는 Thread에서 돌아가도록 선언한 것이고 Dispatchers.IO인 경우에는 Blocking IO 용 공유 쓰레드 풀에서 동작하도록 지정한 것이다. RxJava에서 subscribeOn 함수에 넣은 값들과 유사한 개념이다. 이 Builder 함수들도 GlobalScope 처럼 진행중인 Thread를 block 하지 않고 동작한다.

     

    2. async { }

     

    Coroutine 초기 예제에서도 확인 했던 것 처럼 Coroutine 작업들은 비동기적으로 처리하는 것이 가능하다. 비동기로 처리하려는 Coroutine들은 async { } 로 작업을 선언하면 된다.

     

    CoroutineScope(Dispatchers.IO).launch {
        printLog("Coroutine Start")
        val deferred1 = async {
            delay(3000)
            printLog("1")
            return@async 1
        }
        val deferred2 = async {
            delay(3000)
            printLog("2")
            return@async 2
        }
        val deferred3 = async {
            delay(3000)
            printLog("3")
            return@async 3
        }
        printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
    }

     

    예제에서 확장해서 async Coroutine에 딜레이 3초와 리턴값을 넣고 마지막에 각 Coroutine 결과의 총합을 출력하는 코드를 추가했다.

     

    2020-04-15 22:18:29.090 3877-3990/? D/CoroutineSample: Coroutine Start
    2020-04-15 22:18:32.108 3877-3996/kwony.study D/CoroutineSample: 2
    2020-04-15 22:18:32.109 3877-3993/kwony.study D/CoroutineSample: 3
    2020-04-15 22:18:32.112 3877-3991/kwony.study D/CoroutineSample: 1
    2020-04-15 22:18:32.125 3877-3996/kwony.study D/CoroutineSample: total sum: 6

     

    Coroutine 의 로그 출력 순서는 실행할 때마다 변경되는 반면 마지막에 Coroutine 반환 값의 합을 출력하는 부분은 항상 마지막에 출력되는데 이는 각 async 객체에 await() 함수를 사용해서 리턴값을 반환했기 때문이다. await() 함수는 async 함수가 모두 종료될 때 까지 구문 실행을 기다리도록 하는 함수다. 합을 출력하는 부분은 세개의 Coroutine의 연산결과를 모두 기다려야하기 때문에 제일 마지막에 실행 될 수 밖에 없다.

     

    또 주목할 만한 점은 모든 Coroutine에 3초씩 딜레이를 주었는데 각 Coroutine의 로그 출력 시간은 3초씩 차이가 나지 않는다. 이는 각각의 Coroutine이 Sequential하게 동작하지 않고 Parallel하게 동작했기 때문이다. Coroutine의 로그 출력 순서가 바뀌는 이유도 Parallel 하게 동작했기 때문이다.

     

    3. suspend fun method() = coroutineScope { }

     

    Kotlin 함수 선언 앞에 suspend와 coroutineScope 으로 body를 만들어주면 Coroutine 에서 실행가능한 함수로 선언 해줄 수 있다. 아래 코드는 앞서 예제로 소개한 코드랑 동일한 로직으로 동작하며 출력되는 결과도 동일하다 (Coroutine 로그 출력 순서만 빼면)

     

    private suspend fun coroutineMsg(msg: String, ret: Int): Int = coroutineScope {
        delay(3000)
        printLog(msg)
        return@coroutineScope ret
    }
    
    CoroutineScope(Dispatchers.IO).launch {
        printLog("Coroutine Start")
        val deferred1 = async { coroutineMsg("1", 1) }
        val deferred2 = async { coroutineMsg("2", 2) }
        val deferred3 = async { coroutineMsg("3", 3) }
        printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
    }
    2020-04-15 22:42:07.182 7959-8043/kwony.study D/CoroutineSample: Coroutine Start
    2020-04-15 22:42:10.197 7959-8045/kwony.study D/CoroutineSample: 1
    2020-04-15 22:42:10.197 7959-8048/kwony.study D/CoroutineSample: 3
    2020-04-15 22:42:10.198 7959-8043/kwony.study D/CoroutineSample: 2
    2020-04-15 22:42:10.217 7959-8044/kwony.study D/CoroutineSample: total sum: 6

     

    '개발 > 안드로이드' 카테고리의 다른 글

    Navigator - Getting Started  (0) 2020.04.20
    안드로이드 그림자(Shadow) 효과 넣기  (1) 2020.04.18
    Kotlin으로 깔끔한 Builder를 만들어보자  (1) 2020.04.14
    Exoplayer2 사용하기  (0) 2020.04.12
    FragmentManagers Android  (1) 2020.04.06

    댓글

Designed by Tistory.