---
id: android-kotlin-coroutines-scopes
title: Kotlin Coroutines — Scope 와 Lifecycle 매핑
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [android, kotlin, coroutines, scope, lifecycle, vibe-coding]
tech_stack: { language: "Kotlin", applicable_to: ["Android", "Server (ktor)"] }
applied_in: []
aliases: [viewModelScope, lifecycleScope, GlobalScope, supervisorScope]
---

# Kotlin Coroutines — Scope 와 Lifecycle

> coroutine 의 핵심은 **scope 가 lifecycle 과 묶여 있는가**. 안 묶이면 leak. Android 는 ViewModelScope / LifecycleScope 가 95% 답.

## 📖 핵심 개념
- `CoroutineScope` 는 Job 을 묶는 컨테이너. scope cancel → 모든 자식 cancel.
- `viewModelScope`: ViewModel 살아있는 동안. cleared 되면 cancel.
- `lifecycleScope`: Activity/Fragment 의 onDestroy 에서 cancel.
- `repeatOnLifecycle(STARTED)`: STARTED 동안만 실행, STOP 시 cancel.
- `GlobalScope`: 앱 전체 — 거의 안 씀. leak 무서움.

## 💻 코드 패턴

### ViewModel 안에서
```kotlin
class UserViewModel(private val repo: UserRepository) : ViewModel() {
    private val _state = MutableStateFlow<UiState>(UiState.Idle)
    val state: StateFlow<UiState> = _state

    fun load(id: String) {
        viewModelScope.launch {
            _state.value = UiState.Loading
            try {
                val user = repo.fetch(id)
                _state.value = UiState.Success(user)
            } catch (e: CancellationException) { throw e } // 반드시 re-throw
              catch (e: Exception) { _state.value = UiState.Error(e.message ?: "") }
        }
    }
}
```

### Compose / Activity — repeatOnLifecycle
```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.state.collect { render(it) }
            }
        }
    }
}
```

### supervisorScope — 한 자식 실패가 형제 안 죽이게
```kotlin
suspend fun loadProfile(id: String) = supervisorScope {
    val user = async { fetchUser(id) }
    val posts = async { fetchPosts(id) }
    Profile(
        user = user.await(),                  // user 실패하면 throw
        posts = runCatching { posts.await() }.getOrElse { emptyList() }, // posts 실패는 빈 list
    )
}
```

### Dispatcher 선택
```kotlin
withContext(Dispatchers.IO) { db.query() }       // DB / network
withContext(Dispatchers.Default) { heavyCalc() } // CPU-bound
withContext(Dispatchers.Main) { ui.update() }    // UI
```

## 🤔 의사결정 기준
| 상황 | scope |
|---|---|
| ViewModel 비즈니스 로직 | `viewModelScope` |
| Activity/Fragment UI 수집 | `lifecycleScope` + `repeatOnLifecycle` |
| Compose 안 | `LaunchedEffect`, `rememberCoroutineScope` |
| Service / WorkManager | `CoroutineScope(Dispatchers.IO + Job())` |
| 한 자식 실패가 형제 영향 X | `supervisorScope` |
| 진짜 앱 전역 (1개) | `GlobalScope` (거의 X) |

## ❌ 안티패턴
- **`GlobalScope.launch` 남발**: leak. 어떤 lifecycle 과도 안 묶임.
- **`CancellationException` catch 후 swallow**: structured concurrency 깨짐. re-throw 필수.
- **Main dispatcher 에서 무거운 IO**: ANR. withContext(Dispatchers.IO).
- **viewModelScope 안에서 GlobalScope.launch**: 부모-자식 끊김.
- **runBlocking 을 main thread**: 메인 스레드 blocking. 거의 X.
- **collect 를 lifecycleScope.launch 만**: STOP 상태에서도 collect → 백그라운드 메모리 leak. repeatOnLifecycle 으로.
- **flow 를 hot 으로 만들고 collect 안 함**: emit 사라짐. SharedFlow / StateFlow 검토.

## 🤖 LLM 활용 힌트
- "ViewModel 함수는 viewModelScope 안에서 launch. UI 수집은 repeatOnLifecycle" 강조.
- CancellationException re-throw 명시.

## 🔗 관련 문서
- [[Android_Flow_StateFlow_SharedFlow]]
- [[Android_Lifecycle_Aware_Components]]
