오늘은 코루틴에 대해 알아보려고 한다.
코루틴은 범위가 넓으므로 여러 챕터로 나눠 설명 해볼까 한다.

간단한 예를 들어보자.
빵을 굽는 동안 제빵사가 오븐 앞에서 기다린다면?
우리는 위와 같은 상황에서 어떤 느낌이 들까?
필자는 비효율적이다고 느낀다.
빵을 굽는 동안 제빵사는 다른 일을 하면 보다 효율적이지 않을까?
위와 같이 제빵사는 빵이 오븐 안에서 익어가는 동안 다른 일을 하면 보다 효율적으로 시간을 사용할 수 있을 것이다.
I/O 작업을 요청하고 데이터를 읽어오는 20초 동안 컴퓨터가 기다린다면?
기다리는 20초 동안은 다른 일을 할 수 있지 않을까?
위 과정을 실현 시켜주는 게 코루틴이다.
코루틴은 하나의 프로세스가 CPU를 할당받으면 강제로 다른 프로세스가 차지할 수 없는
비선점형 방식을 일반화한 프로그램 요소이다.
즉, 제빵사가 빵을 구울 때 다른 일을 할 수 있게 해주는 요소이다.
여러 설명보다 직접 코드를 보며 하나씩 실천해보자.
fun myCoroutine() {
runBlocking { // MainThread를 가져와 처리해주는 코루틴 빌더
launch { //현재의 Thread를 차단하지 않고 새로운 코루틴을 생성하는 빌더
doThree() -> '3'
}
launch {
doTwo() -> '2'
}
doOne() -> '1'
}
}
위 코드의 실행 결과는 아래와 같다.
1 -> 3 -> 2
왜 1 -> 3 -> 2라는 결과 나왔을까?
제일 먼저 살펴봐야 하는 포인트는 runBlocking이다.
runBlocking은 MainThread를 가져와 처리해주는 코루틴 빌더이다.
즉, runBlocking안에 기록된 작업 내용은 MainThread에서 처리 하기 때문에 doOne() 함수가 첫 번째로 호출된다.
갑자기 MainThread를 가져왔다고 doOne() 보다 위에 있는 doThree(), doTwo()가 doOne() 보다 호출이 늦게 되는 건 이해하기 어려운 상황이다.
해당 부분을 이해하기 위해 launch에 대해서도 알아보자.
Launch는 runBlocking과 달리 현재 Thread를 차단하지 않고, 새로운 코루틴을 생성하는 빌더이다.
즉, runBlocking은 부모 코루틴이고, 그 안에 있는 launch는 자식 코루틴인것이다.
그리하여 부모 코루틴안의 내용인 doOne()이 먼저 호출되고, 이후 자식 코루틴이 시작되며 doThree(), doTwo() 함수가 호출되는 것이다.
예시를 통해 알아보자.
fun myCoroutine() {
runBlocking {
launch {
delay(1000L) ->' 1초동안 CPU 사용안할거에요.'
doThree() -> '3'
}
launch {
delay(500L) ->' 500밀리초동안 CPU 사용안할거에요.'
doTwo() -> '2'
}
doOne() -> '1'
}
}
위 코드의 실행 결과는 1 -> 2 -> 3 이다.
위와 같이 부모 코루틴에서 doOne()을 실행하고, 이후 launch 자식 코루틴을 실행할 때 첫 번째 자식 코루틴에서 delay() 함수를 활용해 1초동안 딜레이를 발생 시켰다.
이에 1초동안의 여유가 생겼기에 다음 자식 코루틴인 doTwo에서 500ms를 활용해 doTwo()를 작동 시키고 이후 doThree()가 작동한 것 이다.
이처럼 코루틴은 CPU의 사용이 끝나는 시점에서 스스로 다른 자식 코루틴에게 자원을 양보하는 효율적인 요소이다.
suspend 함수는 작업중에 잠에 들 수 있는 요소가 포함되어 있음을 뜻하는 함수이다.
예를 들어보자.
suspend fun doTwo() { //코루틴안에서 delay 등을 사용할 때 사용되는 Suspend 함수.
delay(1000L)
Log.d(TAG, "two")
}
}
suspend fun doThree() { //코루틴안에서 delay 등을 사용할 때 사용되는 Suspend 함수.
delay(2000L)
Log.d(TAG, "two")
}
}
위와 같이 doTwo(), doThree()는 delay() 함수로 인해 잠시 동안 CPU를 사용하지 않는 잠드는 시간이 생긴다.
이를 suspend 함수로 관리해주면 더욱 유연한 코루틴 환경을 구성할 수 있다
우리는 코루틴이 능동적으로 자원을 공유하고 효율적으로 작동하는 것을 알았다.
하지만 능동적인 자원 공유 환경에서 개발자가 의도하는 "방향"에 맞게 작동해야 하는 경우에는 어떻게 해야 할까?
예를 들어보자.
Tesk1()과 Tesk2()가 있고 그 안에 delay()가 각 1초, 500ms초가 있다고 한다면?
위와 같은 경우 Tesk1은 delay() 1초로 인해 Tesk2가 먼저 실행될 것이다.
하지만, 개발자는 Tesk1을 반드시 끝내고 Tesk2로 넘어가고 싶은 상황도 생길 것이다.
이럴 때 사용하면 유용한 것이 바로 Job이다.
Job은 코루틴 자체를 의미하며, launch로 실행되는 코루틴을 관리 할 수 있다.
아래 예시를 확인해보자.
fun doThree() = runBlocking {
val job = launch {
delay(1000L)
Log.d(TAG, "3")
}
job.join()
launch {
Log.d(TAG, "1")
}
launch {
delay(500L)
Log.d(TAG, "2")
}
Log.d(TAG, "4")
}
위 코드의 실행 순서는 어떻게 될까?
3 -> 4 -> 1 -> 2
우리는 분명 앞에 부모 코루틴의 작업이 끝나면 자식 코루틴의 작업이 실행된다는 점을 알고 있었다.
하지만 위 코드를 실행했을 때 부모 코드인 "4"가 먼저 출력되지 않음을 확인할 수 있다.
위와 같이 Job 개념을 활용하면 join() 함수를 이용해 부모 코루틴에서 자식 코루틴을 먼저 실행하는 등
보다 능동적인 관리가 가능하다.
- Kotlin 에서는 fun x = runBlocking { TODO } 와 같이 중괄호를 생략하고 작성할 수 있다.
- runBlocking은 기본적인 반환형은 Unit이다.
- suspend function 안에서 launch를 사용하기 위해서는 coroutineScope{}을 작성해줘야한다.