Post

Kotlin in Action 2판 14장 코루틴과 플로우를 활용한 동시성 프로그래밍

Kotlin in Action 2판 14장 코루틴과 플로우를 활용한 동시성 프로그래밍

14장 코루틴과 플로우를 활용한 동시성 프로그래밍

이 장에서 다루는 내용

  • 동시성·병렬성 개념
  • 일시중단 함수
  • 코루틴과 기존 동시성 모델(콜백, 퓨처, 리액티브)과의 비교
  • 코루틴 호출 구조

14.1 동시성과 병렬성

  • 동시성: 여러 작업을 동시에 실행하는 것. 반드시 물리적으로 동시에 실행되는 것이 아니라, 하나의 코어에서도 작업 전환을 통해 동시성을 구현할 수 있음.
  • 병렬성: 여러 작업을 여러 CPU 코어에서 물리적으로 동시에 실행하는 것.
  • 코틀린 코루틴은 동시성, 병렬성 모두 지원함.

14.2 코틀린의 동시성 처리 방식: 일시중단 함수와 코루틴

  • 코루틴: 코틀린의 강력한 특징으로 비동기적으로 실행되는 논블록킹 동시성 코드를 우아하게 작성가능
  • 구조화된 동시성 작업과 생명주기 관리 기능도 있음
  • 코루틴은 스레드를 블록시키지 않고도 일시적으로 실행을 멈췄다가 재개할 수 있음

14.3 스레드와 코루틴 비교

