안녕하세요. 오늘은 코틀린 코루틴의 정석 - CH1을 읽고 정리한 글입니다.

📌 1장에서 다루는 내용
1. JVM 프로세스와 스레드에 대한 이해
2. 단일 스레드를 사용하는 것의 한계와 멀티 스레드 프로그래밍
3. 기존 멀티 스레드 프로그래밍의 한계와 코루틴이 이를 극복한 방법
1.1 JVM 프로세스와 스레드
코틀린 앱의 실행 진입점은 main 함수이죠.
'앱 실행 → JVM 프로세스 시작 → 메인 스레드 생성 → main 함수 내부 코드 수행 → 종료'의 플로우를 가져요.
메인스레드는 프로세스의 시작과 끝을 함께 합니다.
따라서 메인스레드가 강제 종료되면 프로세스도 강제 종료되겠죠.
⇒ JVM의 프로세스는 기본적으로 메인 스레드를 단일 스레드로 해서 실행되며, 메인스레드 종료 시 함께 종료됩니다.
* 메인 스레드가 항상 프로세스의 끝을 함께 하는 건 아닙니다.
JVM의 프로세스는 사용자 스레드가 모두 종료될 때 종료됩니다. (메인 스레드는 사용자 스레드 중 하나일뿐)
⇒ 따라서 멀티 스레드면 메인 스레드가 강제 종료돼도 항상 프로세스까지 같이 강제 종료되지는 않겠죠.
1.2 단일 스레드의 한계와 멀티 스레드 프로그래밍
단일 스레드 어플리케이션이란 스레드 하나만 사용해 실행되는 앱을 말합니다.
여기서 스레드 하나만 사용한다라는 뜻은 위에서 말했듯이 main 함수를 통해 실행되고 메인스레드를 단일 스레드로 사용하는 구조를 뜻하죠.
1.2.1 단일 스레드 애플리케이션의 한계
스레드는 하나의 작업을 수행할 때 다른 작업을 동시에 수행할 수 없어요.
따라서 안드로이드 단일 스레드 앱에서는
메인 스레드가 네트워크 요청을 하고 응답 대기를 하거나 복잡한 연산 작업을 수행할 때
→ UI 그리는 작업이 멈추고
→ 사용자 입력도 제대로 전달 받지 못하게 됩니다.
⇒ 이로인해 앱이 멈추거나 버벅이게 되죠.
단일 스레드는 작업 속도가 느리고, 해야할 작업이 다른 작업에 의해 방해받을 수 있다는 단점이 있습니다.
1.2.2 멀티 스레드 프로그래밍을 통한 단일 스레드의 한계 극복
위와 같은 단일 스레드의 문제를 멀티 스레드로 해결 가능합니다.
멀티 스레드란 스레드를 여러 개 사용하여 작업을 처리하는 프로그래밍 기법입니다.
→ 동시에 여러 작업을 처리 가능하다는 거죠.
따라서 안드로이드 멀티 스레드 앱에서는
오래 걸리는 작업은 백그라운드 스레드(메인 스레드가 아닌 별도 스레드)에서 처리하도록 합니다.
→ 메인 스레드는 오래 걸리는 작업을 하지 않아 UI가 멈추거나 사용자 입력을 못받는 현상이 방지되는 겁니다.
또 오래 걸리는 작업을 독립적인 작은 작업으로 나눈 후 각 스레드로 배분하게 되면 응답속도가 더 좋아지겠죠?
이런 방식으로 여러 스레드가 동시에 작업을 처리하면 단일 스레드만 사용하는 것에 비해 처리 속도가 빨라집니다.
⇒ 이걸 병렬 처리 Parallel Processing이라고 해요.
* 모든 작업을 작은 단위로 나눠서 병렬로 실행 가능한 것은 아닙니다.
그 작은 작업 간에 독립성이 있을 때만 가능해요.
→ 의존성이 있으면 순차 실행이 필요합니다.
1.3 스레드, 스레드풀을 사용한 멀티 스레드 프로그래밍
코루틴이 등장하기 이전에 만들어진 스레드, 스레드풀을 활용한 멀티 스레드 프로그래밍 방식에 대해 알아봅시다.
1.3.1 Thread 클래스를 사용하는 방법과 한계
Thread 클래스를 사용해 스레드 다루기
오래 걸리는 작업이 별도 스레드에서 실행되도록 Thread 클래스를 상속하는 클래스를 만듭니다.
class ExampleThread: Thread(){
override fun run(){
println("[${Thread.currentThread().name] 새로운 스레드 시작")
Thread.sleep(2000L) // 2초 대기
println("[${Thread.currentThread().name] 새로운 스레드 종료")
}
}
fun main(){
println("[${Thread.currentThread().name] 메인 스레드 시작")
ExampleThread().start()
Thread.sleep(1000L) // 1초 대기
println("[${Thread.currentThread().name] 메인 스레드 종료")
}
// 결과
[main] 메인 스레드 시작
[Thread-0] 새로운 스레드 시작
[main] 메인 스레드 종료
[Thread-0] 새로운 스레드 종료

