목차
- Intro
- 스레드 기반 작업의 한계
- JVM 프로세스와 스레드
- 단일 스레드의 한계와 멀티 스레드 프로그래밍
- 멀티 스레드 프로그래밍: 단일 스레드의 한계 극복
- 스레드를 사용한 멀티 스레드 프로그래밍
- Thread 클래스를 사용하는 방법과 한계
- 기존 멀티 스레드 프로그래밍의 한계와 코루틴
- 기존 멀티 스레드 프로그래밍의 한계
- 코루틴은 스레드 블로킹의 문제를 어떻게 극복하는가?
- Outro
오늘은 코틀린 코루틴을 사용해야 하는 이유에 대해 알아보겠다. 전반적인 내용은 『코틀린 코루틴의 정석』이라는 책을 읽으며 이해한 내용을 바탕으로 작성하겠다. 이번 포스트는 정보를 전달하기보다는, 필자가 이해한 바를 정리해 기록하기 위한 목적이 크다.
code1
fun main() {
println("Hello, Kotlin Coroutines!")
}
코틀린 애플리케이션은 main 함수를 통해 실행된다. 즉, 프로그램의 진입점은 main 함수이다.
애플리케이션을 실행하면 다음과 같은 순서로 동작이 진행된다.

code1을 실행한 후 종료되면 Process finished라는 문구가 출력된다. 즉, 메인 스레드는 일반적으로 프로세스의 시작과 끝을 함께한다.
JVM의 프로세스는 기본적으로 메인 스레드를 단일 스레드로 해서 실행되며 메인 스레드가 종료되면 종료되는 특징을 가진다.
단일 스레드 애플리케이션은 스레드 하나만 사용해 실행되는 애플리케이션이며, 치명적인 문제가 있다.
단일 스레드 애플리케이션의 한계
스레드는 하나의 작업을 수행할 때 다른 작업을 동시에 수행하지 못한다. 글만으로는 이해가 어려우니 바로 예시를 들어보겠다.
배달의민족에서 치킨을 주문한다고 가정해보자. 앱을 실행하면 홈 화면이 나타나고, 카테고리 중 ‘치킨’을 선택하면 가게 목록을 표시된다.
이처럼 화면을 보여주고 사용자가 화면을 눌러 앱과 상호작용 하는 것을 안드로이드 애플리케이션에서는 기본적으로 메인 스레드에서 진행한다.

만약 메인 스레드에서 치킨 가게 목록을 불러오면 어떻게 될까? 아래와 같은 창이 뜨면서 앱이 비정상 종료될 것이다.

치킨 가게 목록을 불러오는 네트워크 요청과 응답 대기 과정은 시간이 오래 걸리는 작업이다. 이런 작업을 메인 스레드에서 수행하면 UI를 그리는 작업이 멈추고, 사용자 입력도 제대로 처리되지 않는다. 안드로이드 애플리케이션은 입력 이벤트에 5초 이내로 응답하지 않으면 ANR(Application Not Responding)이 발생하기 때문에, 결국 앱이 비정상적으로 종료된다.

멀티 스레드 프로그래밍이란 스레드를 여러 개 사용해 작업을 처리하는 프로그래밍 기법이다. 각각의 스레드가 한 번에 하나의 작업을 처리할 수 있으므로 여러 작업을 동시에 처리하는 것이 가능해진다.
그렇다면 앞서 살펴본 치킨 주문 과정에서 발생한 문제를 멀티 스레드 프로그래밍으로 어떻게 해결할 수 있을까?
메인 스레드 대신 별도의 스레드가 치킨 가게 목록을 불러오는 작업을 수행하도록 하면 된다.
오래 걸리는 작업을 백그라운드 스레드에서 처리하도록 만들면 메인 스레드에서 UI가 멈추거나 사용자 입력을 처리하지 못하는 현상을 방지할 수 있다.

이런 방식으로 여러 스레드가 동시에 작업을 처리하면 단일 스레드만 사용하는 것에 비해 처리 속도가 빨라지는데 이를 병렬 처리라고 한다.
코루틴은 기존의 멀티 스레드 프로그래밍의 문제점을 해결하기 위해 만들어졌다. 따라서 멀티 스레드 프로그래밍의 어떻게 발전해 왔는지를 이해해야, 왜 코루틴을 사용해야 하는지 명확하게 설명할 수 있다.
코루틴이 등장하기 이전에 만들어진 스레드를 활용한 멀티 스레드 프로그래밍 방식에 대해 알아보자.
Thread 클래스를 사용해 스레드 다루기
자바에서는 새로운 스레드에서 실행할 코드를 작성하기 위해 Thread 클래스의 run 함수를 재정의(override)해야 한다. 반면, 코틀린은 thread 함수를 제공해 훨씬 쉽게 작성할 수 있다.
Java
class NewThread : Thread() {
override fun run() {
println("[${Thread.currentThread().name}] 새로운 스레드 시작")
// 오래 걸리는 작업이 별도 스레드에서 실행되는 동작을 테스트하기 위해 2초 동안 대기
Thread.sleep(2_000L)
println("[${Thread.currentThread().name}] 새로운 스레드 종료")
}
}
fun main() {
println("[${Thread.currentThread().name}] 메인 스레드 시작")
NewThread().start()
Thread.sleep(1000L) // 1초동안 대기
println("[${Thread.currentThread().name}] 메인 스레드 종료")
}
Kotlin
fun main() {
println("[${Thread.currentThread().name}] 메인 스레드 시작")
// 새로운 스레드 생성
thread {
println("[${Thread.currentThread().name}] 새로운 스레드 시작")
Thread.sleep(2_000L)
println("[${Thread.currentThread().name}] 새로운 스레드 종료")
}
Thread.sleep(1_000L) // 1초 동안 대기
println("[${Thread.currentThread().name}] 메인 스레드 종료")
}
아래 이미지는 코드의 실행 결과이다. 코드의 실행 결과를 보면 메인 스레드에서 출력한 로그와 새로운 스레드에서 출력한 로그가 섞여 있는 것을 확인할 수 있다.

