본문 바로가기
Language/Kotlin

[Kotlin] 코루틴 (Coroutine) #1 - 기본기 1

by 신숭이 2022. 1. 10.

[Kotlin] 코루틴 (Coroutine) #1 - 기본기 1

 

 

 

개념

 

 

코루틴은 동시성 프로그래밍을 위해 탄생하였으며, 일명 '루틴(Routine)' 이라고 불리는 논블로킹(Non-Blocking) 작업(Job)들을 정의한 뒤, 이것들을 조화롭게 활용하여 멀티태스킹을 수행하는 것을 말한다. 그래서 여러 루틴들의 협력한다하여 코(Co)루틴이다. (코틀린의 'Ko'도 아니고, 코틀린만의 개념도 아니다!)

 

논블로킹(Non-Blocking)은 현재 작업을 수행중인 메인스레드를 중단(Blocking)하지 않고, 백그라운드에서 작업을 수행하여 현재 스레드를 종료하지 않도록하는 비동기적 작업 수행을 말하는데, Java 에서는 쓰레드(Thread, Runnable)을 통해 이러한 작업 구현이 가능했다. 그러나 이런 비동기적 코드 구현은 성능은 좋지만, 코드가 복잡해져 안전하지 못한 코드를 만들 가능성이 높아진다. 또한 쓰레드는 비즈니스 코드에서 잦은 문맥교환을 유발해 비용이 많이든다.

 

코틀린이라는 언어는 태생적으로 보일러플레이트(Boilerplate)한 코드(자주 반복되는 복잡한 코드)를 제거하는게 목표이다. 따라서 코루틴은 위와 같은 비동기 작업 코드의 복잡성을 줄일 뿐 아니라 메모리 또한 최적화하는 기술로 코틀린의 목표에 부합한다. 

 

 

 

 

코루틴 = 백그라운드 스레드 인가? 

 

 

각 루틴이 스레드에서 돌아가는 것은 맞다. 하지만 루틴 하나가 스레드 하나를 차지하는 것은 아니다. 내부적으로 필요한 만큼의 최적의 스레드를 사용한다. 

코틀린 문서에서는 위와 같이 설명한다. 코루틴의 가장 큰 특징은 비용없이 일시중단가능한(suspendable) 것이고, (스레드는 문맥교환으로 인한 비용 발생), 특정 스레드에 속하지않고 한 스레드에서 중단된 루틴다른 스레드에 의해 재실행될 수 있다. 실행 루틴이 많지 않은경우에는 내부적으로 하나의 스레드에서 여러개의 코루틴을 실행할 수도 있다.루틴의 일시 중단은 사용자에 의해 제어될 수도 있다!

 

 

 

 

이 포스트에서 공부할 것

 

  • 코루틴 스코프(Coroutine Scope)
  • 코루틴 빌더(Coroutine Builder) : launch
  • 일시 중지 가능한 함수(Suspend Function)

 

 

 

코루틴 빌더

 

 

코루틴 빌더(Coroutine Builder)는 새로운 코루틴 블록을 생성하며 이 블록 내의 코드는 현재 스레드를 차단하지 않고, 백그라운드에서 비동기적으로 수행된다. 코루틴 빌더는 코루틴 스코프(Coroutine Scope) 내에서만 사용이 가능하며 크게 launchasync가 있다. 코루틴 스코프는 코루틴 빌더와 일시중단(Suspend)함수를 사용할 수 있는 영역으로, 우선 아래의 기본 예시를 먼저 보자. 기본 예시도 많은 것을 담고 있다.

 

fun main() { // 메인스레드에서 동작
    GlobalScope.launch{		// 코루틴 스코프
        delay(1000L)    	
        println("World")
    }
    
    println("Hello, ")
    Thread.sleep(2000L)     // JVM에서 메인스레드 바로 종료 방지
}
Hello,
World!

이 프로그램은 실행하자마자 Hello, 를 띄우고 1초뒤 World를 띄운뒤 1초뒤 종료한다. 

 

 

GlobalScope 

GlobalScope는 코루틴 스코프의 일종이다. 코루틴 빌더(launch)는 코루틴 스코프 내에서만 작동하므로 GlobalScope로 작성하였다. 즉 코루틴 빌더(launch)의 실행 범위를 Global 영역으로 제한하는 것으로, GlobalScope는 메인 스레드와 생명주기를 함께한다. 따라서 메인 스레드가 종료되면 GlobalScope내의 코루틴 역시 종료되므로 위 예시에서 Thread.sleep(2000L)을 두지 않았다면 "Hello, " 만 보고 프로그램이 종료되었을 것이다. 코틀린 공식문서에서는 메모리 관리의 어려움을 유발할 수 있는 GlobalScope의 사용을 권장하지 않는다.

 

 