- “[main] 메인 스레드 시작”이 출력
- 새로운 스레드가 생성되고, “[Thread-0] 새로운 스레드 시작”이 출력
- 1초 뒤 “[main] 메인 스레드 종료”가 출력
- 다시 1초 뒤 “[Thread-0] 새로운 스레드 종료”가 출력
⇒ 시간이 오래 걸리는 작업을 새로운 스레드에서 실행하면 작업이 병렬로 실행되는 것을 알 수 있습니다.
* JVM은 스레드를 사용자 스레드와 데몬 스레드로 구분합니다.
- 사용자 스레드: 우선도가 높은 스레드 → 모두 종료될 때 JVM 프로세스 종료
- 데몬 스레드: 우선도가 낮은 스레드
Thread 클래스를 상속한 클래스로 스레드 만들면 기본적으로 사용자 스레드로 생성됩니다.
데몬 스레드로 바꾸고 싶다면 isDaemon 속성을 설정해줘야 해요.

사용자 스레드인 메인 스레드가 종료될 때 프로세스 종료 + 새로운 스레드(데몬 스레드 - 중요도가 낮기 때문에 강제 종료되어도 프로세스는 정상 종료) 강제 종료됩니다.
thread 함수 사용해 새로운 스레드에서 코드 실행하기 코틀린에서 제공하는 새로운 스레드에서 실행할 코드 작성하는 함수인 thread 함수를 사용하면, Thread 클래스 상속받아서 새로운 클래스를 만들 필요가 없습니다.

Thread 클래스를 직접 다루는 방법의 한계
- Thread 클래스를 상속한 클래스를 인스턴스화해 실행할 때마다 매번 새로운 스레드 생성
→ 스레드 생성 비용 비쌈 ⇒ 성능적으로 안좋음 - 스레드 생성과 관리에 대한 책임이 개발자에게 있음
→ 프로그램 복잡성 증가
→ 실수로 인한 오류, 메모리 누수 가능성 있음
⇒ 위의 문제 해결을 위해서는
한번 생성한 스레드를 간편하게 재사용 가능해야하고,
스레드의 관리를 미리 구축한 시스템에서 책임질 수 있어야 합니다.
1.3.2 Executor 프레임웍을 통해 스레드풀 사용하기
Executor 프레임웍은 위에서 말했던 Thread 클래스 문제를 해결합니다.
스레드를 생성하고 관리하는데 스레드풀이라는 개념을 사용하는데,
단순히 스레드의 집합, 모음입니다.
Executor 프레임웍은 스레드풀을 관리하고 사용자로부터 요청받은 작업을 각 스레드에 할당하는 시스템입니다.
- 작업 처리를 위한 스레드풀 생성
- 작업 요청 받으면 쉬고있는 스레드에 작업 분배
- 각 스레드가 작업을 끝내도 스레드를 종료하지 않고 다음 작업 들어오면 재사용
→ 이거 다 Executor 프레임웍이 해줍니다.
그럼 개발자는 뭐하냐?
⇒ 스레드풀에 속한 스레드 개수를 설정하고, 작업 제출만 하면 됩니다.
Executor 프레임웍에는 크게 두 가지 함수가 있습니다.
newFixedThreadPool - 스레드풀 생성, 관리 객체 반환 함수
submit - 스레드풀 관리 객체에 작업을 제출하는 함수