동작 흐름은 아래와 같다.
NewThread().start()에 의해 Thread-0이라는 새로운 스레드 생성이러한 방식으로 시간이 오래 걸리는 작업을 새로운 스레드에 실행할 수 있고, 작업은 병렬로 실행된다.
Thread 클래스를 사용해 스레드 다루기
Thread 클래스를 사용해 스레드 다루면 두 가지 문제가 발생한다.
잦은 스레드 생성
Thread 클래스를 인스턴스화해 실행할 때마다 매번 새로운 스레드가 생성된다. 스레드는 생성 비용이 비싸기 때문에 매번 새로운 스레드를 생성하는 것은 성능적으로 좋지 않다.
생성과 관리에 대한 책임이 개발자에게 있음
스레드 생성과 관리에 대한 책임이 개발자에게 있다. 따라서 프로그램의 복잡성이 증가하며, 실수로 인해 오류나 메모리 누수를 발생시킬 가능성이 증가한다.
멀티 스레드 프로그래밍은 계속해서 단점을 보완하며 발전해 왔다. 하지만 기존의 멀티 스레드 프로그래밍은 스레드 기반으로 작업한다는 한계를 갖고 있다.
단일 스레드 프로그래밍의 한계 극복을 위해 멀티 스레드를 사용한다고 했는데 이게 무슨 말인가? 멀티 스레드 프로그래밍에서 스레드 기반으로 작업할 때 생기는 문제에 대해 알아보자.
스레드는 생성 비용과 작업을 전환하는 비용이 비싸다. 만약 스레드가 아무 작업을 하지 못하고 기다려야 한다면 컴퓨터의 자원이 낭비된다.
배달의민족에서 치킨을 한마리 더 주문해보자.
0번 스레드가 ‘치킨’ 카테고리를 이동해 치킨 가게 목록을 표시하려 할 때, 1번 스레드에서 해당 목록을 불러오고 있다면 결과값을 받을 때까지 대기해야 한다.

이 상황에서 Thread0 스레드는 Thread1으로부터 치킨 가게 목록을 받아오기 전까지 아무것도 하지 못한다. 이렇게 하나의 스레드에서 수행하는 작업이 완료될 때까지 다른 스레드가 사용할 수 없게 되는 것을 스레드 블로킹이라고 한다. 스레드 블로킹은 스레드라는 비싼 자원을 사용할 수 없게 만들어 성능에 치명적인 영향을 준다.
코루틴은 작업 단위 코루틴을 통해 스레드 블로킹 문제를 해결한다. 작업 단위 코루틴이란 스레드에서 실행 중인 작업을 일시 중단할 수 있는 단위이다.
코루틴은 작업이 일시 중단되면 스레드의 사용 권한을 양보하고, 스레드는 그동안 다른 작업을 실행할 수 있다. 일시 중단된 코루틴은 재개 시점에 다시 스레드에 할당되어 실행된다. 이러한 구조 때문에 코루틴은 ‘경량 스레드’라고 불린다.
프로그래머가 코루틴을 생성해 코루틴 스케줄러에 넘기면, 코루틴 스케줄러는 사용 가능한 스레드나 스레드 풀에 코루틴을 분배해 작업을 수행한다.
그림을 통해 이해해보자. 기존의 멀티 스레드 프로그래밍을 통해 Thread0에서 광고를 보여주기 위해서는 치킨 가게 목록을 불러온 후 수행해야 한다. 치킨 가게 목록을 보여줄 때까지 Thread0은 ‘치킨 카테고리 보여주기’ 작업이 점유하고 있기 때문이다.

이러한 문제는 코루틴으로 해결할 수 있다. 코루틴은 자신이 스레드를 사용하지 않을 때 스레드 사용 권한을 반납한다. 스레드 사용 권한을 반납하면 해당 스레드에서는 다른 코루틴이 실행될 수 있다. 치킨 카테고리를 보여주는 도중 치킨 가게 목록이 필요해지면 Thread1이 결과를 반환할 때까지 Thread0의 사용 권한을 반납하고 일시 중지한다. 따라서 Thread0을 사용할 수 있어 광고를 보여주는 작업을 실행할 수 있다. 코루틴은 이런 방식으로 스레드를 효율적으로 사용한다.


요약