권장하는 방식은 아래와 같다. 아래의 코드는 위의 예시와 완전히 동일한 출력 결과를 보인다.

 

fun main() = runBlocking { 
    launch{
        delay(1000L)
        println("World")
    }
    println("Hello, ")
    // 종료 지연 구문이 없어도 된다
}
Hello,
World!

 

runBlocking 

새로운 코루틴을 실행하고, 완료되기 전까지 현재의 스레드를 블로킹한다.

이것도 일종의 코루틴 빌더로서, 해당 함수를 블로킹 가능하게 한다.  블로킹가능 하다는 것은, 해당 함수가 작동하는 스레드에서 함수 내의 코루틴이 완전히 종료되기 전까지, 해당 스레드의 종료를 막는다(Blocking)는 것이다. 위 코드는 main함수에서 실행하고 있으며, main 함수의 코드는 메인 스레드(Main Thread)에서 돌고 있다. 여기서 이 main 함수를 runBlocking 모드로 설정한다면, 이 함수 내의 코드를 코루틴 스코프에 포함시키고, 이 코루틴 스코프가 작동하고 있는 스레드(여기서는 Main Thread)는 하위 코루틴의 종료전까지 종료를 미룬다. 때문에 여기서는 Thread.sleep(2000L) 같은 혐오스러운 코드가 필요없다.

 

 

launch 

async와 함께 대표적인 코루틴 빌더이다. 이번 포스트는 launch부터 살펴보도록 하겠다. launch는 현재 스레드를 차단하지 않고, 새로운 코루틴을 실행할 수 있게 한다. launch는 새로운 코루틴 블록을 생성하며 launch의 특징은 Job 객체를 반환한다는 것이다. Job은 간단한 생명주기를 가지는 작업의 단위이며 아래의 예시를 통해 살펴보자.

 

fun main() = runBlocking { //  Coroutine Scope (상위 루틴)
    var job = launch {	// Coroutine Scope (하위 루틴)
        delay(1000)
        println("World!")
    }

    println("Hello,")
    println("job.isActive: ${job.isActive}, complete: ${job.isCompleted}")
    delay(2000)
    println("job.isActive: ${job.isActive}, complete: ${job.isCompleted}")
}
Hello,
job.isActive: true, complete: false
World!
job.isActive: false, complete: true

 

 

Job의 생명주기는 다음과 같다. Job 은 백그라운드에서 실행하는 작업으로, 특정한 조치를 취하지 않으면 launch 시, ACTIVE 상태로 작업을 시작한다. Job에 대한 설명은 추후에 이어서 하겠다.

 

 

 

일시 중지 가능한 함수 (Suspend Function)

 

 

중단 함수, 일시 중지 가능한 함수, 지연 함수 등 다양한 명칭으로 부르지만, 가장 그 특성을 잘 말하는 이름은 '일시 중지 가능한 함수' 인 것 같다. 근데 이름이 너무 기니까, 이 포스트에서는 Suspend 함수로 통칭하겠다.위의 예시를 보면 delay() 라는 함수를 볼 수 있다. delay의 선언부를 살펴보면 다음과 같다.

 

public suspend fun delay(timeMillis: Kotlin.Long) : Kotlin.Unit { ... }

 

우선 간단하게 말하면 suspend 함수는 코루틴 스코프내에서만 사용이 가능하다. delay() 도 suspend 함수이기에 코루틴 스코프 내에서만 사용가능하다. 다음의 예시를 보자.

 

fun main() = runBlocking { // this: CoroutineScope
    launch { 
    	doWorld() 
    }
    println("Hello")
}

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

 

Hello
World!

 

위 코드는 여태 나왔던 결과와 동일한 결과를 보인다. 부모 코루틴에서 자식 코루틴을 launch 로 빌드하여 만약 코루틴 스코프 내에서 사용하지 않는다면 컴파일 에러가 뜬다.

Suspend 함수는 일시 중단 가능하다고 했다. 이는 Suspend 함수에 의해 Suspend 함수를 호출한 코루틴이 잠시 중단되었다가 Suspend 함수의 작업이 종료되면 다시 해당 코루틴을 재개하기 때문에 일시 중단(Suspend) 함수이다. Suspend 함수는 Intellij IDE 에서도 아래와 같은 아이콘으로 표시해준다.

 

아래의 코드를 보자.

어떻게 실행될까? 위의 코드와 다른점은 doWorld()를 자식 코루틴 스코프에 두지 않았다.

 

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

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

 

이렇게 되면 부모 코루틴 스코프는 doWorld()를 실행하는 동안 일시 중단된다. 그러므로 출력은 다음과 같다.

 

World!
Hello

 

 

Suspend 함수는 아래와 같이 coroutineScope 내에서 실행하도록 미리 지정할 수도 있다. 

 

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

 

 

 

 

댓글