티스토리 뷰
코루틴이 뭔가요?
코루틴은 잠시 중단 가능한 연산의 인스턴스라고 볼 수 있다. 개념적으로 스레드와 비슷한데, 예를 들어 하나의 스레드는 고양이 밥을 주고, 다른 하나의 스레드는 고양이 화장실을 치울 수 있다. 이 작업들은 병렬적으로도 가능하고, 동시적으로도 가능하다.
참고: 병렬은 각 작업이 다른 주체에서 실행되는 것이고, 동시는 주체에 상관없이 다른 작업과 함께 수행되는 것을 의미한다.
코루틴에서는 이러한 작업 단위를 코드 블록으로 분리하여 동시적 실행이 가능하도록 한다.
코루틴의 간단한 동작과정
fun main () = runBlocking{
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
이 코드를 처음 보면, World Hello가 출력될거같지만 실은 Hello World가 출력된다.
먼저 하나씩 한국어 뜻으로 이해해보자.
runBlocking은 현재 실행하는 스레드는 Blocking 된 상태에서 내부를 실행한다! 고 이해하자.
그럼이제, runBlocking 내부에 코드로 접근해본다.
launch는 실행이라는 뜻으로 코루틴 빌더로서 실행할 코루틴을 생성한다.
(코루틴 = 중단 가능한 연산의 인스턴스를 생성한다고 생각하자.)
delay는 중단가능한 함수로 현재 코루틴을 1초(1000ns = 1s) 지연한다.
println("World")를 출력하고 싶으나 delay에 의해 1초 지연된다.
runBlocking안에, 그리고 launch 아래에 있는 println("Hello")는
이전 launch에 의한 코루틴이 지연되는 사이에 실행된다.
왜 println이 실행되는가 궁금하다면?
runBlocking으로 이미 메인 코루틴으로 시작한 것이기 때문에
서브 코루틴은 현재 메인과 엄연히 다른 코루틴(연관은 있으나 ,다른 작업)이기 때문이다.
구조화된 동시성과 runBlocking
다시! 그럼 runBlocking에 대해 살펴보자
runBlocking을 굳이 메인 코루틴을 시작해서 launch로 다른 서브 코루틴을 만든 이유는 뭘까?
이는 코루틴이 구조화된 동시성의 원칙을 따르기 때문이다.
즉, 명확한 부모-자식 관계를 가짐으로써
고아 코루틴 발생 방지, 예외 전파, 안전한 처리를 위함이라 생각하면 된다.
멀티스레드를 사용하던 기존 방식과 유사하지만,
코루틴은 더 가볍고 더 용이하게 각자의 작업을 추적하고 관리하기 위해서 해당 구조를 따른다.
오호.. 다시 코드를 보니,
코루틴의 시작과 자식 코루틴 그리고 그 과정을 쉽게 인식할 수 있긴해 보인다.
fun main () = runBlocking{
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
코루틴과 함수형프로그래밍
그런데 여기서 더 재밌는 점이 있으니, 코루틴은 함수로 표현가능하다는 것이다.
참고로 코틀린은 OOP뿐만 아니라, FP패러다임도 적극 지원해주는 모던 멀티패러다임 언어이기에,
아래와 같이 함수형으로 코루틴을 추출하는게 가능하다.
(이 얼마나 읽기 쉽고, 동시성을 갖추며, 짧고 아름다운가)
fun main() = runBlocking {
launch { doWorld() }
println("Hello")
}
suspend fun doWorld() {
delay(1000L)
println("World!")
}
다른 코루틴 빌더는 없나요?
runBlocking vs coroutineScope
runBlocking은 현재 사용하던 스레드를 기준으로 부모 코루틴으로 삼아 블록에 속한 작업들을 수행한다.
즉 runBlocking 이후 아래 코드 블럭은 자식 코루틴이 끝나야 동작이 가능하다는 것이다.
그렇다.. 똑똑한 코루틴이 이것만 지원해주겠는가?
coroutineScope 빌더를 이용하여 또한 코루틴 범위를 지정할 수 있다.
runBlocking과 coroutineScope은 모든 자식이 완료될 때까지 기다리지만,
runBlocking은 현재 스레드를 Blocking 하는반면,
coroutineScope은 현재 스레드는 다음 코드블록을 수행하고
코루틴안에서 정지 함수를 만나면, NonBlocking하게 동시성을 확보한다.
(이러한 점에서 runBlocking은 일반 함수, coroutineScope는 정지가능한 함수라고 한다)
비교하자면 이러하다.
runBlocking
- 현재 사용 중인 스레드를 Blocking한다.
- 즉, 해당 코루틴이 종료될 때까지 다음 코드 블록이 실행되지 않는다.
coroutineScope
- 현재 스레드를 Blocking하지 않는다.
- 내부에서 정지 함수를 만나면 NonBlocking하게 동시성 확보가 가능하다.
- 일반 함수가 아니라 일시 중단 가능한 함수이다.
fun main() {
runBlocking {
doWorld()
}
}
fun logging(msg: String) {
println("${Thread.currentThread().name} - ${LocalDateTime.now()} : $msg")
}
suspend fun doWorld() {
coroutineScope {
launch {
delay(1000L)
logging("World!")
}
logging("Hello")
}
}
이렇게 하면 Hello가 먼저 출력되고 1초 후 World!가 출력된다.
coroutineScope는 동시성을 갖나요?
직접 코드를 실행시켜서 알아보자,
아래코드를 실행하면 Hello World 1 World 2 Done 이 순서대로 출력된다.
fun main() = runBlocking {
doWorld()
println("Done")
}
suspend fun doWorld() = coroutineScope {
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
runBlocking에 의해 코루틴이 시작되고,
coroutineScope 안에 자녀 코루틴이 시작되었다.
여기서 또 launch에 의해 2초를 기다렸다가 world 2 가 출력되는 코루틴이 실행되고,
launch에 의해 1초를 기다렸다가 world 1 가 출력되는 코루틴이 실행되고,
위 launch 코루틴의 실행시간과 관계없이 Hello가 출력된다.
즉, 약 2초를 기다려서 모든 결과를 얻는다.
그런데 순차적으로 동작했다면 총 3000ns = > 3초를 기다려야했을텐데?
동시에 동작하니 2초안에 끝났다.
동시성을 가진다는게 입증이 되었다!
(해당 코드가 동시성을 어떻게 가졌는가에 대해 궁금하다면,
launch { ... }블록 내부의 두 코드 조각은 동시에 실행되었다는 것만을 이해하면 된다. )
launch로 시작한 코루틴에 대해 순서가 필요할 때는 어떡하죠?
어렵지 않다.
코틀린은 FP 함수형 프로그래밍을 지원해주지 않는가?!
launch로 동작하는 suspend 함수를 변수로 할당하여, 멀티스레딩 thread함수에서 사용했던
join함수와 같이 기다릴 수 있다.
val job = launch {
delay(1000L)
println("World!")
}
println("Hello")
job.join()
println("Done")
launch로 시작한 코루틴이 main 스레드로 동시 동작하던데
어떻게 하나의 스레드로 동시성을 가졌던거죠?
코루틴은 기존에 우리가 알고있는 JVM 스레드보다 훨씬 가볍다.
기본적으로 JVM에서 지원해주는 스레드는 커널레벨 스레드와 연결되는 스레드로 컴퓨터 자원과 굉장히 많이 묶여있다. 그렇기 때문에 컨택스트 스위칭이 무겁고 관리가 어려운데, 코틀린의 코루틴에서 사용하는 스레드는 더 가볍고, JVM 의 메모리를 고갈시키지 않도록 Lazy 하게 동작하며 동시성을 가진다.
그러나! 가볍다고 한들 항상 쓸만큼 정답은 아니다!
참고로 Java의 ParallelStream과 비슷하게
동시성!병렬성!멀티스레딩!이 항상 옳은 진리는 아니다라는 것을 말하고싶다.
fun main() {
val time = measureTimeMillis {
runBlocking {
repeat(50_000) {
launch {
print(".")
}
}
}
}
println("\nExecution Time: $time ms")
}
Execution Time: 577 ms
fun main() {
val time = measureTimeMillis {
runBlocking {
for( i in 1..50_000) {
print(".")
}
}
}
println("\nExecution Time: $time ms")
}
Execution Time: 288 ms
위 코드와 같이 단순 작업에서는 오히려 불필요한 코루틴 생성 비용 + 컨텍스트 스위칭 비용에 의해 무거워지기도한다.(사실 ParallelStream 부작용과 비교하면 큰 차이는 안나는거 같기도 하다. 더 자세한 내용은 다른 챕터에서 더 깊이 알아보자)
마무리
코루틴을 정리하자면:
- 가볍고 효율적인 동시성 처리 기법이다.
- runBlocking, launch, coroutineScope 등 다양한 코루틴 빌더를 활용할 수 있다.
- 함수형 프로그래밍 패러다임과 잘 어울린다.
- NonBlocking 방식으로 동시성을 처리할 수 있다.
이제 코루틴을 활용하여 더 가독성 높고 성능 좋은 비동기 코드를 작성할 수 있을 것이다!
참고) https://kotlinlang.org/docs/coroutines-basics.html
Coroutines basics | Kotlin
kotlinlang.org
'Kotlin' 카테고리의 다른 글
코틀린 코루틴? 원문을 보고 말지 (preview) (1) | 2025.03.10 |
---|