Coroutine - 2 기본 사용법
Coroutine 개념
코루틴이란? 코루틴은 Kotlin 을 사용해 안드로이드 앱을 개발할 때 아마 가장 처음 접하게 되는 키워드 일것이다. 보통 네트워크 통신, 혹은 내부 DB에서 데이터를 불러오거나 저장할 때 사용하게
forstudy.tistory.com
suspend fun
fun main() {
runBlocking {
launch {
delay(1000)
println("World")
}
println("Hello!")
}
}
지난 글에서 suspend 함수는 Coroutine 에서 사용할 수 있는 함수로, suspend 함수를 사용함과 동시에 쓰레드가 프로그램의 나머지 코드를 수행할 수 있도록 해주는 함수라고 설명했다. 위 launch 로 생성된 coroutine 이 실행하는 코드를 suspend 함수로 추출해보자
fun main() {
runBlocking {
launch {
printWorld()
}
println("Hello")
}
}
suspend fun printWorld() {
delay(1000)
println("World")
}
Kotlin 에서 suspend 함수는 suspend 라는 키워드를 붙여 생성하고, 해당 함수가 Coroutine 에서 사용되면 나머지 코드와 동시적으로 실행될 수 있다.
Scope Builder
코루틴 스코프는 여러 빌더들을 사용해서 생성할 수 있다. coroutineScope를 사용해서 직접 코루틴 스코프를 생성할 수도 있는데, 마찬가지로 블럭 안 코드가 모두 실행될 때 까지 종료되지 않는다.
runBlokcing{} 과 coroutineScope 빌더는 모두 코루틴 스코프를 생성하고, 그 안에서 우리는 새로운 코루틴을 생성해 동시적으로 작업을 수행할 수 있다. 두 가지 빌더의 차이점은 다음과 같다
runBlocking{} - 코루틴 스코프안의 코드가 모두 실행될 때까지 스레드를 잠시 멈춘다
coroutineScope{} - 스코프안의 코루틴과 다른 코드를 동시에 실행할 수 있다.
즉 runBlocking{} 은 해당 스코프안의 모든 코드가 실행될 때까지 스레드를 대기시킨다. 해당 스코프안의 코루틴들은 동시적으로 실행될 수 있지만, 프로그램의 나머지 부분은 스레드가 멈추기 때문에 실행되지 않는다.
더 쉬운 이해를 위해 다음 코드를 살펴보자
fun main() {
runBlocking {
launch {
printWorld()
}
}
println("Hello")
}
suspend fun printWorld() {
coroutineScope {
launch {
delay(1000)
println("World")
}
}
}
위 코드에서, println("Hello") 라인만 main 스코프 안으로 옮겼다. 위 코드는 이전 결과완 다르게 "World Hello" 라는 결과를 출력한다.
runBlocking은 자기 블럭 안의 코드가 모두 실행될 때까지 스레드를 대기시키기 때문이다.
이런 차이점 때문에 runBlocking 은 일반적인 함수고, coroutineScope 는 suspend 함수가 된다.
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
...
}
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
...
}
Scope Builder 와 동시성
coroutineScope 빌더는 모든 supend 함수에서 동시적으로 작업을 수행하기 위해 사용할 수 있다. 코루틴 스코프안에서 두 개의 코루틴을 생성해보자
fun main() {
runBlocking {
printWorld()
println("Done")
}
}
suspend fun printWorld() {
coroutineScope {
launch {
delay(2000)
println("World2")
}
launch {
delay(1000)
println("World1")
}
println("Hello")
}
}
위 코드의 결과는 다음과 같다
Hello
World1
World2
Done
printWorld 함수의 coroutineScope 로 인해 생성된 코루틴 스코프는, 코드 블럭이 끝날 때까지 종료되지 않는다. 따라서 2초 뒤에 World2 를 출력하는 코루틴이 종료되고 나서야 해당 스코프가 종료되고, 마지막 결과인 Done 을 출력하게 된다.
Job 객체
launch 코루틴 빌더는 Job 이라는 객체를 반환한다. 우리는 이 반환된 Job 객체를 통해 해당 코루틴을 취소할 수도 있고, 해당 코루틴이 완료될 때까지 명시적으로 스레드를 대기시킬 수 있다. 다음 코드를 보자
fun main() {
runBlocking {
val job = launch {
delay(1000)
println("World!")
}
println("Hello")
job.join()
println("Done")
}
}
위 코드의 실행 결과는 다음과 같다
Hello
World!
Done
Job 객체의 join() 이라는 함수를 통해 해당 코루틴이 종료될 때까지 대기할 수 있다. 따라서 Hello 가 먼저 출력되고, World! 가 출력되고 난 뒤에 Done 이 출력되는 것이다.
Coroutine 은 너무너무 가볍다
이제 대충 코루틴에 대해 감이 잡혔다. 코루틴은 스레드와 비슷하게 여러 작업을 동시에 수행하게 도와주는 역할을 한다. 물론 스레드는 병렬적으로 코드를 실행하고, 코루틴은 동시적으로 실행하기 때문에 약간의 차이는 있지만, 기존에 Thread를 활용해야 할 수 있는 작업을 코루틴으로도 쉽게 할 수 있게 되었다.
쓰레드를 생성해서 비동기적으로 작업을 처리하는 것은 어렵기도 하지만, 성능적으로도 아주 큰 이슈가 된다. Cpu 가 쓰레드를 바꾸는 것을 Context Switching 이라고 하는데, 이 작업에 아주 많은 비용이 들기 때문이다. 따라서 스레드가 아주 많아지면 스레드를 교체하는 비용이 어마어마하게 커지게 된다.
하지만 코루틴은 쓰레드를 변경하지 않고, 쓰레드에 비해 엄청나게 가볍다. 아래의 코드는 코루틴 10만개를 생성해, 5초 뒤에 .을 찍게 하는 코드이다.
fun main() {
runBlocking {
repeat(100_000) {
launch {
delay(5000)
println(".")
}
}
}
}
위 코드는 아주 정상적으로 결과를 출력한다. 그러나 코루틴을 쓰레드로 바꿔서 다시 한 번 실행해보자.
fun main() {
repeat(100_000) {
thread {
Thread.sleep(5000)
println(".")
}
}
}
위 코드를 실행하면, 아마 IDE 가 메모리가 부족하다는 에러 메시지를 띄우고 비정상적으로 종료될 것이다.
우리는 위 결과에서 코루틴이 쓰레드에 비해 아주 가볍다는 것을 알 수 있다.