코루틴 공식 가이드 읽고 분석하기 — Part 1
공식 가이드 읽기 (1 / 8)
Korean [English]
(목차로 돌아가기)
위 예제는 메인 함수 안에서 GlobalScope.launch { } 코드블록을 이용하여 Hello, World! 를 출력하는 간단한 프로그램입니다. 함수의 가장 마지막 라인에서 2초간 정지(Sleep)하는 코드가 쓰인 이유는 코드에서 우리가 사용한 GlobalScope.launch { } 라는 코드 블록의 특성과 관련이 있습니다.
GlobalScope.launch {} 코드 블록은 사실 코루틴을 생성하기 위한 코루틴 빌더이며 이렇게 생성되어 실행되는 코루틴은 호출(실행) 스레드를 블록하지 않기 때문에 그대로 두면 메인 함수가 종료되고 메인 함수를 실행한 메인 스레드 역시 종료되어 프로그램이 끝나게 됩니다. 이를 방지하기 위해 임의의 시간을 지정하여 지연 시킨 것 입니다. 이렇게 스레드를 멈추는 역할을 수행하는 함수를 중단 함수(Blocking function) 이라고 합니다.
우리는 이러한 중단 함수가 현재 스레드를 멈추게 할 수 있다는 것을 코드상에 보다 명시적으로 나타내기 위해 다음과 같이 runBlocking {} 블록을 사용할 수 있습니다.
runBlocking{} 블록은 주어진 블록이 완료될 때까지 현재 스레드를 멈추는 새로운 코루틴을 생성하여 실행하는 코루틴 빌더 입니다.
코루틴 안에서는 runBlocking { } 의 사용은 권장되지 않으며, 일반적인 함수 코드 블록에서 중단 함수를 호출할 수 있도록 하기 위해서 존재하는 장치 입니다. (위 예제에서는 delay() 중단 함수를 사용하기 위해 쓰였습니다.)
메인 함수 자체를 runBlocking {} 코루틴 빌더를 이용하여 작성하면 지연을 위한 delay() 중단 함수의 사용이 보다 자연스러워 집니다.
delay() 는 중단 함수이며 모든 중단 함수들은 코루틴 안에서만 호출될 수 있다는 제약이 있습니다. GlobalScope.launch{ } 코드블록에서 delay(1000L) 를 사용할 수 있었던 이유도 GlobalScope.launch{ } 가 주어진 코드블록을 수행하는 코루틴을 생성하는 코루틴 빌더이며, 해당 코드블록은 코루틴 안에서 수행되고 있기 때문입니다.
지금까지의 예제에서는 GlobalScope.launch{ } 로 실행 된 코루틴의 수행이 완료될때 까지 현재 스레드(main 함수)를 대기시키기 위해서 임의의 지연(2초)을 주었는데 이것은 실제 프로그램에서는 적절한 방법이 아닙니다. 그 이유는 내부적으로 실행중인 코루틴(자식 코루틴)들이 작업을 완료하고 종료 될 때까지 얼마나 대기해야 할 지 부모 코루틴은 예측할 수 없기 때문입니다.
이러한 문제를 해결하기 위해서 우리는 다음과 같이GlobalScope.launch{ } 의 결과로 반환되는 Job 인스턴스를 이용할 수 있습니다.
이제 runBlocking{ } 빌더로 생성 된 코루틴 블록(편의상 이후부터는 메인 코루틴이라 칭함)은 GlobalScope.launch{ } 빌더를 이용해 생성한 코루틴이 종료될 때까지 대기한 후 종료됩니다. 이는 자식 코루틴의 실행 흐름에 연결됨으로써 가능했습니다.
job.join()
여기서 한가지 생각해 볼 만한 문제가 있습니다. 메인 코루틴 안에서 두개 이상의 자식 코루틴들이 수행되고, 우리는 모든 자식 코루틴들의 종료를 기다리도록 구현해야 한다면 어떨까요? 우리는 자식 코루틴들에 대응되는 모든 Job 객체들의 참조를 어딘가에 유지하고 있다가 부모 코루틴이 종료되어야 하는 시점에 실행 된 모든 자식 코루틴들의 Job 들에 join 하여 자식 코루틴들의 종료를 기다려야 할 것입니다. 너무 번거롭지 않나요? 바로 이 부분이 코루틴 스코프(Scope)가 빛을 발하는 부분입니다.
모든 코루틴들은 각자의 스코프를 갖습니다. 그래서 다음과 같이 runBlocking{ } 코루틴 빌더등을 이용해 생성 된 코루틴 블록 안에서 launch{ } 코루틴 빌더를 이용하여 새로운 코루틴을 생성하면 현재 위치한 부모 코루틴에 join() 을 명시적으로 호출할 필요 없이 자식 코루틴들을 실행하고 종료될 때까지 대기 할 수 있습니다.
Scope builder
만일 어떤 코루틴들을 위한 사용자 정의 스코프가 필요한 경우가 있다면 coroutineScope{ } 빌더를 이용할 수 있습니다. 이 빌더를 통해 생성 된 코루틴은 모든 자식 코루틴들이 끝날때까지 종료되지 않는 스코프를 정의하는 코루틴 입니다.
이 시점에 우리는 예제로 계속 사용하고 있는 runBlocking{ } 빌더와 coroutineScope{ } 빌더가 무슨 차이가 있는지 궁금할 수 있습니다. 그 차이는 runBlocking{ } 과 달리 coroutineScope{ } 는 자식들의 종료를 기다리는 동안 현재 스레드를 블록하지 않는다는 점 입니다.
위 예제의 실행 결과는 다음과 같습니다.
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
(runBlocking 과의 차이를 보기 위해 coroutineScope 를 runBlocking 으로 바꿔서 실행 해보면 Task from runBlocking 이 가장 마지막에 출력됩니다. launch{ } 블록이 실행 기회를 얻지 못하였기 때문입니다.)
Extract function refactoring
지금까지의 코루틴 예제들은 하나의 메인 함수 블록 안에모든 로직을 기술하였습니다. 이것은 우리의 일상적인 코딩 패턴이 아니며 단지 샘플을 위한 뚱뚱한 함수일 뿐입니다. 이제 이것들을 그 용도에 맞는 개별 함수로 분리하여 보다 실용적으로 사용할 수 있는 패턴으로 변경해 보겠습니다.
위 샘플 코드에서 볼 수 있는것처럼 코루틴 내부에서 실행되는 중단 함수들은 suspend 키워드를 함수명 앞에 붙임으로써 만들 수 있습니다. 이러한 함수들이 일반 함수와 비교해 갖는 차이점은 delay() 와 같은 다른 중단함수들을 호출 할 수 있다는 점입니다. 그 이유는 suspend 키워드를 붙여 만든 이 함수 역시 중단 함수이기 때문에 특정 코루틴 컨텍스트 안에서 수행되고 있고, 코루틴 컨텍스트 안에서는 모든 중단 함수를 호출 할 수 있기 때문입니다.
하지만 만약 중단 함수가 현재 스코프에서 수행 될 코루틴 빌더를 포함한다면 어떨까요? 이 경우에 suspend 키워드 만으로는 충분하지 않습니다. 위 예제에서 doWorld() 함수를 CoroutineScope 의 확장함수로 만드는 방법을 생각해 볼 수있겠지만 이것은 API 를 불명확하게 만듭니다.
좀 더 나은 방법은 명시적으로 CoroutineScope 을 필드로 갖는 클래스를 만들고 그 클래스가 해당 suspend 함수를 갖게하는 것, 혹은 외부(outer) 클래스의 구현을 암시적으로 사용하는 방법이 있습니다.
또 다른 수단으로는 CoroutineScope(coroutineContext) 자체를 생성하여 사용할 수 있지만 이러한 접근 방식은 구조적으로 안전하지 않습니다. 왜냐하면 이러한 방식을 사용하는 순간부터 우리에겐 더 이상 이 메서드가 실행되는 스코프를 컨트롤 할 방법이 없어지기 때문입니다. private API 만이 이 빌더를 사용할 수 있습니다.
Coroutines are light-weight
일반적인 스레드 구현으로는 메모리 부족(Out-Of-Memory) 오류가 발생할 수 있는 동작도 다음과 같이 코루틴으로 작성하면 정상적으로 동작합니다.
위 예제는 십만개의 코루틴을 수행하고 1초후에 각각의 코루틴들은 점(.)을 출력합니다. 같은 동작을 스레드로 구현하여 수행해 본다면 아마도 여러분의 코드는 Out-Of-Memory 류의 메모리 부족 예외를 발생 시켰을 것입니다.
Global coroutines are like daemon threads
다음 코드는 오랜 시간동안 Global scope에서 수행되는 코루틴을 만들어 실행합니다. 이 코루틴은 “I’m sleeping” 이라는 문자열을 500ms 간격으로 천번 출력합니다. 이렇게 무거운(오래걸리는) 코루틴이 수행되는 동안 메인 함수는 그것보다는 짧은 시간을 대기한 후 종료합니다.
위 예제에서 Global scope 에서 실행 된 코루틴은 마치 데몬 스레드와 같이 자신이 속한 프로세스의 종료를 지연시키지 않고 프로세스 종료 시 함께 종료되기 때문에 다음과 같이 허용된 시간 동안만 동작한 결과를 만들어 냅니다.
I’m sleeping 0 …
I’m sleeping 1 …
I’m sleeping 2 …
끝.
(목차로 돌아가기)