⇒ submit 함수를 통해 ExecutorService(스레드풀 관리하는 객체)에 작업 1, 작업 2 제출
→ Thread-1, Thread-2에서 각각 작업 수행
→ ExecutorService 종료 위해 shutdown 함수 호출
만약 위의 상황에서 작업 3이 추가된다면?
스레드풀에 스레드가 2개이므로 작업 1이 끝나야 3이 시작됩니다.
ExecutorService 내부 구조와 동작

위와 같이 ExecutorService 객체는 크게 두 부분으로 나뉩니다.
- 작업 대기열 - 할당 받은 작업을 적재하는 곳
- 스레드풀 - 작업을 수행하는 스레드의 집합
⇒ '요청 받음(작업 제출 받음) → 작업 대기열 적재 → 스레드 할당'의 플로우를 가지죠.

위의 상황(작업1, 2, 3)을 예시로 자세한 플로우를 보자면
'ExecutorService에 작업1 제출 → 작업1을 스레드에 할당
→ ExecutorService에 작업2 제출 → 작업2를 스레드에 할당(모든 스레드에서 작업이 실행 중인 상황)
→ ExecutorService에 작업3 제출(작업 대기열에 적재) → 작업1 실행 완료 → 작업3을 스레드에 할당'
입니다.
근데 사실 개발자는 ExecutorService 객체 내부 동작을 전혀 신경쓰지 않아도 됩니다.
작업 적재나 스레드 분배 같은 일들은 위에서 봤듯이 ExecutorService 객체가 하니까요.
개발자는 그냥 스레드풀 구성할 스레드 개수 지정 및 작업 제출만 하면 되는거죠.
⇒ Executor 프레임웍은 개발자가 더 이상 스레드 관리를 하지 않아도 되고, 스레드 재사용도 가능하다는 점에서 혁신적
하지만 이것도 한계가 있습니다.
바로 스레드 블로킹 Thread Blocking
Executor 프레임웍에서
'작업 제출 → 결과 전달' 의 플로우에서
언제 올지 모르는 값(결과)를 기다리는데 Future 객체를 사용해요.
이 Future 객체는 미래에 언제 올지 모르는 값을 기다리는 함수 get을 갖고 있고,
get 함수를 호출하면 그 스레드가 값 반환까지 블로킹됩니다.

메인 스레드가 future.get()을 호출하고 있으므로 결과값이 반환될 때까지 메인 스레드가 블로킹됩니다.
여기서는 2초만 블로킹하지만 실제 앱에서는 이보다 더 오랜시간동안 블로킹되겠죠?
그럼 심각한 성능적 문제가 발생합니다.
* 스레드 블로킹은 스레드를 사용하는 어디서든 발생 가능한 문제입니다.
Thread.sleep - 스레드 블로킹을 일으키는 대표적 함수죠.
→ 일정시간 대기할 때 사용 ⇒ 스레드가 대기하도록 명령받은 시간동안 해당 스레드를 블로킹해요.
1.3.3 이후의 멀티 스레드 프로그래밍과 한계
멀티 스레드 프로그래밍은 계속해서 발전해왔습니다.
Executor 프레임웍의 Future 객체 단점을 보완하기 위해
CompletableFuture 객체(작업 체이닝 기능을 제공하여 스레드 블로킹을 줄임) 등장 / 리액티브 프로그래밍 RxJava(결과값-데이터 스트림으로 처리 → 스레드 블로킹 방지, 손쉬운 스레드풀 전환) 등장 등
하지만 스레드 기반 작업의 한계를 해결하지는 못했어요.
스레드 기반 작업의 한계란?
스레드는 생성 비용과 작업 전환 비용이 비쌉니다.
따라서 스레드가 아무 작업 안하고 기다린다면, 컴퓨터 자원 낭비가 심하겠죠.
⇒ 스레드 블로킹이 스레드 기반 작업의 한계입니다.
1.4 기존 멀티 스레드 프로그래밍의 한계와 코루틴
1.4.1 기존 멀티 스레드 프로그래밍의 한계

