Global Scope를 쓰지 말아야하는 이유

유진·4일 전
0

Android

목록 보기
3/3

프로젝트에 있는 모든 Global Scope를 제거하였다. 그 이유는?

가장 큰 이유는 공식문서에 Avoid GlobalScope라고 나와있음

그 이유에 대해서도 3가지나 나와 있음.
하지만 그냥 3줄 나와있고 끝임.
나는 궁금하다! 왜 쓰면 안될까?

자 그럼 공식문서에서 말해주는 이유에 대해서 가져와보겠다.

This is similar to the Inject Dispatchers best practice. By using GlobalScope, you're hardcoding the CoroutineScope that a class uses bringing some downsides with it:

  1. Promotes hard-coding values. If you hardcode GlobalScope, you might be hard-coding Dispatchers as well.

  2. Makes testing very hard as your code is executed in an uncontrolled scope, you won't be able to control its execution.

  3. You can't have a common CoroutineContext to execute for all coroutines built into the scope itself.

한국어로 바꿔보면

1. GlobalScope를 하드코딩하게되면 Dispatcer도 하드코딩하게 된다.

-> 이게 왜 안좋냐? 공식문서에 따르면 Dispatcher를 생성하지 말고 주입해서 끼우라고 했음.

그럼 Dispatcher는 왜 주입해야하냐?
-> 테스트 용이성, 유연성, 유지보수성 때문이다.

// DO inject Dispatchers
class NewsRepository(
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

// DO NOT hardcode Dispatchers
class NewsRepository {
    // DO NOT use Dispatchers.Default directly, inject it instead
    suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

2. GlobalScope는 제어할 수 없는 영역(scope)에서 실행됨으로 제어나 테스트 하기가 힘들다.

-> ㅇㅋ 이건 이해 완료

3. scope 자체에 빌드된 모든 코루틴에서 실행할 공통 CoroutineContext를 가질 수 없다.

-> 공식문서에는 can't have니까 가질 수 없다지만.. 사실 가질 수 없다보단, 공통된 하나의 CoroutineContext를 쓸 이유가 없다가 더 맞는 말이다. 코루틴의 장점은 유연성으로, CoroutineScope 마다, 심지어 그 scope안에서도 context를 바꿔서 실행하게 할 수 있는데, 그에 반해 GlobalScope를 써버리면 그 자체가 불가능하니까 구리다..는 거다.

val scope = CoroutineScope(Job()) // 기본적으로 어떤 Dispatcher도 지정하지 않음

scope.launch(Dispatchers.Main) {
    println("UI 업데이트 작업")
}

scope.launch(Dispatchers.IO) {
    println("네트워크 작업")
}

정리하자면 coroutine의 설계 원칙인 "유연성"을 활용할 수 없게 되어 버린다는 내용이다.


그외 문제

그외에도 쓰지 말아야하는 이유를 개발자의 관점에서 더 말해보자면, 2번이랑 연관이 있긴 한데,

메모리 누수문제

메모리 누수가 뭐냐?

응용 프로그램에서 데이터를 메모리에 올려놓은 후, 더 이상 사용하지 않음에도 불구하고 해제하지 않아 사용할 수 있는 메모리가 점점 줄어드는 현상을 말합니다.

사용하지 않는 메모리는 해제해야한다. 그걸 자동으로 해주는게 GC (Garbage Collection)인데, GlobalScope의 경우 앱의 전체를 주기로 작동하는 녀석이기 때문에, 앱이 종료되기 전까지는 죽지 않는다. viewModelScopelifecycleScope를 사용한다면 알아서 GC가 처리해줄텐데, 앱 전체를 무대로 하니.. 알아서 처리되지 않는다.

GlobalScope.launch {
    delay(5000) // 5초 대기
    println("작업 완료")
}
// 액티비티 종료 후에도 "작업 완료"가 출력됨.

그렇기 때문에 해당 사용이 완료되더라도, 참조가 남아 있어 GC 되지 않고, 코루틴이 계속 실행될 수 위험이 있다. 이러한 리소스 낭비가 발생할 수 있기 때문에 GlobalScope 남발을 지양해야 한다.

생명주기를 고려하지 않는다.

비슷한 문제로, 생명주기를 고려하기 힘들다는 문제도 있다. 안드로이드에서는 lifecycle이라는것이 있는데, 액티비티가 시작되서~종료되는 그 일련의 과정을 말한다. 네트워크 호출의 경우 해당 화면(액티비티)에서 나가버리면 해당 네트워크 호출은 더이상 필요가 없기때문에 cancel 해야한다. 그런 lifecycle을 다 알고서 실행되는 코드의 경우 사용자가 뒤로가기를 누르면 해당 네트워크 호출을 자동으로 취소해준다. 하지만 GlobalScope는 말그대로 글로벌 영역에서 실행되기 때문에 lifecycle 따위는 알지 못한다.

다 일일히 개발자가 화면에서 벗어나면~ 호출을 취소~ 하는 코드를 작성해야하는데, 어느 개발자가 그 많은 코드에 취소 명령을 적고 있겠나, 생각만 해도 번잡시럽다. 그래서 GlobalScope보다는viewModelScope, lifecycleScope 등 수명 주기를 인식하는 스코프를 사용하는게 앱에게도, 그리고 개발자의 생산성에도 큰 도움이 될 것이다.


그렇다면 GlobalScope를 사용해야하는 때는 언제인가?

로깅 같이 정말 앱의 수명주기와 별개로 돌아가야하는 로직이라면.. 추천한다..


reference

https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html

https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ko

profile
안드로이드 학생 개발자 에디 / 유진입니다

0개의 댓글