Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancellation and Exceptions in Coroutines (Part 4) - Coroutines & Patterns for work that shouldn’t be cancelled 아티클 정리 #12

Open
ChanJun-Park opened this issue Sep 15, 2022 · 0 comments

Comments

@ChanJun-Park
Copy link
Owner

Part 2 에서 더이상 실행될 필요가 없는 코루틴을 취소하는 것의 중요성과 방법을 알아봤다. 안드로이드 개발에서는 Jetpack 에서 제공하는 viewMdoelScopelifecycleScope 를 사용하여 이를 처리해줄 수 있었다. 만약 직접 CoroutineScope 를 생성한다면 특정한 하나의 Job 에 스코프를 묶어주고, cancel 까지 직접 호출해줘야 한다.

그런데 만약 사용자가 앱 화면을 벗어난 경우에도 코루틴의 실행을 취소하지 말아야 하는 경우에는 어떻게 해야할까? 이 아티클에 대해서는 그러한 패턴을 구현하는 방법에 대해서 알아본다.

Coroutines or WorkManager?

코루틴은 앱의 프로세스가 살아있는 동안 계속해서 동작할 수 있다. 앱이 죽으면 코루틴도 종료된다. 만약 앱의 프로세스 라이프사이클보다 더 길게 유지되어야 하는 작업을 해야한다면 Android WorkManager 를 사용해야 한다.

만약 앱의 프로세스가 살아있는 동안에만 동작을 수행해야할 필요가 있다면 코루틴을 사용할 수 있다. 이러한 동작을 구현할 수 있는 패턴은 무엇이 있을까?

Coroutines best practices

앞서 말한 요구사항을 구현할 패턴은 Coroutines best practices 들을 기반으로 만들어지기 때문에 Coroutines best practices 를 다시 한번 복습해보자.

1. Inject Dispatchers into classes

클래스에서 코루틴을 생성하거나 withContext 와 같은 함수로 컨텍스트를 변환할때 Dispatchers 를 하드코딩 하지 말고 클래스에 Inject 하는 방식으로 구현해야 한다. 이런 식으로 구현하면 테스트시 Dispatchers 를 테스트 용도에 맞게 바꿔서 전달할 수 있어서 테스트 코드 작성을 쉽게 할 수 있다.

2. The ViewModel/Presenter layer should create coroutines

UI 레이어가 직접 비즈니스 로직을 처리하지 않아야 한다. 비즈니스 로직은 ViewModel/Presenter 에서 처리하도록 한다. UI 레이어는 테스트를 하기 위해 안드로이드 에뮬레이터가 필요해서, 애뮬레이터가 필요없는 ViewModel/Presenter 를 테스트하는 것이 더 쉽다.

3. The layers below the ViewModel/Presenter layer should expose suspend functions and Flows

Data 레이어와 같은 영역은 suspend functions 나 Flows 를 반환하도록 메소드를 작성해야 한다. 이렇게 함으로써 ViewModel Layer 에서 코루틴을 생성하고 해당 코루틴의 lifecycle 을 관리해줄 수 있다. 만약 코루틴을 생성할 필요가 있다면 coroutineScope 나 supervisorScope 를 이용해서 sub scope 를 만든 다음 생성해야 한다(?)

만약 다른 lifecycle 의 scope 를 사용하고 싶다면 이 글을 계속 읽어라

Operations that shouldn’t be cancelled in Coroutines

다음 예제 코드를 보자

class MyViewModel(private val repo: Repository) : ViewModel() {
  fun callRepo() {
    viewModelScope.launch {
      repo.doWork()
    }
  }
}
class Repository(private val ioDispatcher: CoroutineDispatcher) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      veryImportantOperation() // This shouldn’t be cancelled
    }
  }
}

만약 doWork() 을 실행하는 코루틴의 lifecycle 과는 상관없이 veryImportantOperation() 가 반드시 실행되게 하고 싶으면 어떻게 해야할까?

이런 경우에는 앱의 커스텀 Application 클래스에 직접 CoroutineScope 를 만들고 CoroutineScope를 이용해서 새로운 코루틴을 생성한 뒤 그 코루틴 내부에서 veryImportantOperation() 실행되게 함으로써 요구사항을 구현할 수 있다. 이 scope 는 클래스에 inject 되어 사용되어야 한다.

이 스코프를 applicationScope 라고 이름 지을 수 있고, 자식 코루틴의 실패가 다른 코루틴에 영향을 주어서는 안되기 때문에 SupervisorJob 을 사용해야 한다.

class MyApplication : Application() {
  // No need to cancel this scope as it'll be torn down with the process
  val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}

앱이 종료될때 앱 내부의 모든 코루틴이 종료되기 때문에 따로 이 applicationScope 를 취소시켜줄 필요는 없다.

Which coroutine builder to use?

veryImportantOperation 의 유형에 따라 사용해야할 코루틴 빌더의 유형이 달라질 수 있다. launch 나 async

  • 만약 값을 반환해야 하는 경우에는 async 코루틴 빌더를 사용하고 await 를 통해 값을 반환받는다.
  • 만약 반환 값이 없는 경우에는 launch 코루틴 빌더를 사용하고 join 을 통해서 코루틴이 종료될때까지 기다릴 수 있다.
class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      externalScope.launch {
        // if this can throw an exception, wrap inside try/catch
        // or rely on a CoroutineExceptionHandler installed
        // in the externalScope's CoroutineScope
        veryImportantOperation()
      }.join()
    }
  }
}
class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork(): Any { // Use a specific type in Result
    withContext(ioDispatcher) {
      doSomeOtherWork()
      return externalScope.async {
        // Exceptions are exposed when calling await, they will be
        // propagated in the coroutine that called doWork. Watch
        // out! They will be ignored if the calling context cancels.
        veryImportantOperation()
      }.await()
    }
  }
}

doWork 를 호출한 viewModelScope 가 종료된다고 하더라도 veryImportantOperation() 의 실행은 종료되지 않을 것이다. 또한 veryImportantOperation()가 종료되기 전까지는 doWork 함수 호출이 종료되지 않을 것이다.

What about something simpler?

veryImportantOperation 를 처리할 또다른 시도를 해볼 수 있는데 withContext 를 이용해서 extenralScope 의 context 로 변환해주는 것이다.

class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      withContext(externalScope.coroutineContext) {
        veryImportantOperation()
      }
    }
  }
}

그러나 이러한 방법에는 주의해야할 함점이 있다.

  • doWork 를 실행하는 코루틴이 취소되는 경우 veryImportantOperation() 가 완료된 시점이 아니라 다음 suspend 포인트에서 doWork() 취소가 마무리된다.
  • extenralScope 에 설정한 CoroutineExceptionHandlers 가 예외를 잡지 못하는데, withContext 를 사용하는 경우 예외를 재발생 시키기 때문에다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant