코틀린의 코루틴 — 1. Coroutines Basic

hongbeom
hongbeomi dev
Published in
7 min readJun 5, 2020

--

코루틴의 기본적인 기능에 대해 알아봅니다.

이 글은 공식 코루틴 가이드 링크의 내용을 기반으로 하여 작성하였습니다. 이 글을 작성할 당시 버전은 1.3.71입니다.

1. Coroutine Basic2. Cancellation and Timeouts3. Composing Suspending Functions4. Coroutine Context and Dispatchers5. Asynchronous Flow 1부6. Asynchronous Flow 2부7. Channels

What is Coroutine?

코루틴은 동시성 프로그래밍을 가능하도록 만든 개념입니다. 나온지 꽤 오래된 기술이지만 스레드에 대한 이슈가 많아지면서 최근 다시 조명을 받게 되었습니다. 코루틴은 Context Switching 오버헤드가 적은 Non-blocking 일종의 경량 스레드라고 할 수 있습니다. 코루틴은 프로그램이 실행 중일 때 특정 시점에 코루틴으로 이동하여 그 전에 실행하던 루틴을 정지하도록 하게 할 수 있습니다. 그렇다면 코틀린에서 코루틴은 어떻게 사용하고 어떤 식으로 구현이 되어있는지 기본부터 천천히 살펴보겠습니다.

First Step

먼저 아래 코드를 보겠습니다.

코루틴은 몇 개의 CorouineScope의 launch내의 corouine builder에서 실행되는데 위 코드에서 사용한 GlobalScope는 만들어진 코루틴의 생명주기가 전체 애플리케이션의 생명주기를 따라간다는 것을 의미합니다.

GlobalScope.lauch {...}thread {...} 로, delay(...)Thread.sleep(...) 으로 대체하여 동일한 결과를 기대할 수 있습니다. 하지만 GlobalScope.lauchthread 로 교체하는 것으로 시작하게 되면 컴파일러는 다음과 같은 오류를 만들어냅니다.

Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

왜냐하면 delay 는 스레드를 블록하지 않고 코루틴을 중단시키는 특수한 suspending function이므로 코루틴에서만 사용할 수 있기 때문입니다.

Blocking과 Non-Blocking 세계의 연결

위에서 봤던 첫 번째 예제는 non-blocking한 delay()와 blocking한 Thread.sleep()을 혼합한 예제였습니다. 어느 부분에서 blocking한 것인지, 어느 부분에서 non-blocking한 것인지 혼동하기 쉬웠는데 runBlocking 이라는coroutine builder를 사용하여 blocking에 대해 명확하게 알 수 있도록 해보겠습니다.

출력 결과는 동일하지만 이 코드는 non-blocking한 delay() 만을 사용합니다. runBlocking 을 호출하는 메인 스레드는 runBlocking 내부의 코루틴이 완료될 때까지 멈춰있습니다.

runBlocking 을 사용하여 메인 함수의 실행을 마무리하는 방법으로 보다 자연스럽게 위 코드를 다시 작성할 수 있습니다.

여기서 runBlocking<Unit> {...} 은 최상위 메인 코루틴을 시작하는데 사용되는 어댑터 같은 역할을 합니다. main() 함수가 Unit 타입을 리턴(즉, 아무것도 리턴하지 않는다.)하기 때문에 구체적으로 Unit 타입을 명시해주고 있습니다.

job을 기다리기

다른 코루틴이 작동하는 동안 잠시 딜레이를 주는 것은 좋은 방법이 아니기 때문에 백그라운드에서 시작한 job이 완료될 때까지 (non-blocking한 방식으로) 명시적으로 기다리게 할 수 있습니다.

이제 위에서 봤던 모든 코드와 결과는 동일하지만 메인 코루틴의 코드는 백그라운드의 작업 기간을 신경쓸 필요가 없어졌습니다!

구조화된 동시성

아직 코루틴을 바로 활용하기엔 아쉬운 점이 있습니다. GlobalScope.launch 를 사용할 때 최상위 코루틴을 생성하는데 생명주기가 애플리케이션의 생명주기를 따라가기 때문에 애플리케이션을 실행하는 동안 일부 메모리 자원을 소모하게 됩니다. 이 때 이 코루틴에 대한 참조를 잊지 않고 그대로 두는 것은 코루틴의 코드가 너무 오래 지연되는 경우같은 리스크가 존재하기 때문에 이는 상당히 위험합니다.

이를 위한 해결책은 GlobalScope 를 통해 코루틴을 만드는 것 대신, 우리가 보통 스레드를 사용하는 것처럼 우리가 수행하고 있는 작업의 범위 내에서 코루틴을 생성할 수 있습니다.

위 코드에서는 runBlocking 코루틴 builder를 이용하여 메인 함수 내부에서 사용되는 코루틴을 생성하였습니다. runBlocking 내부에서 생성하는 모든 코루틴 builder의 스코프는 코드 블록 내부로 제한됩니다. 왜냐하면 최상위 코루틴(예제에서는 runBlocking )은 모든 코루틴이 완료될 때까지 완료되지 않기 때문입니다.

Scope builder

다른 builder가 제공하는 코루틴 스코프 외에도 coroutineScope builder를 사용하여 자신만의 스코프를 선언할 수 있습니다. 이것은 코루틴의 범위를 만들고 모든 하위 launch들이 완료될 때까지 완료되지 않습니다.

runBlockingcoroutineScope 는 둘 다 자신 블록 내부의 동작이 완료되기를 기다리기 때문에 비슷하게 보일 수 있습니다. 이 둘의 주된 차이점은 runBlocking 은 메서드가 대기 중인 현재 스레드를 차단하는 반면 coroutineScope 는 중단되는 동안 다른 사용을 위해 기본 스레드를 해제한다는 것입니다. 이런 차이점 때문에 runBlocking 은 정규 함수이고 coroutineScope 는 일시 중단 함수입니다.

다음과 같은 예로 증명됩니다.

“Task from coroutine scope”가 출력된 직후 coroutineScope가 아직 완료되지 않았음에도 불구하고 “Task from runBlocking”가 출력됩니다.

Extract function 리팩토링

이제 launch{…} 안에 있는 코드 블록을 추출하여 별도의 함수로 만들어보겠습니다. 아래 코드에 대해 함수로 추출하는 리팩토링을 실시하면 suspend 한정자가 있는 새로운 함수가 나오게 됩니다. Suspending 함수는 일반 함수와 마찬가지로 코루틴 내부에서 사용할 수 있지만 코루틴의 실행을 정지시키기 위해 (delay 같은) 다른 suspend 함수를 차례대로 실행할 수 있다는 것입니다.

Coroutines ARE light-weight

위 코드에서는 10만개의 코루틴이 launch 되고 1초 후 각 코루틴이 점을 찍는 작업을 실행하게 됩니다. 이것을 실행하면 어떻게 될까요? (메모리 부족 오류를 발생시킬 가능성이 매우 높습니다.)

Global coroutine은 Daemon thread와 같다.

다음 코드는 GlobalScope에서 장기간 실행되는 코루틴을 launch 하는데 이 코루틴은 “I’m sleeping”을 초당 두 번씩 출력한 다음 약간의 지연 후 리턴됩니다.

GlobalScope에서 만들어진 활성화된 코루틴은 프로세스를 지속시키지 못합니다. 이는 데몬 스레드와 유사합니다.

읽어주셔서 감사합니다🙌

다음 글에서 Cancelling and Timeouts 라는 주제로 이어서 작성하겠습니다.

--

--