
회사에서 Kotlin Springboot Webflux를 활용한 비동기 기반의 다량의 데이터를 처리 해야되는 비디오 업로드 전용 서버를 구축하면서 학습한 내용을 두서없이 정리해본다.
혹시나 이상한 것이 있거나 하면 정정해 주면 최고다.
코루틴이란, 비동기 작업을 효율적으로 처리할 수 있는 경량 스레드 라고 요약할 수 있다.
본론으로 바로 들어가, 다음 세 개의 순으로 코루틴을 살펴보자.
우선 routine에 대해 알고 넘어가자. 우리는 코딩을 할 때 매일 routine 이란 것을 잘 사용하고 있다.
맞다. 우리가 알고 있는 main routine / sub routine 으로 나뉜다.
fun main() {
// ...
val a = subRoutine(3)
// ...
}
fun subRoutine(val number): Int {
// ...
// ...
return number * 10
}
위에서 보이는 main이 메인으로 동작하는 main routine, main routine에서 서브 함수인 subRoutine(3)을 호출하면서 sub routine이 호출된다.
이건 우리가 너무나도 잘 알고 있는 일반적인 흐름이다.
서브루틴의 경우 처음부터 진입이 되어 맨 윗줄부터 순차적으로 진행이 되고, 쭉쭉쭉 밑의 코드들을 실행하다가 return 문을 만나, 해당 서브루틴을 빠져나온다. 또한, 진입점과 탈출점 사이의 쓰레드들은 block 되어있다.
하지만, 이제 코루틴이란 녀석을 살펴보자.
이 코루틴이란 녀석은 조금 다르다.

코루틴 또한 루틴이다. 코루틴을 하나의 함수로 이해해보자.
하지만 위의 그림을 보면, 일반적인 함수와 다르게 진입점과 탈출점이 한 개만 존재하는 것이 아닌, 여러 개가 존재한다. 위의 그림대로 해석해보자면, 코루틴은 언제든 나갔다 들어왔다 할 수 있는 뭔가 자유로운?느낌의 루틴인 것 같다.
fun drawPerson() {
coroutineScope {
drawHead()
drawBody()
drawLeg()
}
}
suspend fun drawHead() {
delay(1000)
// ...
}
suspend fun drawBody() {
delay(1000)
// ...
}
suspend fun drawLeg() {
delay(1000)
// ...
}
drawPerson 이라는 함수가 있다. 이 함수 안에는 cocoutineScope라는 코루틴 빌더가 보인다.
coroutineScope 라는 코루틴을 만나게 되면 해당 함수는 코루틴으로 동작하게 되고, 언제든 함수 실행 중간에 나갈 수도, 다시 들어올 수도 있게 된다. 그렇다면 코루틴은 언제든 나갈 수 있는 걸까?
보면 알겠지만 coroutine안에 사용된 함수들의 선언쪽을 바라보면 suspend 라는 키워드가 함수 앞에 붙어 있다. 이는 ‘일시 중단 함수’ 라는 것인데, 함수 내에 일시 중단 지점을 포함할 수 있는 함수 라는 의미이다. (자세한건 나중에 다룰수도 안다룰수도 있다.)
아무튼 이 suspend 라는 키워드가 있는 함수를 만나면 코루틴 밖으로 나갈 수 있게 되는 것이다.
다음은 위의 코드의 작동 순서이다.
drawPerson() 을 호출하면 coroutineScope 블럭을 만나 하나의 코루틴을 만들어 시작하게 된다.suspend 로 정의된 함수를 만나면 코루틴을 잠시 탈출한다. 위에서는 drawHead() 부분에서 더 이상 아래 코드를 실행하지 않고 코루틴 함수를 나온다.drawHead() 라는 suspend 함수를 만나 코루틴을 잠시 탈출했지만, 어디선가 drawHead() 는 계속 작동하고 있을 수도 있고, 다른 쓰레드에서 돌아가고 있을 수도 있다. 이건 개발자가 선택해야 되는 부분이다.drawHead() 가 끝나게 되면 아까 탈출했던 지점인 drawPerson() 으로 돌아와 drawHead() 아래 부분부터 코드가 다시 진행된다.이런 식으로 코루틴과 suspend는 동작한다.
위에서 보다시피, 코루틴의 이 나갔다 들어오는 이 특성은 동시성 프로그래밍을 가능하게 한다.
이번에 또 동시성 프로그래밍은 무엇인가.
병렬성은 많이들 들어봤겠지만, 동시성은 또 뭔가… 뭐가 다른건지 머리가 복잡하다.
| 동시성 | 병렬성 |
|---|---|
| 동시에 실행되는 것 같이 보이는 것 | 진짜 동시에 실행되는 것 |
| 싱글 코어에서 멀티 쓰레드를 동작시키는 방식 | 멀티 코어에서 멀티쓰레드를 동작시키는 방식 |
| 한 번에 많은 것을 처리 | 한번에 많은 일을 처리 |
| 논리적인 개념 | 물리적인 개념 |

