Kotlin

코루틴이란?

mayleaf 2023. 10. 23. 19:51

이 글을 쓰는 내용

이 글은 코루틴이 내부적으로 어떻게 동작하는지 큰 흐름을 작성하였다

이 글을 쓰는 목적

코루틴이 아닌 다른 Async Non-Blocking 프로그래밍을 경험해보신 분이 읽어보면 코루틴도 다 똑같다는 것을 알리고자 썼다.

특히 Node.js의 이벤트루프 개념을 알고 있는 사람이면 더 쉽게 이해하실 수 있을 것 같다.

그리고 그 알고 계신분들 중에서도 코틀린을 쓰는 이유가 있구나라는 생각이 들었으면 해서 이 글을 작성한다.

 

비동기 프로그래밍이란?

비동기 프로그래밍이 뭐냐고 물어본다면 이전에는 IO바운드, 오래걸리는 CPU 연산등을 메인쓰레드에서 처리하면 쓰레드 멜트다운이 일어나기 때문에 쓰레드 풀에 있는 다른 쓰레드에 작업을 넘기는 프로그래밍 기법이라고 설명했을 것이다. 적어도 Node.js는 이렇게 이해해도 무방하다고 생각했다.

 

그런데 이 용어, 프로그래밍 진영마다 이야기하는게 다르다. 그래서 이 부분이 오해의 소지를 항상 만든다.

Node.js 진영에서는 비동기 프로그래밍이 뭐냐고 물어보면 위에 이야기한 것처럼 이야기한다. 비동기라고 이야기하면 당연히 Nonblocking의 개념도 포함해서 생각하는 것이다. 

자바진영에서는 어떤 로직의 제어권을 caller가 아니라 callee에게 넘겨주는 것을 비동기 프로그래밍이라고 이야기한다. 로직의 결과물을 caller에서 받아서 caller가 직접 처리하는 것을 동기처리라고 이야기하고, callee가 이후 로직을 넘겨받았다는 전제하에, callee가 해야할 일을 다 하고 나면 caller로부터 받은 로직을 처리하는것을 비동기라고 한다. 블로킹은 어떤 작업을 callee한테 던졌을때, callee가 그 일을 마칠때까지 caller가 아무것도 하지 않고 pending되어있는 상태가 블로킹, 논블로킹은 callee한테 일을 던진 동안 다른 일을 하고 있어서 작업의 흐름이 막히지 않으면 논 블로킹이다.

블로킹여부와 동기여부가 자바진영에서는 구분되기 때문에 이게 한 쓰레드 위에서 일어나는 일이라고 하더라도, 이런 작업의 흐름을 자바에서는 비동기 방식이라고 부른다.

Node.js나 js  기반의 웹 프로그래밍을 해보신 분들은 아시겠지만, 노드 진영에서는 비동기라고 하면 당연히 비동기 논 블로킹을 상정하고 이야기한다. await을 이용한 promise 슈가링이 나오기 전까지, 그리고 promise이전에는 블로킹이 될만한 영역(api콜을 하거나 db 커넥션 풀에 sql 쿼리를 날릴때등)에는 아예 콜백을 직접 넘기는게 당연했다. 상식적으로 블로킹되는 부분이 아닌데, 콜백지옥을 맛보고 싶은 사람은 예나 지금이나 없었을 것이기 때문에 그때의 자바스크립트 개발자들은 당연히 비동기 == 논블로킹이었을 것이다.

논블로킹에 대해서 이야기하지 않은 것은 콜백을 사용하는 경우는 논블로킹인게 너무 당연했기 때문에 그러하였고, 아마 처음 자바스크립트를 접하시는 분들은 비동기의 개념안에 논블로킹까지 담아서 생각을 하기 쉬우셨을 것 같다.

 

그러나 jvm진영에서는 비동기와 논블로킹을 구분지어 생각한다.

하지만 지금부터 나오는 비동기라는 용어는 비동기 + 논블로킹을 전제로 이야기하는 것이다. 위에 언급하였다시피, 대부분의 경우 비동기를 쓰는 경우에는 논블로킹하게 일을 처리하기 위함이기 때문이다. (이 부분에서 너무 깊게 생각하지 말고, 일단 넘어가자. 나중에 동기여부와 블로킹여부 사분면을 가져온 블로그글을 써보도록 하겠다)

 

코루틴과 비동기 방식의 연관성

지금까지 비동기에 대해서 이야기한 이유는, 코틀린도 비동기를 다루는 방식이 있기 때문이다.

비동기 프로그래밍은 현대 소프트웨어 개발에서 핵심적인 역할을 한다. 그중에서 코틀린은 코루틴이라는 개념을 통해서 비동기 작업을 효율적으로 다루는 방법을 제공한다. 

코틀린 코루틴이란?

