[Android] 코루틴 들여다보기

Jay·2025년 5월 18일

코루틴

목록 보기
1/4
post-thumbnail

Coroutine이 안드로이드에 도입된 지 어언 몇 년 동안 안드로이드 개발자분들께서는 안 써보신 분들은 없으실 겁니다.
저도 Coroutine을 써보고 굉장히 좋다는 느낌을 받았습니다.
그런데 Coroutine을 그냥 사용하기만 했지 Coroutine이 어떻게 동작되고 왜 쓰는지 잘 몰랐기 때문에 한번 알아보도록 하겠습니다.

코루틴의 도입

Coroutine의 역사는 꽤 오래되었습니다.
멜빈 콘웨이 선생님께서 1958년에 Coroutine이라는 용어를 만들어 어셈블리 프로그램에 적용했습니다.
하지만 이렇게 오래된 Coroutine이 kotlin 언어가 개발됐을 때 바로 도입되지 않았는데요.
왜 바로 도입되지 않았을까?해서 Coroutine에 대한 내용을 많이 찾아보니 기준에 따른 여러 Coroutine의 내용들이 있었습니다.

하지만 두 가지 Coroutine에 대해 얘기해 보겠습니다.


1. Stackful Coroutine

이름에서 알 수 있듯이 Coroutine은 각각의 스택을 가집니다.

위 그림처럼 coroutine 호출로 수행될 작업들이 각각의 스택으로 만들어지며 스택 메모리에 쌓이게 됩니다.
또한 스택의 주소와 다른 스택의 주소를 가지며 *suspend/resume을 통해 context switching이 일어나 여러 작업을 동시에 할 수 있습니다.
stackful coroutine은 처럼 자신의 스택 내의 함수를 재귀적으로 호출도 가능하며, 처럼 다른 스택의 코루틴내 함수를 호출할 수 있습니다.

*suspend/resume과 context switching에 대한 내용은 다음 글에서 다시 얘기해보도록 하겠습니다.

이러한 방법으로 stackful Coroutine은 Coroutine 호출에 있어서 서로 간의 상호작용을 유연하게 만들지만, Coroutine이 각각의 스택을 가져야 하므로 메모리 사용량이 늘어날 뿐만 아니라 스택 관리가 복잡해지는 단점이 있습니다.


2. Stackless Coroutine

1번과 반대로 Stakless(스택이 더 적은) Coroutine입니다.
Stackless Coroutine은 스택이 하나만 생성되는 것은 아니지만, Coroutine이 스택을 공유하는 방법입니다.

그림 처럼 Stackless Coroutine은 힙 메모리를 동적 할당하고 스택을 생성합니다.
Stackless Coroutine 또한 suspend/resume을 통해 작업을 수행하며,
Coroutine 호출이 일어나면 처럼 스택의 내용을 힙 메모리로 옮겨 관리하게 됩니다.
호출에 응하는 Coroutine 함수는 처럼 스택에 들어와서 작업을 수행하게 됩니다.

이러한 방법은 메모리 사용량이 줄어들게 되고 관리가 간단하다는 장점이 있습니다.
단점은 아무래도 재귀 함수 호출 및 다른 Coroutine으로부터 호출의 제한 등이 있습니다.

Kotlin에서 사용하는 Coroutine은 Stackless Coroutine을 사용하고 있습니다.
아무래도 관리가 쉽고 사용 방법이 까다롭지 않아 메모리 사용량을 줄여 성능을 높이기 때문인 것 같습니다.
그럼 안드로이드에서 사용하는 Coroutine에 대해서 알아보겠습니다.


✅ Coroutine이란?

Coroutine은 co(함께) + routine(일련의 작업)으로 어떠한 여러 작업들을 동시에 하는 멀티태스킹이라고 생각하시면 됩니다.
Coroutine은 일반적인 routine(Main routine, Sub routine)과 차이가 있습니다.
Main routine은 프로그램이 실행될 때 가장 먼저 호출되는 부분입니다.
프로그램의 전체적인 흐름을 제어하고 필요에 따라 Sub routine을 호출하는 방식입니다.
즉, Main 함수라고 생각하시면 됩니다.
Sub routine은 특정 작업을 수행하는 코드의 집합으로 일반적인 함수 또는 메서드라고 부릅니다.

그림처럼 Sub routine은 routine의 진입점과 탈출점이 있습니다.
Sub routine을 호출하면 진입점으로부터 시작해 routine(작업)을 수행하고 탈출하게 되는 원리입니다.


반대로 Coroutine은 Sub routine과 차이가 있습니다.

Coroutine은 Subroutine과 반대로 진입점과 탈출점이 여러 개 존재합니다.
Coroutine에서 수행되는 작업들은 원하는 시점에 진입과 탈출할 수 있습니다.

Coroutine 특징을 통해 더 알아 보겠습니다.


1. coroutine은 비선점형

Coroutine은 비선점형 방식을 사용하고 있습니다.
운영체제의 용어로 선점형 스케줄링, 비선점형 스케줄링이 있습니다.

선점형 스케줄링
CPU를 점유한 프로세스가 실행 중에 긴급 실행을 요구하는 프로세스가 발생되는 경우 긴급 실행을 요구하는 프로세스에게 CPU를 양보하는 스케줄링 방법입니다.

비선점형 스케줄링
CPU를 점유한 프로세스가 긴급한 실행을 요구하는 프로세스에게 CPU를 양보하지 않도록 하는 스케줄링 방법입니다.