뭐 이런식으로 생각하면 된다. 위의 표에서 이해되지 않는 말이 있지만 이해하기 쉽게 생각하자면,
이렇게 이해해 볼 수 있을 것 같다. 아무튼, 코루틴은 개념적으로 병렬성이 아닌 동시성을 지원한다.
fun drawPersonA() {
coroutineScope {
drawHead()
drawBody()
drawLeg()
}
}
suspend fun drawHead() {
delay(1000)
// ...
}
suspend fun drawBody() {
delay(1000)
// ...
}
suspend fun drawLeg() {
delay(1000)
// ...
}
fun drawPersonB() {
coroutineScope {
drawHead()
drawBody()
drawLeg()
}
}
suspend fun drawHead() {
delay(1000)
// ...
}
suspend fun drawBody() {
delay(1000)
// ...
}
suspend fun drawLeg() {
delay(1000)
// ...
}
위와 같이 메인 쓰레드 내에 두 개의 코루틴이 존재하는 상황이다.
메인 쓰레드가 먼저 만나는 코루틴이 위쪽의 drawPersonA() 라고 가정해보자. 위의 상황에서 보다시피, drawPersonA() 는 코루틴이고, drawHead() 를 만나는 코루틴을 잠시 빠져나온다.
하지만 위에서 말했다시피 메인 쓰레드는 놀고 있지 않을 것이고, 다른 suspend 함수를 찾거나 재개될 수 있는 다른 코드를 찾는다.
위쪽 코루틴을 빠져나온 쓰레드가 아래쪽 drawPersonB() 에 있는 코루틴을 만나게 되어, 또 한번 suspend 함수를 만나게 되면 아까 설명했던 동시성 프로그래밍 현상이 일어난다. 미친듯이 빠르게(?) 한손으로 그림을 그리는 상황이 발생하는 것이다.
이렇게 활용하면 쓰레드 하나에서 동시성 프로그래밍이 가능하다. 물론 이방법만 있는 것은 아닐 것이지만 말이다.
Context Switching
동시성 프로그래밍을 위해drawPersonA(),drawPersonB()를 번갈아가며 새로운 쓰레드를 점유했다가 놓아주고를 엄청 반복해야 한다. 이를 Context Switching 이라고 한다.
꽤나 비용이 많이 든다.
앞서 말해왔던, 코루틴이라는 코틀린의 능력으로 비동기 처리가 쉬워진다. 주인장도 하면서 이거 맞나…? 싶은 것들이 꽤 있었다.
예부터 들어보자. 이건 별점 낮은 웹툰에 자주 등장하는 소갈비찜 레시피이다.
이걸로 알아보자.
Callback을 사용해 구현해보자. 귀찮으니 몇개는 생략하겠다.
fun cookBeefRib(beef: Beef) {
val 소갈비 = beef
cutBeef(소갈비) { 토막난 소갈비 ->
putIntoColdWater(토막난 소갈비) { 핏물뺀 소갈비 -> {
stingWithKnife(핏물뺀 소갈비) { 칼집낸 소갈비 -> {
startBoil(칼집낸 소갈비) { 삶은 소갈비 -> {
seasonBeef(삶은 소갈비) { 양념된 소갈비 -> {
steamBeef(양념된 소갈비) { 맛있는 소갈비 -> {
맛있는소갈비.doEat()
}
}
}
}
}
}
}
써놓고 나니 나도 어이가 없다. 위에서 보이다시피, 콜백으로 구현할 때 흔히 나오는 Callback Hell이다. 심지어 에러 핸들링도 생략되어 있는데, 보는데도 눈이 아프다. (적을때 함수명 고민 많이했다.)
자 이제 오늘의 주인공, coroutine으로 작성된 코드를 볼까.
suspend fun cookBeefRib(beef: Beef) {
val 소갈비 = beef
try {
val 토막난 소갈비 = cutBeef(소갈비)
val 핏물뺀 소갈비 = putIntoColdWater(토막난 소갈비)
val 칼집낸 소갈비 = stingWithKnife(핏물뺀 소갈비)
val 삶은 소갈비 = startBoil(칼집낸 소갈비)
val 양념된 소갈비 = seasonBeef(삶은 소갈비)
val 맛있는 소갈비 = steamBeef(양념된 소갈비)
맛있는소갈비.eat()
} catch(e: Exception) {
callMyMom()
}
}
비동기 하는 것 같지도 않은 이따구(?)로 생긴 코드가 확실히 비동기 코드이다. 각 함수들은 오래걸리는 비동기 작업들이지만 순서는 확실하게 지켜진다.
이 것이 가능한 이유는 cookBeefRib() 라는 함수가 코루틴이기에 cutBeef() 함수를 만나면 실행함과 동시에 잠시 cookBeefRib() 을 빠져나간다. 그러다 cutBeef() 를 끝내면 다시 cookBeefRib() 으로 돌아와 작업을 진행하게 된다. 이게 코루틴 비동기 처리이다.
서버를 개발하면서 비동기 작업이 핵심이었기에, 또한 이렇게 제대로 비동기 처리를 해야되는 작업은 처음 제대로 해봤기에 RxKotlin, Coroutine 등 이것저것을 찾아보며 공부한 결과, 보기에도 이해하기에도 코루틴이 훨씬 쉬웠기에 이 코루틴을 너무 요긴하게 잘 사용했다.
효율적이고 접하기 가장 쉬운 Kotlin으로 할 수 있는 비동기 처리 방식이라고 생각한다.
이상한 것이나, 궁금한 것이 있다면 문의주세요😄