Android 코루틴을 처음 접하면 헷갈리는 개념 중 하나가 Scope입니다. 왜 scope를 사용해야하고, 어떤 scope를 사용해야하는지 의문이 생기게 됩니다.
이번 글에서는 코루틴 Scope가 무엇인지, 왜 필요한지, 어떤 종류가 있고 어떻게 사용하는지 정리해보도록 하겠습니다.
Scope는 코루틴의 생명주기를 관리하는 범위입니다. 쉽게 말해 "이 코루틴이 언제 시작되고 언제 끝나는지"를 결정하는 관리자라고 생각하면 됩니다.
왜 Scope가 필요한가?
// ❌ 이런 코드는 불가능
launch { // 에러! Scope가 없음
delay(1000)
}
// ✅ Scope와 함께 사용
lifecycleScope.launch {
delay(1000)
}
Android에서 사용하는 주요 Scope들을 살펴보겠습니다.
GlobalScope.launch {
// 앱이 종료될 때까지 살아있음
}
globalScope가 가진 특징 때문에 거의 사용되지 않고 있습니다. Activity가 종료되어도 코루틴이 계속 실행되어 메모리 누수가 발생할 수 있고, 코루틴이 언제 끝날지 알 수가 없습니다. 또한 불필요한 작업이 백그라운드에서 계쏙 돌아가기 때문에 리소스 낭비가 발생합니다. globalScope가를 사용해야될 대부분의 상황에 더 나은 대안이 있어서 globalScope는 거의 사용되지 않고 있습니다.
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = repository.fetchData()
_uiState.value = data
}
}
}
viewModelScope는 ViewModel에서 사용되는 Scope입니다. ViewModel의 내부의 모든 비동기 작업에 사용되며, 네트워크 요청 또는 데이터베이스 조회를 할 때 사용됩니다. 추가적으로 비즈니스 로직 처리를 할 때도 viewModelScope를 사용합니다.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Activity가 destroy되면 자동 취소
}
}
}
lifeCycleScope은 UI를 직접 업데이트하거나 일회성 작업에 사용됩니다. ViewModel없이 간단한 작업을 처리할 때도 사용됩니다. lifeCycleScope은 Activity가 백그라운드로 가도 계속 실행되는데, 이를 방지하기 위해 사용되는 것이 RepeatOnLifeCycle입니다.
// 일반 lifecycleScope
lifecycleScope.launch {
// Activity가 백그라운드로 가도 계속 실행됨
flow.collect { data ->
updateUI(data)
}
}
// repeatOnLifecycle (추천)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// STARTED 상태일 때만 실행
// 백그라운드로 가면 자동 중지, 돌아오면 재시작
flow.collect { data ->
updateUI(data)
}
}
}
RepeatOnLifeCycle을 사용하면 위의 코드의 설명과 같이 Activity의 생명주기가 STARTED일 때만 코루틴이 작동되고, 백그라운드로 가면 중지됩니다.
@Composable
fun MyScreen() {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
// 버튼 클릭 시 코루틴 실행
}
}) {
Text("클릭")
}
}
rememberCoroutineScope는 버튼 클릭 같은 일회성 이벤트에 주로 사용됩니다. SnackBar, Toast를 표시하거나, 애니메이션을 트리거하는데 사용되는 Scope입니다.
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun fetchData() {
scope.launch {
// 작업
}
}
fun cleanup() {
scope.cancel() // 수동으로 취소 필요!
}
}
coroutineScope를 직접 생성해 사용하는 경우는 Repository나 Manager 같은 커스텀 클래스, 또는 Android 컴포넌트가 아닌 곳에서 코루틴을 사용할 때입니다.
특별한 생명주기 관리가 필요한 상황에서도 직접 생성해 사용할 수 있지만,
이 경우에는 반드시 cancel()을 호출해 메모리 누수를 방지해야 합니다.
suspend fun loadUserProfile() = supervisorScope {
val userDeferred = async { loadUser() }
val postsDeferred = async { loadPosts() }
// 하나가 실패해도 다른 작업은 계속됨
val user = runCatching { userDeferred.await() }
val posts = runCatching { postsDeferred.await() }
combineResult(user, posts)
}
supervisorScope은 "여러 자식 작업을 동시에 수행하지만, 서로의 실패로 인해 전체 작업이 중단되면 안 되는 상황"에서 사용됩니다. 예외가 발생해도 전체 코루틴이 죽지 않으므로, 구조화된 동시성을 유지하면서도 안정적인 처리가 가능합니다.
코루틴을 안전하고 예측 가능하게 사용하기 위해서는 작업의 성격과 생명주기에 맞는 Scope를 선택하는 것이 무엇보다 중요합니다. 각 Scope의 특성과 역할을 이해해두면 구조화된 동시성을 자연스럽게 적용할 수 있고, 더 안정적인 비동기 코드를 만들 수 있습니다.
결국 코루틴을 제대로 활용하는 첫 단계는 Scope를 올바르게 이해하고 상황에 맞게 사용하는 것입니다.
이 점을 기억해두면 Android 개발에서 코루틴을 훨씬 더 명확하고 효율적으로 다룰 수 있습니다.