Coroutine은 비선점형이므로 Coroutine으로 호출한 프로그램이 실행되면 CPU를 점유하고 제어권을 양도하기 전까진 다른 Coroutine이 실행될 수 없습니다.

Coroutine 호출로 A 작업을 수행하여 끝날 때까지 CPU를 점유하도록 합니다.
하지만 수행 중에 갑자기 I/O 처리를 해야 하는 B 작업이 수행되어야 한다면 A 작업을 suspend(중단) 시키고 B 작업이 끝난 후 A 작업을 resume(재개) 하게 됩니다.
( suspend로 인해 A 작업의 CPU 점유가 중단 되었습니다. )

※ 추가 : Dispatchers의 Default 즉 CPU를 제어하는 경우에는 CPU를 양보하지 않고 선점하는 경우가 발생할 수 있습니다.



2. coroutine은 light-weight 스레드

안드로이드 공식 문서에 보면 Coroutine은 가벼운 중량의 스레드라고 말하고 있습니다.
그 이유는 kotlin에서 사용하는 Coroutine은 stackless Coroutine이라고 말한 바 있습니다.
프로그램이 실행될 때, 운영체제는 각 스레드에게 고유의 스택 메모리를 할당합니다.
그렇기 때문에 stackless는 적은 스레드로 즉 Coroutine이 light-weight 스레드라고 말할 수 있습니다.

정말 적은 양(가벼운)의 스레드로 Coroutine을 처리하는지 알아보기 위해 Thread랑 비교해 보겠습니다.
스레드 실행 버튼과 코루틴 실행 버튼을 눌려 Thread의 수를 알아보겠습니다.


💻 Thread 실행

private fun startThread() {
        val randomTimeMillis = (5000..10000).random().toLong()
        Thread {
            Thread.sleep(randomTimeMillis)
            // 하나의 스레드에서만 값이 증가 가능
            synchronized(lock) {
                threadCount += 1
                Log.d("TAG", "${threadCount}번 스레드(${randomTimeMillis}초)")
            }

        }.start()
    }


버튼을 클릭할 때마다 스레드를 계속 생성하고 있습니다.
하지만 이 방법은 메모리를 효율적으로 사용하지 못해 결국 성능이 떨어질 수밖에 없습니다.
실제로 스레드를 계속 생성하니 렉이 걸리는 현상이 발생하기도 했습니다.



💻 Coroutine 실행

private suspend fun startCoroutine()
    {
        val randomTimeMillis = (1000..5000).random().toLong()
        delay(randomTimeMillis)
        // 하나의 코루틴에서만 값이 증가 가능
        mutex.withLock {
            coroutineCount += 1
            Log.d("TAG", "${coroutineCount}번 코루틴(${randomTimeMillis}초)")
        }
    }

보이는 화면에서는 *스레드 풀의 스레드들로만 Coroutine을 처리하고 있습니다.
suspend가 일어나면 Stackless Coroutine에서 다뤘던 내용처럼 스레드 안의 스택의 내용은 heap 메모리로 이동하기 때문에 적은 양의 스레드로 Coroutine을 비동기 처리가 가능하게 되는 것입니다.

*스레드 풀 : 사전에 생성된 스레드의 집합으로, 작업을 수행하기 위해 재사용되는 스레드들입니다.



3. Coroutine은 동시성

1번 내용을 읽다 보면 하나의 작업이 끝나고 다른 작업을 수행한다고 말했습니다.
절차적인 느낌이 들겠지만 Coroutine의 routine이 여러 곳에서 진입/탈출이 가능해 동시성의 개념을 갖고 있습니다.
사실 작업의 속도가 빠르다 보니 동시에 처리한다고 생각하시면 될 것 같습니다.
그런데 동시성(Concurrency)병렬성(Parallelism)과 차이가 있습니다.
병렬성(Parallelism)은 동일한 시간에 독립적인 작업이 가능합니다.
요즘의 휴대폰들은 멀티 코어 프로세스를 이용하기에 여러 스레드를 병렬처리가 가능합니다.

그럼에도 불구하고 Coroutine을 사용하는 이유는 스레드를 병렬 처리했을 때 *context switching이 일어나며 이는 오버헤드가 발생해 성능을 떨어뜨릴 수 있기 때문입니다.

*context switching : 현재 처리 중인 작업(프로세스 및 스레드)에서 다른 작업으로 전환할 때 발생하는 과정

※ 추가 : 이 경우에도 Dispatchers의 Default 즉 CPU를 제어하는 경우에는 병렬성 작업이 가능할 수 있습니다.




🎞 회고

Coroutine의 종류, 동작 과정과 특징에 대해 알아보았습니다.
아마 Coroutine을 많이 사용하고 계시겠지만 Coroutine에 대해서는 자세하게 모르셨던 분들도 계셨을 거라고 생각합니다.
작성 된 글이 그런 분들에게 도움이 되는 내용이었으면 합니다.
제가 작성한 글은 틀린 내용이 있을 수 있습니다.
피드백은 언제나 환영입니다!

2개의 댓글

comment-user-thumbnail
2025년 5월 23일

완전 low-level까지 공부하시는 모습이 보기 좋습니다. 덕분에 많이 배웁니다.
근데 thread와 coroutine 스레드를 생성하는 차이를 어디부분을 보면 좋을까요? 잘 안 보이네요

1개의 답글