๐Ÿ’ก Kotlin Coroutines: ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ •๋ฆฌ

devdoยท2025๋…„ 12์›” 1์ผ

์ฝ”ํ‹€๋ฆฐ

๋ชฉ๋ก ๋ณด๊ธฐ
4/4

๐Ÿ’ก Kotlin Coroutines: ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํ™œ์šฉ ์˜ˆ์‹œ

Kotlin Coroutines๋Š” Android ํ™˜๊ฒฝ์—์„œ ๋น„๋™๊ธฐ(Asynchronous) ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์œ„ํ•œ ๊ฐ€์žฅ ํ˜„๋Œ€์ ์ด๊ณ  ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ๋ฉ”์ธ ์Šค๋ ˆ๋“œ(UI ์Šค๋ ˆ๋“œ)๋ฅผ ๋ธ”๋กœํ‚นํ•˜์ง€ ์•Š๊ณ , ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ๊ณผ ๊ฐ™์€ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

ํ•ต์‹ฌ์€ suspend ํ•จ์ˆ˜์™€ **๊ตฌ์กฐํ™”๋œ ๋™์‹œ์„ฑ(Structured Concurrency)**์ž…๋‹ˆ๋‹ค.


1. โš™๏ธ ์ฝ”๋ฃจํ‹ด์„ ์œ„ํ•œ ํ™˜๊ฒฝ ์„ค์ •

Kotlin Coroutines๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด build.gradle.kts ํŒŒ์ผ์— ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// build.gradle.kts (Module: app)

dependencies {
    // ์ฝ”๋ฃจํ‹ด ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    
    // Android ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ์‹œ
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    // ViewModel์—์„œ ์ฝ”๋ฃจํ‹ด ์‚ฌ์šฉ ์‹œ
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
}

2. ๐Ÿ“ ํ™œ์šฉ ์˜ˆ์‹œ: ์•ˆ๋“œ๋กœ์ด๋“œ MVVM ์•„ํ‚คํ…์ฒ˜

์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ์—์„œ ViewModel ๋‚ด์—์„œ ๋„คํŠธ์›Œํฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ UI์— ๋ฐ˜์˜ํ•˜๋Š” ํ‘œ์ค€์ ์ธ ์ฝ”๋ฃจํ‹ด ํ™œ์šฉ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

2.1. suspend ํ•จ์ˆ˜ ์ •์˜ (๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด)

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ์ด๋‚˜ Retrofit์„ ์‚ฌ์šฉํ•œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋“ฑ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์€ suspend ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” ์ฝ”๋ฃจํ‹ด ์Šค์ฝ”ํ”„ ๋‚ด์—์„œ๋งŒ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹คํ–‰ ์ค‘ ๋ธ”๋กœํ‚น ์—†์ด ์ž ์‹œ ๋ฉˆ์ท„๋‹ค๊ฐ€(suspending) ๋‚˜์ค‘์— ์žฌ๊ฐœ๋ฉ๋‹ˆ๋‹ค.

// 1. Data Class (์˜ˆ์‹œ)
data class User(val id: Int, val name: String)

// 2. Repository (๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด)
class UserRepository {
    
    // suspend ํ‚ค์›Œ๋“œ: ์ด ํ•จ์ˆ˜๊ฐ€ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•จ์„ ๋ช…์‹œ
    suspend fun fetchUser(userId: Int): User {
        // withContext๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ IO ์Šค๋ ˆ๋“œ๋กœ ์ „ํ™˜ (๋„คํŠธ์›Œํฌ, DB ์ž‘์—…์— ์ ํ•ฉ)
        return withContext(Dispatchers.IO) {
            // ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด๋‚˜ DB ์ฟผ๋ฆฌ ์ฝ”๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.
            
            // ์‹œ๋ฎฌ๋ ˆ์ด์…˜: 2์ดˆ ๋™์•ˆ ์ง€์—ฐ (๋ธ”๋กœํ‚น ์—†์ด)
            kotlinx.coroutines.delay(2000)
            
            // ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
            User(userId, "์‚ฌ์šฉ์ž #$userId")
        }
    }
}

2.2. ViewModel์—์„œ ์ฝ”๋ฃจํ‹ด ์‹คํ–‰ (ViewModel ๋ ˆ์ด์–ด)

ViewModel์—์„œ๋Š” viewModelScope๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋ฃจํ‹ด์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. viewModelScope๋Š” ViewModel์˜ ์ˆ˜๋ช… ์ฃผ๊ธฐ์™€ ํ•จ๊ป˜ ์‹œ์ž‘ํ•˜๊ณ , ViewModel์ด ํŒŒ๊ดด๋  ๋•Œ ์ฝ”๋ฃจํ‹ด์„ ์ž๋™์œผ๋กœ ์ทจ์†Œ(Cancel)ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค (๊ตฌ์กฐํ™”๋œ ๋™์‹œ์„ฑ).