스레드는 동시성/병렬성을 위한 전통적인 추상화

  • 자바에서처럼 코틀린에서도 스레드는 서로 독립적으로 동시에 실행되는 코드 블록을 지정할 수 있음.
  • 코틀린 표준 라이브러리의 thread 함수를 사용하면 새 스레드를 쉽게 시작할 수 있음. ``` kotlin import kotlin.concurrent.thread

fun main() { println(“I’m on ${Thread.currentThread().name}”) // 메인 스레드 thread { println(“And I’m on ${Thread.currentThread().name}”) // 새 스레드 } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
- 스레드는 블로킹 작업(예: 네트워크 대기) 동안 자원을 낭비하게 됨
- JVM에서 생성하는 각 스레드는 OS가 관리하는 시스템 스레드로 생성·관리 비용이 높고,최신 시스템에서도 수천 개까지만 효율적으로 관리 가능
- 스레드는 작업(예시: 네트워크 요청)이 끝날 때까지 블록되는 특징을 가지고 있음  
  - 즉 대기 중인 동안 다른 의미 있는 작업을 하지 못하고 시스템 자원만 차지함

#### 코루틴의 특징

- 코루틴은 스레드의 경량 추상화로, 일시 중단 가능한 계산을 나타냄
- 초경량 추상화로, 노트북에서도 10만 개 이상의 코루틴을 실행 가능함
    - 생성·관리 비용이 매우 저렴하기 때문에 미세한 작업이나 아주 짧게 실행하는 작업에도 적합
- 코루틴은 시스템 자원을 블록시키지 않고 실행을 일시 중단할 수 있고, 나중에 멈춘 지점부터 다시 재개 가능함
  - 네트워크나 IO 대기 등 비동기 작업에서 매우 효율적임
- 구조화된 동시성 개념을 통해 취소 및 오류 처리를 위한 매커니즘을 제공
---

## 14.4 잠시 멈출 수 있는 함수: 일시중단 함수

- 코루틴의 가장 큰 특징은 **순차적으로 보이는 코드**에서 넌블로킹 비동기 처리가 가능하다는 점

- 예시: 블로킹 코드
    ``` kotlin
    fun login(credentials: Credentials): UserID
    fun loadUserData(userID: UserID): UserData
    fun showData(data: UserData)

    fun showUserInfo(credentials: Credentials) {
        val userID = login(credentials)
        val userData = loadUserData(userID)
        showData(userData)
    }
    ```
- 위 코드는 네트워크 대기 동안 스레드를 블록시켜 자원을 낭비함

- **코루틴 기반 일시중단 함수**는 다음과 같이 순차적 코드를 유지하면서도 넌블로킹 비동기를 구현할 수 있음
    ``` kotlin
    suspend fun login(credentials: Credentials): UserID
    suspend fun loadUserData(userID: UserID): UserData
    fun showData(data: UserData)

    suspend fun showUserInfo(credentials: Credentials) {
        val userID = login(credentials)
        val userData = loadUserData(userID)
        showData(userData)
    }
    ```
- `suspend` 키워드는 함수가 일시적으로 실행을 멈추고 재개될 수 있음을 나타내며 이때 스레드를 블록시키지 않음
- 대신 함수 실행이 중단되면 다른 코드가 같은 스레드에서 실행 가능
- 실제로 네트워크 라이브러리(Ktor HTTP, Retrofit, OkHttp 등)도 코루틴과 함께 동작하는 API를 제공함

---

## 14.5 코루틴과 다른 동시성 접근 방식 비교

- **콜백 기반**
    ``` kotlin
    fun loginAsync(credentials: Credentials, callback: (UserID) -> Unit)
    fun loadUserDataAsync(userID: UserID, callback: (UserData) -> Unit)
    fun showData(data: UserData)

    fun showUserInfo(credentials: Credentials) {
        loginAsync(credentials) { userID ->
            loadUserDataAsync(userID) { userData ->
                showData(userData)
            }
        }
    }
    ```
    - 콜백 중첩이 발생(콜백 지옥), 가독성과 유지보수성이 떨어짐.

- **퓨처(CompletableFuture) 기반**
    ``` kotlin
    fun loginAsync(credentials: Credentials): CompletableFuture<UserID>
    fun loadUserDataAsync(userID: UserID): CompletableFuture<UserData>
    fun showData(data: UserData)

    fun showUserInfo(credentials: Credentials) {
        loginAsync(credentials)
            .thenCompose { loadUserDataAsync(it) }
            .thenAccept { showData(it) }
    }
    ```
    - 콜백 중첩은 줄어들지만, 새로운 연산자를 익혀야 하고 반환 타입 변경 필요.

- **반응형 스트림(RxJava 등) 기반**
    ``` kotlin
    fun login(credentials: Credentials): Single<UserID>
    fun loadUserData(userID: UserID): Single<UserData>
    fun showData(data: UserData)

    fun showUserInfo(credentials: Credentials) {
        login(credentials)
            .flatMap { loadUserData(it) }
            .doOnSuccess { showData(it) }
            .subscribe()
    }
    ```
    - 역시 타입이 복잡해지고, 연산자 개념을 익혀야 함.

- **코루틴 기반**은 기존 함수에 `suspend`만 붙이면 되고, 코드는 순차적이며 스레드를 블록시키는 단점을 피할 수 있음

---
## 14.5.1 일시 중단 함수 호출

- **일시 중단 함수**는 실행을 일시적으로 멈출 수 있기 때문에, 임의의 위치에서 호출할 수 없음.
- 반드시 **코루틴이나 다른 일시 중단 함수** 내에서만 호출 가능함.
    - 이는 '함수가 실행을 일시 중단할 수 있다면 그 함수를 호출하는 함수의 실행도 잠재적으로 일시중단 될 수 있음' 이라는 논리와 일치함

예시:
``` kotlin
suspend fun showUserInfo(credentials: Credentials) {
    val userID: UserID = login(credentials)
    val userData: UserData = loadUserData(userID)
    showData(userData)
}
  • 일반 함수에서 일시 중단 함수 호출 시 컴파일 오류
    1
    2
    3
    4
    5
    
      suspend fun mySuspendingFunction() {}
    
      fun main() {
          mySuspendingFunction() // Error: Suspend function should be called only from a coroutine or another suspend function
      }
    
  • 일시 중단 함수 호출 방법
    1. 가장 단순한 방법: main 함수 자체를 suspend로 선언
      1
      2
      3
      
       suspend fun main() {
           // 일시 중단 함수 호출 가능
       }
      
      • 다만, 안드로이드 등 프레임워크에서는 메인 함수를 suspend로 바꿀 수 없으므로 범용적이지 않음.
    2. 코루틴 빌더 함수 사용
      • 코루틴 빌더는 새 코루틴을 만들고, 일시 중단 함수 실행 진입점 역할을 함

This post is licensed under CC BY 4.0 by the author.