코틀린 코루틴은 비동기 프로그래밍을 지원하기 위한 구조로, 코드의 비동기 실행을 쉽게 관리하고 조절할 수 있게 해준다. 코루틴은 스레드를 블록하지 않고 비동기 작업을 수행할 수 있도록 해주기 때문이다.

 

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Start")
        delay(1000) // 1초 대기
        println("End")
    }
    Thread.sleep(2000) // 메인 스레드를 종료하지 않게 하기 위해 대기
}

runBlocking, launch, delay, 이런 것들은 우선 다 무시하고 흐름을 설명하면 runBlocking 스코프는 코루틴이 실행되는 스코프이다.

나중에 설명하겠지만 이 코루틴이 실행되는 스코프는 해당 영역내에서 쓰레드풀을 선정해서 논블로킹으로 작업을 실행할 수 있는 범위이다.

코루틴은 "Start"를 출력한 후, delay 함수로 일시 중지되며, 해당 시간 동안 다른 작업을 수행할 수 한다. delay는 겉보기로는 Thread.sleep과 동일한 역할을 할것으로 기대된다. 실제 동작도 runBlocking안에서는 유사한 역할을 한다. 하지만 이를 컴파일해서 까보면 delay이후부터 runBlocking 스코프가 끝날때까지의 코드들은 전부 비동기적으로 동작하게 변해서 continuation으로 감싸여져서 delay가 끝났을때 실행되는 코드처럼 동작한다. 노드에서 setTimeout에 두번째 인자로 콜백을 넘기는 것과 유사하게 생각하면 된다. 단지 코드가 절차적으로 동작하는 것처럼 보이도록 슈가링이 된 것이다.

setTimeout 같이 코루틴 스코프 안에서 흐름을 멈추게 될만한 함수들은 suspend함수라고 불린다. suspend하는 순간이 존재한다면 이 코드 뒤의 내용들은 모두 continuation이라는 일종의 콜백으로 감싸여져서 suspend 이후에 실행되도록 된다. 

이때 suspend되는 부분이 하나 이상이라면, suspend되는 지점들을 상태로 들고 있다가 상태를 계속 바꿔가면서 코루틴 스코프의 작업을 모두 실행할 수 있도록 동작하려한다.

continuation?

설명을 하던중 continuation이라는 단어가 계속 나왔는데, continuation이란 callee가 해야할 일을 모두 마치고나서 마지막에 실행되어야하는 로직이 담긴 객체이다. 항상 함수의 마지막에서만 실행이 가능한 콜백이라고 생각하면 편할 것 같다.

더 간단하게 설명하면 사실 동기적으로 작성된것처럼 보이는 코틀린의 코루틴스코프 내용 코드들은 내부적으로는 promise가 슈가링되어서 await ,async처럼 동작하는 것처럼 보이게 해주는 역할이고, 내부 동작은 결국은 자바스크립트 엔진이 동작하는 방식과 거의 유사하다.

 

코틀린, 코루틴 사용하는 이유

지금까지의 설명을 보면 자바스크립트 엔진이랑 다를게 없는데, 코루틴이 왜 특별한건가 싶을 수 있다.

 

코루틴의 가장 큰 장점은 자유롭게 쓰레드 풀을 선정할 수 있다는 점이다. 

코루틴의 경우 코루틴 컨텍스트에 어떤 쓰레드풀을 사용할 것인지 명시해서 사용할 수 있기 때문에 엄청 많은 객체들을 transform해줘야하는 일이 있거나 할때에 병렬로 돌려버릴 수가 있다. 그러나 자바스크립트 엔진의 경우 그렇게 해결하는 방법이 마땅치 않다.

Worker를 이용해서 쓰레드 풀을 사용할 수 있으나, 구조적으로 ForkJoinpool처럼 쓰레드 배분이 잘 되는지는 의문이기 때문이다. Worker를 사용할때 파일 이름이랑 데이터를 넘기는 모습을 보면 사용법도 직관적이라고 느껴지진 않는다.

 

사실 이렇게 쓰레드 풀을 선택하는 것은 Reactor를 사용할때에도 마찬가지인데, 이런 부분을 볼때 코틀린 자바가 왜 Generic하게 모든 곳에서 쓰일 수 있구나 하는 생각이 들곤한다. Node.js는 libUV빨로 돌아가는건데, 코틀린, 자바는 수틀리면 직접 쓰레드풀을 생성해서 관리할 수 있으니 일반적으로 어디에서나 사용할 수 있구나 라는 생각이 든다.

 

 

결론
코틀린 코루틴은 비동기 프로그래밍을 더 편리하게 만들어주는 강력한 도구이다. 이러한 비동기 작업은 continuation을 통해 관리되며, 이를 통해 코루틴은 일시 중지하고 재개할 수 있어서 편의성이 진짜 좋은 것 같다.