// 1. ViewModel (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ ˆ์ด์–ด)
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    
    // UI์— ๋ณด์—ฌ์ค„ ์ƒํƒœ (LiveData ๋˜๋Š” StateFlow ์‚ฌ์šฉ)
    private val _userState = MutableStateFlow<User?>(null)
    val userState: StateFlow<User?> = _userState
    
    fun loadUserData(userId: Int) {
        // 2. viewModelScope๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋ฃจํ‹ด ์‹œ์ž‘
        // ViewModel์ด ์‚ด์•„์žˆ๋Š” ๋™์•ˆ ์ฝ”๋ฃจํ‹ด์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
        viewModelScope.launch {
            try {
                // 3. suspend ํ•จ์ˆ˜ ํ˜ธ์ถœ (awaiting its result)
                val user = repository.fetchUser(userId) 
                
                // 4. ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ: UI ์Šค๋ ˆ๋“œ์—์„œ ์ž๋™์œผ๋กœ ์‹คํ–‰๋จ
                _userState.value = user
                
            } catch (e: Exception) {
                // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
                Log.e("UserViewModel", "๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", e)
            }
        }
    }
}

2.3. ์—ฌ๋Ÿฌ ๋น„๋™๊ธฐ ์ž‘์—… ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ

๋‘ ๊ฐ€์ง€ ์ด์ƒ์˜ ๋…๋ฆฝ์ ์ธ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰ํ•˜๊ณ  ๋ชจ๋“  ๊ฒฐ๊ณผ๊ฐ€ ๋„์ฐฉํ•  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ค์•ผ ํ•  ๊ฒฝ์šฐ, async ๋นŒ๋”์™€ await ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

class DataService(private val repository: UserRepository) {
    
    // ๋‘ ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰
    suspend fun loadDashboardData(userId: Int): Pair<User, String> {
        
        // CoroutineScope ๋‚ด์—์„œ async๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘์—…์„ ๋ณ‘๋ ฌ๋กœ ์‹œ์ž‘
        val userDeferred = CoroutineScope(Dispatchers.IO).async { 
            repository.fetchUser(userId) 
        }
        
        val settingDeferred = CoroutineScope(Dispatchers.IO).async { 
            fetchUserSettings() // ๋ณ„๋„์˜ suspend ํ•จ์ˆ˜ ๊ฐ€์ •
        }

        // await()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‘ ์ž‘์—…์ด ๋ชจ๋‘ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.
        val user = userDeferred.await()
        val settings = settingDeferred.await()

        return Pair(user, settings)
    }
    
    // ์„ค์ • ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋˜ ๋‹ค๋ฅธ suspend ํ•จ์ˆ˜ (์˜ˆ์‹œ)
    private suspend fun fetchUserSettings(): String {
        delay(1500) // 1.5์ดˆ ์ง€์—ฐ
        return "Loaded Settings"
    }
}

3. ๐Ÿ”‘ ์ฝ”๋ฃจํ‹ด ํ•ต์‹ฌ ๊ฐœ๋… ์š”์•ฝ

๊ฐœ๋…์„ค๋ช…ํ™œ์šฉ
suspendํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰ ์ค‘ ์ž ์‹œ ๋ฉˆ์ถœ ์ˆ˜ ์žˆ๋Š”(Non-blocking) ๋น„๋™๊ธฐ ํ•จ์ˆ˜์ž„์„ ํ‘œ์‹œ.๋ชจ๋“  ๋„คํŠธ์›Œํฌ/DB ์ ‘๊ทผ ํ•จ์ˆ˜ ์ •์˜.
launch์ฝ”๋ฃจํ‹ด์„ ์‹œ์ž‘ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ฌด์‹œํ•˜๊ฑฐ๋‚˜(fire-and-forget), ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์‚ฌ์šฉ.ViewModel์—์„œ UI ์—…๋ฐ์ดํŠธ ์ฝ”๋ฃจํ‹ด ์‹œ์ž‘.
async / await๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋ฃจํ‹ด์„ ์‹œ์ž‘(async)ํ•˜๊ณ , ๋‚˜์ค‘์— ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์„ ๋•Œ(await) ์‚ฌ์šฉ. ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ์— ์ฃผ๋กœ ํ™œ์šฉ๋จ.์—ฌ๋Ÿฌ API ํ˜ธ์ถœ์„ ๋™์‹œ์— ์ˆ˜ํ–‰.
Dispatchers์ฝ”๋ฃจํ‹ด์ด ์‹คํ–‰๋  ์Šค๋ ˆ๋“œ(์ปจํ…์ŠคํŠธ)๋ฅผ ์ง€์ •.Dispatchers.Main (UI), Dispatchers.IO (๋„คํŠธ์›Œํฌ/DB), Dispatchers.Default (CPU ์ง‘์•ฝ ์ž‘์—…).
withContextํ˜„์žฌ ์ฝ”๋ฃจํ‹ด์ด ์‹คํ–‰๋˜๋Š” ์ปจํ…์ŠคํŠธ๋ฅผ ๋ณ€๊ฒฝ(์Šค๋ ˆ๋“œ ์ „ํ™˜)ํ•˜๊ณ  ์ž‘์—…์„ ์ˆ˜ํ–‰ ํ›„ ์›๋ž˜ ์ปจํ…์ŠคํŠธ๋กœ ๋ณต๊ท€.Dispatchers.Main์—์„œ Dispatchers.IO๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ˆ˜ํ–‰.
profile
์ž๋ฐ” ์Šคํ”„๋ง ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค. ๋ฐฐ์šด ๊ฒƒ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€