Kotlin CoroutineScope을 알아보고, cancel 처리에 대해서 알아보자.
개인 광고 영역
runBlocking과 GlobalScope에 대해서 알아보았다.
- Kotlin Coroutines의 runBlocking은 언제 써야 할까? 잘 알고 활용하자!
- Kotlin Coroutines의 GlobalScope을 어떻게 사용할 수 있을까? - 사실 추천하지 않는다.
이 두 가지의 코루틴은 이유 없이 사용해서는 안 되는 것들이다. 그럼 사용할 수 있는 건 가장 기본적인 CoroutineScope이 남는다.
필자가 한참 코루틴을 학습할 때 적었던 내용에 CoroutineScope의 활용 방법은 이미 다양하게 나온다.
이 글에서는 CoroutineScope의 사용 방법을 좀 더 알아보고(MainScope, CoroutineScope), 무한 루프 상의 cancel 처리 동작을 알아보려고 한다.
이 글에서 알아볼 내용
- CoroutineScope 사용 방법을 알아본다.
- CoroutineScope 안에서 동작하는 무한 루프에서의 cancel의 동작을 알아본다.
- AAC-ViewModel의 viewModelScop을 알아본다.
CoroutineScope의 활용
GlobalScope을 활용할 때는 상황에 따라 다를 수 있지만 장시간 동작해야 하는 프로그램에서는 사용 시 주의가 필요하고, 메모리 문제와 앱의 동작에 영향을 미치지 않는 사용법이 필요함은 이전 글에서 알 수 있었다.
이 글에서는 CoroutineScope을 알아볼 건데,
문서를 보면 Android에서의 활용할 수 있는 MainScope()에 대한 부분을 소개하고 있는데, 그대로 가져와보았다.
class MyUIClass {
val scope = MainScope() // the scope of MyUIClass
fun destroy() { // destroys an instance of MyUIClass
scope.cancel() // cancels all coroutines launched in this scope
// ... do the rest of cleanup here ...
}
/*
* Note: if this instance is destroyed or any of the launched coroutines
* in this method throws an exception, then all nested coroutines are cancelled.
*/
fun showSomeData() = scope.launch { // launched in the main thread
// ... here we can use suspending functions or coroutine builders with other dispatchers
draw(data) // draw in the main thread
}
}
이 MainScope은 내부 코드를 보면 결국 CoroutineScope을 활용하고 있음을 알 수 있고, 안드로이드 관련 코드를 추가로 담아두었다는 점이다.
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
한 번 더 타고 들어가면 CoroutineScope을 상속받고, 이를 활용할 수 있는 context만 주입하는 공통적인 코드를 사용하고 있다.
참고로 이 코드는 internal이기에 외부에서는 사용하지 못한다.
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
override val coroutineContext: CoroutineContext = context
// CoroutineScope is used intentionally for user-friendly representation
override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}
만약 MainScope 대신 CoroutineScope을 직접 상속받는다면 아래처럼 작업해야 한다.
class YourActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + SupervisorJob()
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
결국 cancels을 하기 위한 작업은 해줘야 한다.
무조건 cancel을 부른다고 종료하는 것은 아니다.
이 코드를 보고 예측을 해보자.
class CoroutineCancel {
@Test
fun testCancel() = runBlocking {
val job = SupervisorJob()
CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
println("Job active")
delay(1)
}
}
delay(50)
job.cancel()
delay(500)
}
}
이 코드 내부에 있는 CoroutineScope은 종료하지 않는다.
참고 테스트 코드라서 500ms가 지나면 종료된다.
- while은 쉬지 않고 돌아간다.
- 내부의 CoroutineScope에 대한 종료를 호출하는 코드가 없기 때문에 종료하지 못한다.
먼저 이 코드의 while 문을 별도의 suspend 함수로 변경해보자.
private suspend fun suspendLoop() = withContext(Dispatchers.IO) {
while (isActive) {
println("Job active")
delay(1)
}
}
이 역시 500ms가 넘어가야 종료되는 건 동일하다.
그럼 종료할 수 있도록 수정을 해야 하는데 job을 아래와 같이 추가하면 자연스럽게 종료한다.
val job = SupervisorJob()
CoroutineScope(Dispatchers.IO + job).launch {
suspendLoop()
}
while에서의 종료
job만 가지고 잘 처리하더라도 종료는 할 수 있다. 이 부분은 간단히 해결할 수 있지만 사실 while 문을 활용할 때 문제가 있는데, 언제 cancel이 일어나는지가 중요하다.
이 코드는 isActive를 체크하다 보니 isActive의 상태에 따라 종료한다.
private suspend fun suspendLoop() = withContext(Dispatchers.IO) {
while (isActive) {
println("Job active")
delay(1)
}
}
이 부분은 과거에 작성한 job의 상태를 정리한 글을 참고하시면 될 것 같다.
Kotlin Coroutines의 Job 동작을 알아보자
만약 아래와 같이 단순히 무한 루프를 동작 시켰다면 동작하지 않는다.
private suspend fun suspendLoop() = withContext(Dispatchers.IO) {
while (true) {
println("Job active")
delay(1)
}
}
이 코드를 보면 무한 루프에서는 최소한 isActive를 이용해 제어해야 함을 알 수 있다.
AAC ViewModel의 코루틴 사용 추천 방법
이 번엔 안드로이드에서 가장 많이 사용하는 ViewModel에서의 코루틴 사용 방법을 알아본다.
Use Kotlin coroutines with Architecture components의 viewModelScope을 활용할 수 있는데 다음과 같다.
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
여기서 말하는 viewModelScop은 어떤 것일까?
이 viewModelScop은 ViewModel ktx를 이용해 사용할 수 있는 extension에 해당한다.
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:lastVersion'
그래서 내부 코드를 조금 살펴보면 아래와 같은데, 앞에서 보았던 MainScope()의 코드와 동일하지만 Dispatches 사용법은 조금 다름을 알 수 있다.
/**
* [CoroutineScope] tied to this [ViewModel].
* This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*/
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
그럼 이 코루틴은 언제 종료할까? 끝에 있는 Closeable의 상속 부분을 중요하게 봐야 한다.
Kotlin extensions use를 알아보고, 사용법을 알아보자. - Closeable 참고
Closeable을 구현하고, AAC-ViewModel의 내부 코드에 보면 clear가 있고, 이 코드에서 closeWithRuntimeException 이란 코드가 있다.
이 코드 내부에서 확인할 수 있듯 Closeable인 경우 close를 한 번 더 호출할 수 있음을 알 수 있다.
정리하면 Android onDestroy가 호출되면 ViewModel의 clear가 호출되고, closeWithRuntimeException이 호출되어 자연스럽게 AAC-ViewModel의 viewModelScop을 함께 종료한다.
public abstract class ViewModel {
// 생략
@MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
마무리
CoroutineScope의 다양한 접근 방법을 확인해 보았다.
일부는 알아서 처리해 주는 것도 있지만 결국 사용자가 잘 알고 써야 할 부분들이 많다. 위에서 보았든 MainScope을 쓰고, CoroutineScope을 썼다면 종료 처리는 필수이다.
그리고 cancel을 한다고 해서 무조건 종료하는 것 역시 아님을 알 수 있었다.
정리하면
- Lifecycle에 따른 Scope 활용이 필요하다.
- Lifecycle에 따라 Scope의 cancel은 꼭 호출해야 한다.
- cancel을 한다고 해서 모든 Scope가 무조건 종료하는 것은 아니다.
- 무한 루프에서는 cancel을 하기 위해서는 isActive를 체크해 줘야 한다.
이 글만 보기 아쉽죠. Effective Kotlin 더 보기
지금까지 작성한 EffectiveKotlin 관련 글은 태그로 모아 링크를 추가한다.
Comments