이미지와 같이 작업1, 2가 있는데 작업 1에 작업2의 결과가 필요하다면, 작업 2가 수행될때까지 스레드 블로킹이 발생합니다.
자원 낭비로 인해 성능 문제가 심각하게 발생해요.
⇒ 멀티 스레드 프로그래밍에서 스레드 블로킹은 피할 수 없는 문제입니다.
간단한 작업에서는 콜백이나 체이닝함수(한 함수의 결과를 바로 다른 함수로 연결해 호출하는데 사용 - CompleteableFuture)로 스레드 블로킹을 피하기도 합니다.
그치만 간단한 작업만 할 수 없으니까요 ..
1.4.2 코루틴은 스레드 블로킹 문제를 어떻게 극복하는가?
코루틴은 작업 단위 코루틴으로 스레드 블로킹을 해결합니다.
작업 단위 코루틴이란?
스레드에서 작업 실행 도중 일시중단할 수 있는 작업 단위를 뜻해요.
코루틴은 작업이 일시중단되면 더 이상 스레드 사용이 필요없어집니다.
→ 그러면 스레드 사용 권한을 양보하고,(양보된 스레드는 다른 작업에 사용 가능 상태가 됨)
→ 일시중단된 코루틴은 재개시점에 다시 스레드에 할당, 실행됩니다.
코루틴은 경량 스레드라고 불립니다.
사용자가 코루틴을 만들어 코루틴 스케줄러에 넘기면, 스케줄러가 자신이 사용할 수 있는 스레드나 스레드풀에 해당 코루틴을 분배해 작업을 수행해요.
→ 코루틴이 스레드 사용하던 중 필요가 없어지면 해당 스레드를 다른 코루틴에 양보합니다.
⇒ 스레드 블로킹이 발생하지 않습니다. 마치 스레드에 코루틴을 붙였다 뗐다 하는 것처럼 사용하니까요.
같은 상황에서도


⇒ 딱 봐도 코루틴이 더 스레드를 효율적으로 사용하죠?
코루틴은 작업 단위로서의 코루틴이 스레드를 사용하지 않을 때 스레드 사용 권한을 양보하는 방식입니다.
따라서 스레드 사용이 최적화되고, 스레드 블로킹이 방지되는거죠.
자연스럽게 스레드에 비해 생성과 전환 비용이 적게 들고 작업을 생성하고 전환하는데 필요한 리소스와 시간이 매우 줄어듭니다.
⇒ 이게 경량 스레드라고 불리는 이유입니다. 다 가벼워져요.
코루틴은 경량 스레드라는 장점도 있지만 또 다른 장점도 있어요.
바로 구조화된 동시성으로 비동기 작업을 안전하게 만들고, 예외처리를 효과적으로 처리할 수 있으며, 실행중인 스레드를 손쉽게 전환할 수 있다는 점 등 더 있습니다.
위 내용들은 이후의 장들에서 더 다룬다고 합니다. 그때 더 알아볼게요.
이렇게 오늘은 스레드 기반 작업의 한계와 코루틴의 등장에 대해서 정리해보았습니다.
코루틴에 대해서 아무런 지식없이 대충 비동기로 좋다고 하니까 무분별 사용하다가 이렇게 공부해보니 되게 재밌었어요.
다음에는 CH2 - 코루틴 개발 환경 설정으로 찾아올게요.
'🤖안드로이드🤖' 카테고리의 다른 글
| [안드로이드] CoroutineDispatcher (0) | 2025.10.02 |
|---|---|
| [안드로이드] 코루틴 개발 환경 설정 (0) | 2025.09.17 |
| [안드로이드] 테스트 코드에 대하여 (2) | 2025.08.26 |
| [안드로이드] 멀티모듈, 왜 그리고 어떻게? (2) | 2025.08.25 |
| [안드로이드] 프로젝트 구조 뜯어보기 - Android SDK에 대하여 (5) | 2025.08.19 |