지금까지는 비동기 처리하면 RxProgramming이 먼저 떠올랐지만 Google에서 Android 공식 언어를 Java에서 Kotlin으로 변경한 이후 Kotlin Coroutine을 주로 사용하는 추세이다. 과연 Coroutine은 무엇일까?
Coroutine?
Coroutine은 Kotlin에서만 지원하고 있는 개념이 아니다. 그러므로 ‘Ko’routine(Kotlin + routine)이 아니라 ‘Co’routine(Co- + routine)이다. 간단하게 설명하자면 Coroutine은 일종의 가벼운 스레드(Light-weight thread)로 동시성 작업을 간편하게 철할 수 있게 해주는 역할을 한다.
위키피디아에서는 아래와 같이 정의한다.
실행의 지연과 재개를 허용함으로써, 비선점적 멀티태스킹을 위한 서브 루틴을 일반화한 컴퓨터 프로그램 구성요소
여기서 비선점적 멀티태스킹 / 서브 루틴은 무슨 뜻일까?
비선점적 멀티태스킹?
컴퓨터구조에서 많이 들었던 개념인 것 같은데 비선점형은 하나의 태스크가 다른 태스크 가 실행 중이어도 프로세서(CPU)를 차지할 수 있다. 반대로 선점형은 하나의 태스크가 다른 태스크가 실행 중이라면 프로세서(CPU)를 차지할 수 없다.
코루틴은 비선점형 멀티태스킹, 스레드는 선점형 멀티태스킹이다. 그러므로 코루틴은 병행성(=-동시성)은 제공하고 병렬성은 제공하지 않는다.
루틴?
Routine은 하나의 Task, Function 이라고 이해해도 될 것 같다. 보통 프로그램은 다양한 Routine들을 조합시켜 제작한다. Routine은 Main Routine과 Sub Routine으로 나뉘는데 Main Routine이 Sub Routine을 호출하는 방식이다. Coroutine 또한 Routine의 한 종류이지만 다음과 같은 특징이 있다.
- Main-Sub 개념을 구분하지 않는다. 그러므로 모든 Routine들이 서로를 호출할 수 있다.
- 진입과 탈출이 자유롭다. Sub Routine은 return을 만나야만 탈출할 수 있다.
Coroutine을 왜 사용할까?
개발을 하면서 가장 머리를 싸매는 순간은 어떤 코드를 동시에 처리를 해야 하는지, 반대로 그러면 안되는지를 결정하는 순간일 것이다. 여기서 결정한다고 하더라도 코드를 작성했을 때 동시에 처리를 하는 경우 순서에 따라 결과가 달라져 문제를 해결할 수 없는 경우가 다반사이다. 특히 Android에서는 UI를 그리는 작업, 데이터를 받아오는 작업을 동시에 수행하는 경우가 많기 때문에 이를 제어하기 위해서는 비동기 처리는 필수적이다.
그렇다고 기존에 비동기 처리를 위한 방법이 아예 없는 것은 아니었다. RxJava라는 것이 존재했다. 하지만 현재 Coroutine으로 대체되는 가장 큰 이유는 RxJava의 러닝커브가 상당하기 때문이다.
Coroutine 장점
위의 설명들을 바탕으로 Corutine의 장점을 정리하면 아래와 같다.
- Routine간 협력을 통한 비선점적 멀티태스킹
Coroutine을 사용하면 비동기로 Routine을 실행하고 일반적인 Sub Routine과 다르게 진입과 탈출이 자유로워 Routine간 협력을 통해 비선점적 멀티태스킹을 가능하게 한다. - 동시성 프로그래밍 지원
동시성 프로그래밍이란 2개 이상의 프로세스가 동시에 작업을 하는 상태를 말하는데, Coroutine은 단일 코어에서 실행되어야 하기 때문에 각 Routine을 교차 배치 한다. 다중 Thread를 이용하게 되면 각 Thread 간 교체 시 Context Switching 비용이 발생한다. 하지만 Coroutine은 하나의 Thread 내에서 스케줄링이 가능하기 때문에 경량 쓰레드(Light-weight thread)라고도 불린다. - 쉬운 비동기 처리
Multi Thread와 비교했을 때 Thread 간 통신과 콜백 구조로 코드가 흐르지 않기 때문에 코드 흐름 파악이 쉽다. 또한, 개발자가 직접 작업을 스케줄링하기 때문에 코드 작성이 간단하고 예상하지 못한 상황을 줄일 수 있다.
Coroutine을 직접 사용해보자!
Coroutine Tutorial
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
위는 공식 문서에 있는 간단한 Coroutine 예제이다. 실행 결과는 아래와 같다.
Hello
World!
코드를 하나하나 간단히 뜯어보겠다.
- launch: Coroutine Builder이다. 코드의 나머지 부분과 동시에 새로운 코루틴을 실행하여 독립적으로 작동한다. ‘Hello’가 먼저 출력된 이유이다.
- delay(1000L): 특별한 suspend 함수이다. 특정 시간 동안 코루틴을 일시 중단한다. 코루틴을 일시 중단하면 기본 스레드는 차단되지 않기 때문에 다른 코루틴에서 스레드를 이어서 사용할 수 있다. 여기서 1000L은 1초이다.
- runBlocking: Coroutine Builder이다. 위의 코드에서 코루틴은 모두 runBlocking 안에 포함되어 있다. 만약 그렇지 않다면 에러가 발생할 것이다.
결국 정리하면 CoroutineScope 안에서 1초 뒤 World!를 출력하는 코루틴이 만들어졌고 Hello를 출력하는 코드는 해당 코루틴과 별도로 Main Coroutine에 존재하므로 Hello 다음에 World! 가 출력되게 된다. 더 자세한 내용은 아래에서 설명하겠다.
Suspend Function
위의 코드에서 1초 뒤 World!를 출력하는 코루틴을 따로 함수로 뺀다면 어떻게 해야할까? 해당 작업을 위해서 suspend function이라는 것이 존재한다.
아래와 같이 suspend keyword를 사용하여 함수를 분리할 수 있다. 결과는 동일하다.
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
결론
Coroutine은 너무 중요한 개념이다. 그런데 처음 이해하기에는 어려운 감이 없지않아 있다. 자주 사용해보고 실제로 적용해보는 과정에서 이해를 할 수 있다면 익숙해지지 않을까 싶다.
참고 웹사이트
https://kotlinlang.org/docs/coroutines-basics.html
https://dev.gmarket.com/82
https://eocoding.tistory.com/88
https://whyprogrammer.tistory.com/596