Android 앱에서 Kotlin 코루틴 사용

이 Codelab에서는 Android 앱에서 Kotlin 코루틴을 사용하는 방법을 알아봅니다. 새 코루틴은 콜백의 필요성을 줄여 코드를 단순화할 수 있는 백그라운드 스레드를 관리하는 새로운 방법입니다. 코루틴은 데이터베이스 액세스나 네트워크 액세스와 같은 장기 실행 작업의 비동기 콜백을 순차적 코드로 변환하는 Kotlin 기능입니다.

실행할 작업에 관한 코드 스니펫은 아래와 같습니다.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

콜백 기반 코드는 코루틴을 사용하여 순차 코드로 변환됩니다.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

장기 실행 작업에 콜백 스타일을 사용하는 아키텍처 구성요소를 사용하여 빌드된 기존 앱으로 시작합니다.

이 Codelab을 완료하면 앱에서 코루틴을 사용하여 네트워크에서 데이터를 로드할 수 있는 충분한 경험을 얻게 되고 코루틴을 앱에 통합할 수 있게 됩니다. 또한 코루틴 권장사항과 코루틴을 사용하는 코드를 대상으로 테스트를 작성하는 방법을 알아봅니다.

기본 요건

  • 아키텍처 구성요소 ViewModel, LiveData, Repository, Room에 관한 기본 지식
  • 확장 함수 및 람다를 포함한 Kotlin 구문 사용 경험
  • Android에서 기본 스레드, 백그라운드 스레드, 콜백을 비롯한 스레드를 사용하는 방법에 관한 기본적인 이해

실행할 작업

  • 코루틴으로 작성된 코드를 호출하여 결과를 얻습니다.
  • 정지 함수를 사용하여 비동기 코드를 순차 코드로 만듭니다.
  • launchrunBlocking를 사용하여 코드가 실행되는 방식을 제어합니다.
  • suspendCoroutine를 사용하여 기존 API를 코루틴으로 변환하는 기법을 알아보세요.
  • 아키텍처 구성요소와 함께 코루틴 사용
  • 코루틴 테스트를 위한 권장사항을 알아봅니다.

필요한 항목

  • Android 스튜디오 3.5 (Codelab은 다른 버전에서 작동할 수도 있지만 일부 내용이 누락되거나 다르게 표시될 수 있습니다.)

이 Codelab을 진행하는 동안 코드 버그, 문법 오류, 불명확한 문구 등의 문제가 발생하면 Codelab 왼쪽 하단에 있는 오류 신고 링크를 통해 신고해 주세요.

코드 다운로드

다음 링크를 클릭하여 이 Codelab의 모든 코드를 다운로드합니다.

ZIP 파일 다운로드

또는 다음 명령어를 사용하여 명령줄에서 GitHub 저장소를 클론합니다.

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

자주 묻는 질문자주 묻는 질문

먼저 시작 샘플 앱이 어떤 모습인지 살펴보겠습니다. 다음 안내에 따라 Android 스튜디오에서 샘플 앱을 엽니다.

  1. kotlin-coroutines ZIP 파일을 다운로드한 경우 파일의 압축을 풉니다.
  2. Android 스튜디오에서 coroutines-codelab 프로젝트를 엽니다.
  3. start 애플리케이션 모듈을 선택합니다.
  4. execute.pngRun 버튼을 클릭하고 Android Lollipop을 실행해야 하는 에뮬레이터를 선택하거나 Android 기기를 연결합니다 (최소 SDK는 21임). Kotlin 코루틴 화면이 표시됩니다.

이 시작 앱은 스레드를 사용하여 화면을 누른 후 짧은 지연 시간을 늘립니다. 또한 네트워크에서 새 제목을 가져와서 화면에 표시합니다. 지금 사용해 보면 잠시 후에 수치와 메시지가 변경됩니다. 이 Codelab에서는 이 애플리케이션을 코루틴을 사용하도록 변환합니다.

이 앱은 아키텍처 구성요소를 사용하여 MainActivity의 UI 코드를 MainViewModel의 애플리케이션 로직과 분리합니다. 잠시 시간을 내어 프로젝트 구조를 숙지하세요.

  1. MainActivity은 UI를 표시하고 클릭 리스너를 등록하며 Snackbar를 표시할 수 있습니다. MainViewModel에 이벤트를 전달하고 MainViewModelLiveData에 따라 화면을 업데이트합니다.
  2. MainViewModelonMainViewClicked의 이벤트를 처리하고 LiveData.를 사용하여 MainActivity와 통신합니다.
  3. Executors는 백그라운드 스레드에서 작업을 실행할 수 있는 BACKGROUND,를 정의합니다.
  4. TitleRepository는 네트워크에서 결과를 가져와서 데이터베이스에 저장합니다.

프로젝트에 코루틴 추가

Kotlin에서 코루틴을 사용하려면 프로젝트의 build.gradle (Module: app) 파일에 coroutines-core 라이브러리를 포함해야 합니다. Codelab 프로젝트에서는 이미 이 작업을 완료했으므로 Codelab을 완료하기 위해 이 작업을 수행할 필요는 없습니다.

Android에서의 코루틴은 핵심 라이브러리로 사용할 수 있으며 Android 관련 확장 프로그램은 다음과 같습니다.

  • kotlinx-corountines-core - Kotlin에서 코루틴을 사용하는 기본 인터페이스
  • kotlinx-coroutines-android - 코루틴에서 Android 기본 스레드 지원

시작 앱은 이미 build.gradle.에 종속 항목이 포함되어 있습니다. 새 앱 프로젝트를 만들 때 build.gradle (Module: app)를 열고 프로젝트에 코루틴 종속 항목을 추가해야 합니다.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Android에서는 기본 스레드를 차단하지 않는 것이 중요합니다. 기본 스레드는 UI에 관한 모든 업데이트를 처리하는 단일 스레드입니다. 또한 모든 클릭 핸들러와 기타 UI 콜백을 호출하는 스레드도 있습니다. 따라서 최적의 사용자 경험을 보장하기 위해 원활하게 실행되어야 합니다.

앱이 눈에 띄는 일시중지 없이 사용자에게 표시되도록 하려면 기본 스레드가 16ms 이상(약 60fps) 화면을 업데이트해야 합니다. 대용량 JSON 데이터 세트 파싱, 데이터베이스에 데이터 쓰기, 네트워크에서 데이터 가져오기 등 많은 일반적인 작업이 이보다 오래 걸립니다. 따라서 기본 스레드에서 이와 같은 코드를 호출하면 앱이 일시중지되거나 끊기거나 멈출 수 있습니다. 그리고 기본 스레드를 너무 오랫동안 차단하면 앱이 비정상 종료될 수 있으며 Application Not Responding 대화상자가 표시될 수도 있습니다.

아래 동영상을 통해 기본 안전성을 도입하여 코루틴이 Android에서 이 문제를 해결하는 방법을 알아보세요.

콜백 패턴

기본 스레드를 차단하지 않고 장기 실행 작업을 이행하는 한 가지 패턴은 콜백입니다. 콜백을 사용함으로써 백그라운드 스레드에서 장기 실행 작업을 시작할 수 있습니다. 작업이 완료되면 콜백이 호출되어 기본 스레드의 결과를 알립니다.

콜백 패턴의 예를 살펴보겠습니다.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

이 코드는 @UiThread로 주석 처리되므로 기본 스레드에서 실행하기에 충분한 속도로 실행되어야 합니다. 즉, 다음 화면 업데이트가 지연되지 않도록 매우 빠르게 반환해야 합니다. 하지만 slowFetch이 완료되는 데는 몇 초 또는 몇 분이 걸리므로 기본 스레드는 결과를 기다릴 수 없습니다. show(result) 콜백을 사용하면 slowFetch가 백그라운드 스레드에서 실행되고 준비가 되면 결과를 반환할 수 있습니다.

코루틴을 사용하여 콜백 삭제

콜백은 훌륭한 패턴이지만 몇 가지 단점이 있습니다. 콜백을 매우 많이 사용하는 코드는 읽기 어렵고 추론하기가 더 어려워질 수 있습니다. 또한 콜백은 예외와 같은 일부 언어 기능의 사용을 허용하지 않습니다.

Kotlin 코루틴을 사용하면 콜백 기반 코드를 순차 코드로 변환할 수 있습니다. 순차적으로 작성된 코드는 일반적으로 읽기가 더 쉬우며 예외와 같은 언어 기능을 사용할 수도 있습니다.

결국 정확히 동일한 작업을 합니다. 즉, 장기 실행 작업에서 결과를 사용할 수 있을 때까지 기다렸다가 실행을 계속합니다. 하지만 코드에서는 완전히 다르게 보입니다.

키워드 suspend은 코루틴에서 사용할 수 있는 함수 또는 함수 유형을 표시하는 방법입니다. 코루틴이 suspend로 표시된 함수를 호출하면 함수는 일반 함수 호출처럼 차단될 때까지 차단되는 대신 결과가 준비될 때까지 실행을 정지하고, 결과와 함께 중단한 위치에서 다시 시작합니다. 결과를 기다리는 동안 정지된 동안에는 다른 함수 또는 코루틴을 실행할 수 있도록 실행 중인 스레드를 차단 해제합니다.

예를 들어 아래 코드에서 makeNetworkRequest()slowFetch()은 모두 suspend 함수입니다.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

콜백 버전과 마찬가지로 makeNetworkRequest@UiThread로 표시되므로 기본 스레드에서 즉시 반환해야 합니다. 즉, 일반적으로 slowFetch와 같은 차단 메서드를 호출할 수 없습니다. 여기서 suspend 키워드는 놀라운 역할을 합니다.

콜백 기반 코드에 비해 코루틴 코드는 코드가 더 적은 현재 스레드를 차단 해제한 것과 동일한 결과를 얻습니다. 순차 스타일로 인해 여러 콜백을 만들지 않고도 장기 실행 작업을 쉽게 연결할 수 있습니다. 예를 들어 두 네트워크 엔드포인트에서 결과를 가져와서 데이터베이스에 저장하는 코드는 콜백이 없는 코루틴에서 함수로 작성할 수 있습니다. 방법은 다음과 같습니다.

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

다음 섹션에서 코루틴을 샘플 앱에 소개합니다.

이 연습에서는 지연 후 메시지를 표시하는 코루틴을 작성합니다. 시작하려면 Android 스튜디오에서 모듈 start가 열려 있는지 확인하세요.

CoroutineScope 이해

Kotlin에서 모든 코루틴은 CoroutineScope 내에서 실행됩니다. 범위는 전체 작업에 걸쳐 코루틴의 전체 기간을 제어합니다. 범위의 작업을 취소하면 그 범위에서 시작된 코루틴이 모두 취소됩니다. Android에서는 예를 들어 사용자가 Activity 또는 Fragment에서 벗어날 때 범위를 사용하여 실행 중인 모든 코루틴을 취소할 수 있습니다. 범위를 사용하면 기본 디스패처를 지정할 수도 있습니다. 디스패처는 코루틴을 실행하는 스레드를 제어합니다.

UI에서 시작된 코루틴의 경우 일반적으로 Android의 기본 스레드인 Dispatchers.Main에서 시작하는 것이 좋습니다. Dispatchers.Main에 시작된 코루틴이 정지된 동안에는 기본 스레드를 차단하지 않습니다. ViewModel 코루틴은 거의 항상 기본 스레드의 UI를 업데이트하므로 기본 스레드에서 코루틴을 시작하면 스레드 스레드가 추가로 절약됩니다. 기본 스레드에서 시작된 코루틴은 코루틴이 시작된 후 언제든지 디스패처를 전환할 수 있습니다. 예를 들어 다른 디스패처를 사용하여 기본 스레드에서 큰 JSON 결과를 파싱할 수 있습니다.

viewModelScope 사용

AndroidX lifecycle-viewmodel-ktx 라이브러리는 UI 관련 코루틴을 시작하도록 구성된 ViewModel에 CoroutineScope를 추가합니다. 이 라이브러리를 사용하려면 프로젝트의 build.gradle (Module: start) 파일에 라이브러리를 포함해야 합니다. 이 단계는 Codelab 프로젝트에서 이미 완료했습니다.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

라이브러리는 viewModelScopeViewModel 클래스의 확장 함수로 추가합니다. 이 범위는 Dispatchers.Main에 바인딩되며 ViewModel이 삭제되면 자동으로 취소됩니다.

스레드에서 코루틴으로 전환

MainViewModel.kt에서 다음 코드와 함께 다음 TODO를 찾습니다.

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

이 코드는 util/Executor.kt에 정의된 BACKGROUND ExecutorService를 사용하여 백그라운드 스레드에서 실행됩니다. sleep가 현재 스레드에서 차단되므로 기본 스레드에서 호출되면 UI가 정지됩니다. 사용자가 기본 뷰를 클릭한 후 1초 후에 스낵바를 요청합니다.

코드에서 BACKGROUND를 삭제하고 다시 실행하면 이를 확인할 수 있습니다. 로드 중 스피너가 표시되지 않고 1초 후 모든 상태가 최종 상태로 돌아갑니다.

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

updateTaps를 같은 작업을 하는 코루틴 기반 코드로 바꿉니다. launchdelay을 가져와야 합니다.

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

이 코드는 같은 작업을 하며 1초 후에 스낵바를 표시합니다. 하지만 몇 가지 중요한 차이점이 있습니다.

  1. viewModelScope.launch이(가) viewModelScope에서 코루틴을 시작합니다. 즉, viewModelScope에 전달한 작업이 취소되면 이 작업/범위의 모든 코루틴이 취소됩니다. delay가 반환되기 전에 사용자가 활동을 종료한 경우 ViewModel을 제거할 때 onCleared가 호출되면 이 코루틴은 자동으로 취소됩니다.
  2. viewModelScope의 기본 디스패처가 Dispatchers.Main이므로 이 코루틴은 기본 스레드에서 실행됩니다. 다양한 스레드 사용 방법을 나중에 살펴보겠습니다.
  3. delay 함수는 suspend 함수입니다. Android 스튜디오에서는 왼쪽 여백의 아이콘으로 표시됩니다. 이 코루틴은 기본 스레드에서 실행되지만, delay는 1초 동안 스레드를 차단하지 않습니다. 대신 디스패처는 다음 문에서 코루틴이 1초 후에 재개되도록 예약합니다.

실행해보세요. 기본 뷰를 클릭하면 1초 후 스낵바가 표시됩니다.

다음 섹션에서는 이 함수를 테스트하는 방법을 살펴보겠습니다.

이 연습에서는 방금 작성한 코드를 위한 테스트를 작성합니다. 이 연습에서는 kotlinx-coroutines-test 라이브러리를 사용하여 Dispatchers.Main에서 실행되는 코루틴을 테스트하는 방법을 보여줍니다. 이 Codelab의 뒷부분에서는 코루틴과 직접 상호작용하는 테스트를 구현합니다.

기존 코드 검토

androidTest 폴더에서 MainViewModelTest.kt을 엽니다.

MainViewModelTest.kt를 참조하세요.

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

규칙은 JUnit에서 테스트를 실행하기 전과 후에 코드를 실행하는 방법입니다. 다음 두 가지 규칙을 사용하여 오프기기 테스트에서 MainViewModel을 테스트할 수 있습니다.

  1. InstantTaskExecutorRule는 각 작업을 동기식으로 실행하도록 LiveData를 구성하는 JUnit 규칙입니다.
  2. MainCoroutineScopeRule는 이 코드베이스의 맞춤 규칙으로, kotlinx-coroutines-testTestCoroutineDispatcher를 사용하도록 Dispatchers.Main를 구성합니다. 이렇게 하면 테스트용 가상 클록을 발전시키고 코드가 단위 테스트에서 Dispatchers.Main를 사용할 수 있습니다.

setup 메서드에서 테스트 가짜를 사용하여 MainViewModel의 새 인스턴스가 생성됩니다. 이 인스턴스는 실제 네트워크나 데이터베이스를 사용하지 않고 테스트를 작성하는 데 도움이 되도록 시작 코드에 제공된 네트워크 및 데이터베이스의 가짜 구현입니다.

이 테스트에서 가짜는 MainViewModel의 종속 항목을 충족하는 경우에만 필요합니다. 이 Codelab의 뒷부분에서는 코루틴을 지원하도록 가짜를 업데이트합니다.

코루틴을 제어하는 테스트 작성

기본 뷰를 클릭한 후 1초 후에 탭이 업데이트되도록 하는 새 테스트를 추가합니다.

MainViewModelTest.kt를 참조하세요.

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

onMainViewClicked를 호출하면 방금 만든 코루틴이 실행됩니다. 이 테스트는 onMainViewClicked가 호출된 직후 탭 텍스트를 \tapt;0 taps" 상태로 유지하며 1초 후에 "1 taps"로 업데이트되는지 확인합니다.

이 테스트는 가상 시간을 사용하여 onMainViewClicked에서 실행된 코루틴 실행을 제어합니다. MainCoroutineScopeRule를 사용하면 Dispatchers.Main에서 실행되는 코루틴 실행을 일시중지, 다시 시작, 제어할 수 있습니다. 여기서 advanceTimeBy(1_000)를 호출하면 기본 디스패처가 1초 후에 재개되도록 예약된 코루틴을 즉시 실행합니다.

이 테스트는 완전히 확정적이므로 항상 동일한 방식으로 실행됩니다. 또한 Dispatchers.Main에서 실행된 코루틴 실행을 완전히 제어할 수 있으므로 값이 설정될 때까지 1초를 기다리지 않아도 됩니다.

기존 테스트 실행

  1. 편집기에서 클래스 이름 MainViewModelTest를 마우스 오른쪽 버튼으로 클릭하여 컨텍스트 메뉴를 엽니다.
  2. 컨텍스트 메뉴에서 execute.pngRun 'MainViewModelTest'를 선택합니다.
  3. 향후 실행에서는 툴바의 execute.png 버튼 옆에 있는 구성에서 이 테스트 구성을 선택할 수 있습니다. 기본적으로 구성은 MainViewModelTest로 호출됩니다.

테스트 통과가 표시됩니다. 실행하는 데 1초도 걸리지 않습니다.

다음 연습에서는 기존 콜백을 사용하여 코루틴을 사용하는 방법을 알아봅니다.

이 단계에서는 코루틴을 사용하도록 저장소 변환을 시작합니다. 이를 위해 ViewModel, Repository, RoomRetrofit에 코루틴을 추가합니다.

코루틴을 사용하도록 전환하기 전에 아키텍처의 각 부분이 어떤 역할을 하는지 이해하는 것이 좋습니다.

  1. MainDatabaseTitle를 저장하고 로드하는 Room을 사용하여 데이터베이스를 구현합니다.
  2. MainNetwork는 새 제목을 가져오는 네트워크 API를 구현합니다. 이 어댑터는 Retrofit을 사용하여 제목을 가져옵니다. Retrofit는 오류 또는 모의 데이터를 무작위로 반환하도록 구성되지만, 실제 네트워크 요청을 하는 것처럼 동작합니다.
  3. TitleRepository은 네트워크 및 데이터베이스의 데이터를 결합하여 제목을 가져오거나 새로고침하는 단일 API를 구현합니다.
  4. MainViewModel는 화면 상태를 나타내고 이벤트를 처리합니다. 사용자가 화면을 탭할 때 제목을 새로고침하도록 저장소에 지시합니다.

네트워크 요청은 UI 이벤트에 의해 구동되며 이를 기반으로 코루틴을 시작하려고 하므로 코루틴 사용을 시작하는 자연스러운 위치는 ViewModel입니다.

콜백 버전

MainViewModel.kt를 열어 refreshTitle 선언을 확인합니다.

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

이 함수는 사용자가 화면을 클릭할 때마다 호출되며, 저장소가 제목을 새로고침하고 데이터베이스에 새 제목을 작성하도록 합니다.

이 구현은 콜백을 사용하여 몇 가지 작업을 실행합니다.

  • 쿼리를 시작하기 전에 _spinner.value = true와 함께 로드 스피너를 표시합니다.
  • 결과가 나오면 _spinner.value = false를 사용하여 로드 스피너를 삭제합니다.
  • 오류가 발생하면 스낵바에 지시를 표시하고 스피너를 지웁니다.

onCompleted 콜백은 title를 전달하지 않습니다. 모든 제목을 Room 데이터베이스에 쓰기 때문에 UI가 Room가 업데이트된 LiveData'를 관찰하여 현재 제목으로 업데이트됩니다.

코루틴 업데이트에서는 똑같은 동작을 유지합니다. Room 데이터베이스와 같이 식별 가능한 데이터 소스를 사용하여 UI를 자동으로 최신 상태로 유지하는 좋은 패턴입니다.

코루틴 버전

코루틴으로 refreshTitle을 다시 작성해 보겠습니다.

바로 필요하기 때문에 저장소(TitleRespository.kt)에 빈 정지 함수를 만듭니다. suspend 연산자를 사용하여 코루틴과 호환된다고 Kotlin에 알리는 새 함수를 정의합니다.

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

이 Codelab을 완료하면 Retrofit과 Room을 사용하여 새 제목을 가져오고 코루틴을 사용하여 데이터베이스에 쓰도록 업데이트합니다. 지금은 이를 위해 500밀리초의 시간을 소비한 다음 작업을 계속합니다.

MainViewModel에서 refreshTitle의 콜백 버전을 새 코루틴을 시작하는 버전으로 바꿉니다.

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

이 함수를 단계별로 살펴보겠습니다.

viewModelScope.launch {

코루틴이 탭 수를 업데이트하는 것과 마찬가지로 먼저 viewModelScope에서 새 코루틴을 실행합니다. 이렇게 하면 OK인 Dispatchers.Main을 사용합니다. refreshTitle이 네트워크 요청과 데이터베이스 쿼리를 실행하더라도 코루틴을 사용하여 기본 안전 인터페이스를 노출할 수 있습니다. 즉, 기본 스레드에서 안전하게 호출하세요.

viewModelScope를 사용하고 있으므로 사용자가 이 화면에서 벗어나면 이 코루틴에서 시작된 작업이 자동으로 취소됩니다. 즉, 추가 네트워크 요청을 하거나 데이터베이스를 쿼리하지 않습니다.

다음 몇 줄의 코드는 실제로 repository에서 refreshTitle를 호출합니다.

try {
    _spinner.value = true
    repository.refreshTitle()
}

이 코루틴은 어떤 작업도 로드하기 전에 로드 스피너를 시작합니다. 그런 다음 일반 함수처럼 refreshTitle를 호출합니다. 그러나 refreshTitle는 정지 함수이므로 일반 함수와 다르게 실행됩니다.

콜백을 전달할 필요가 없습니다. 코루틴은 refreshTitle에서 재개될 때까지 정지됩니다. 일반 차단 함수 호출과 유사하게 보이지만 네트워크 및 데이터베이스 쿼리가 완료될 때까지 자동으로 대기하고 기본 스레드를 차단하지 않습니다.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

정지 함수의 예외는 일반 함수의 오류와 마찬가지로 작동합니다. 정지 함수에서 오류가 발생하면 호출자에 발생합니다. 따라서 완전히 실행되지 않더라도 일반 try/catch 블록을 사용하여 처리할 수 있습니다. 이 기능은 모든 콜백에 대해 커스텀 오류 처리를 빌드하는 대신 오류 처리를 위해 내장된 언어 지원을 사용할 수 있으므로 유용합니다.

그리고 코루틴에서 예외가 발생하면 코루틴은 기본적으로 상위 요소를 취소합니다. 즉, 여러 관련 작업을 함께 쉽게 취소할 수 있습니다.

그런 다음 마지막으로 블록에서 쿼리가 실행된 후에 스피너가 항상 꺼지도록 할 수 있습니다.

start 구성을 선택한 다음 execute.png를 눌러 애플리케이션을 다시 실행합니다. 아무 곳이나 탭하면 로드 스피너가 표시됩니다. Google에서 아직 네트워크 또는 데이터베이스를 연결하지 않았으므로 제목은 그대로 유지됩니다.

다음 연습에서는 실제로 작동하도록 저장소를 업데이트합니다.

이 연습에서는 TitleRepository의 작업 버전을 구현하기 위해 코루틴이 실행되는 스레드를 전환하는 방법을 알아봅니다.

refreshTitle에서 기존 콜백 코드 검토

TitleRepository.kt를 열고 기존 콜백 기반 구현을 검토합니다.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

TitleRepository.kt에서 refreshTitleWithCallbacks 메서드는 로드와 오류 상태를 호출자에 전달하는 콜백으로 구현됩니다.

이 함수는 새로고침을 구현하기 위해 몇 가지 작업을 합니다.

  1. BACKGROUND ExecutorService 대화목록을 다른 대화목록으로 전환
  2. 차단 execute() 메서드를 사용하여 fetchNextTitle 네트워크 요청을 실행합니다. 그러면 현재 스레드에서 네트워크 요청이 실행됩니다. 이 경우에는 BACKGROUND의 스레드 중 하나입니다.
  3. 결과가 성공하면 insertTitle를 사용하여 데이터베이스에 저장하고 onCompleted() 메서드를 호출합니다.
  4. 결과가 성공하지 못했거나 예외가 있는 경우 onError 메서드를 호출하여 호출 실패에 관해 발신자에게 알립니다.

이 콜백 기반 구현은 기본 스레드를 차단하지 않으므로 기본 안전성이 보장됩니다. 그러나 작업이 완료되면 콜백을 사용하여 발신자에게 알려야 합니다. 또한 전환한 BACKGROUND 스레드에서도 콜백을 호출합니다.

코루틴에서 차단 호출 호출

네트워크 또는 데이터베이스에 코루틴을 도입하지 않고 코루틴을 사용하여 이 코드를 기본 안전성으로 설정할 수 있습니다. 이렇게 하면 콜백을 제거하고 결과를 호출한 스레드로 다시 전달할 수 있습니다.

코루틴 내에서 대량 목록 정렬 및 필터링, 디스크에서 읽기와 같은 차단 또는 CPU 집약적인 작업을 실행해야 할 때 언제든지 이 패턴을 사용할 수 있습니다.

디스패처 간에 전환하기 위해 코루틴은 withContext를 사용합니다. withContext를 호출하면 람다 전용의 다른 디스패처로 전환되었다가 이 람다 결과와 함께 호출했던 디스패처로 돌아옵니다.

기본적으로 Kotlin 코루틴은 Main, IO, Default의 세 가지 디스패처를 제공합니다. IO 디스패처는 네트워크나 디스크에서 읽기와 같은 IO 작업에 최적화되어 있고, 기본 디스패처는 CPU 집약적인 작업에 최적화되어 있습니다.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

이 구현은 네트워크 및 데이터베이스에 대한 차단 호출을 사용하지만 여전히 콜백 버전보다 조금 더 간단합니다.

이 코드는 계속 차단 호출을 사용합니다. execute()insertTitle(...)을 호출하면 모두 이 코루틴이 실행 중인 스레드가 차단됩니다. 그러나 withContext를 사용하여 Dispatchers.IO로 전환하면 IO 디스패처의 스레드 중 하나가 차단됩니다. Dispatchers.Main에서 실행되었을 수 있는 이 메서드를 호출한 코루틴은 withContext 람다가 완료될 때까지 정지됩니다.

콜백 버전과 비교할 때 다음과 같은 두 가지 중요한 차이점이 있습니다.

  1. withContext는 호출한 Dispatcher(이 경우에는 Dispatchers.Main)에 다시 결과를 반환합니다. BACKGROUND 실행기 서비스의 스레드에서 콜백을 호출한 콜백 버전입니다.
  2. 호출자는 이 함수에 콜백을 전달할 필요가 없습니다. 사용자는 정지에 의존하여 결과나 오류를 가져올 수 있습니다.

앱 다시 실행

앱을 다시 실행하면 새로운 코루틴 기반 구현이 네트워크에서 결과를 로드하는 것을 확인할 수 있습니다.

다음 단계에서는 코루틴을 Room과 Retrofit에 통합합니다.

코루틴 통합을 계속 진행하기 위해 Room의 안정적인 버전과 Retrofit에서 정지 함수 지원을 사용한 다음 정지 함수를 사용하여 방금 작성한 코드를 간소화합니다.

Room의 코루틴

먼저 MainDatabase.kt를 열고 insertTitle를 정지 함수로 만듭니다.

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

이렇게 하면 Room에서 쿼리를 기본 안전성으로 설정하고 백그라운드 스레드에서 자동으로 실행합니다. 그러나 코루틴 내에서만 이 쿼리를 호출할 수 있습니다.

Room에서 코루틴을 사용하기만 하면 됩니다. 정말 멋집니다.

Retrofit의 코루틴

다음으로 Retrofit과 코루틴을 통합하는 방법을 알아보겠습니다. MainNetwork.kt를 열고 fetchNextTitle를 정지 함수로 변경합니다.

기본 네트워크.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Retrofit과 함께 정지 함수를 사용하려면 다음 두 가지 작업을 해야 합니다.

  1. 함수에 정지 수정자 추가
  2. 반환 유형에서 Call 래퍼를 삭제합니다. 여기서는 String를 반환하지만 복잡한 json 지원 유형도 반환할 수 있습니다. Retrofit의 전체 Result에 계속 액세스 권한을 제공하려면 정지 함수에서 String 대신 Result<String>를 반환하면 됩니다.

Retrofit은 정지 함수를 기본 안전성으로 자동 설정해 Dispatchers.Main에서 직접 호출할 수 있습니다.

Room 및 Retrofit 사용

Room과 Retrofit이 정지 함수를 지원하므로 이제 저장소에서 사용할 수 있습니다. TitleRepository.kt를 열고 차단 함수와 비교하여 정지 함수를 사용하면 로직이 어떻게 크게 간소화되는지 알아보세요.

제목Repository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

와, 많은 짧아요. 어떻게 된 것일까요? 정지 및 재개에 의존하면 코드가 훨씬 더 짧아집니다. Retrofit을 사용하면 여기에서 Call 대신 String 또는 User 객체와 같은 반환 유형을 사용할 수 있습니다. 정지 함수 내에서 Retrofit가 백그라운드 스레드에서 네트워크 요청을 실행하고 호출이 완료되면 코루틴을 재개할 수 있으므로 안전합니다.

또한 withContext을 삭제했습니다. Room과 Retrofit은 모두 기본 안전성을 갖춘 정지 함수를 제공하므로 Dispatchers.Main에서 이 비동기 작업을 조정할 수 있습니다.

컴파일러 오류 수정

코루틴으로 이동하려면 일반 함수에서 정지 함수를 호출할 수 없으므로 함수의 서명을 변경해야 합니다. 이 단계에서 suspend 수정자를 추가하면 실제 프로젝트에서 정지하도록 함수를 변경하면 어떤 일이 발생하는지 보여주는 몇 가지 컴파일러 오류가 발생했습니다.

프로젝트를 살펴보고 생성된 정지로 함수를 변경하여 컴파일러 오류를 수정합니다. 다음은 각 항목의 간단한 해결 방법입니다.

TestingFakes.kt

새로운 정지 수정자를 지원하도록 테스트 모조를 업데이트합니다.

TitleDaoFake

  1. Alt + Enter를 눌러 헤이라시의 모든 함수에 정지 수정자를 추가합니다.

MainNetworkFake

  1. Alt + Enter를 눌러 헤이라시의 모든 함수에 정지 수정자를 추가합니다.
  2. fetchNextTitle를 이 함수로 대체
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Alt + Enter를 눌러 헤이라시의 모든 함수에 정지 수정자를 추가합니다.
  2. fetchNextTitle를 이 함수로 대체
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • refreshTitleWithCallbacks 함수는 더 이상 사용되지 않으므로 삭제합니다.

앱 실행

앱이 다시 실행되면 컴파일되면 ViewModel에서 Room 및 Retrofit에 코루틴을 사용하여 데이터를 로드하는 것을 확인할 수 있습니다.

축하합니다. 이 앱을 코루틴으로 완전히 전환했습니다. 마지막으로 오늘 배운 내용을 테스트하는 방법에 대해 알아보겠습니다.

이 연습에서는 suspend 함수를 직접 호출하는 테스트를 작성합니다.

refreshTitle는 공개 API로 노출되므로 직접 테스트되므로 테스트에서 코루틴 함수를 호출하는 방법을 보여줍니다.

다음은 이전 실습에서 구현한 refreshTitle 함수입니다.

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

정지 함수를 호출하는 테스트 작성

TODOS가 두 개 있는 test 폴더에서 TitleRepositoryTest.kt을 엽니다.

첫 번째 테스트 whenRefreshTitleSuccess_insertsRows에서 refreshTitle를 호출해 봅니다.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

refreshTitlesuspend 함수이므로 Kotlin은 코루틴이나 다른 정지 함수를 제외한 다른 함수를 호출하는 방법을 알 수 없으며, "Suspend 함수 refreshTitle은 코루틴이나 다른 정지 함수에서만 호출해야 합니다.

테스트 실행기는 코루틴에 관해 알지 못하므로 이 테스트를 정지 함수로 만들 수 없습니다. ViewModel에서와 같이 CoroutineScope를 사용하여 코루틴을 launch할 수 있지만, 테스트는 반환하기 전에 코루틴을 완료해야 합니다. 테스트 함수가 반환되면 테스트가 종료됩니다. launch로 시작하는 코루틴은 비동기 코드로, 나중에 특정 시점에 완료될 수 있습니다. 따라서 비동기 코드를 테스트하려면 코루틴이 완료될 때까지 테스트에 대기하도록 테스트에 알리는 방법이 필요합니다. launch는 비차단 호출이므로 즉시 반환되며 함수가 반환된 후에도 코루틴을 계속 실행할 수 있습니다. 이는 테스트에서 사용할 수 없습니다. 예를 들면 다음과 같습니다.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

이 테스트는 때때로 실패합니다. launch 호출은 즉시 반환되고 테스트 사례의 나머지 부분과 동시에 실행됩니다. 테스트에서는 refreshTitle가 아직 실행되었는지 알 수 없으며, 데이터베이스가 업데이트되었는지 확인하는 등의 어설션이 취약합니다. 또한 refreshTitle에서 예외가 발생해도 테스트 호출 스택에서 발생하지 않습니다. 대신 GlobalScope의 포착되지 않은 예외 핸들러에 발생합니다.

kotlinx-coroutines-test 라이브러리에는 정지 함수를 호출하는 동안 차단하는 runBlockingTest 함수가 있습니다. runBlockingTest가 정지 함수를 호출하거나 새 코루틴을 launches하는 경우 기본적으로 즉시 실행됩니다. 이를 정지 함수 및 코루틴을 일반 함수 호출로 변환하는 방법으로 생각할 수 있습니다.

또한 runBlockingTest는 포착되지 않은 예외를 다시 발생시킵니다. 이를 통해 코루틴이 예외를 발생시키는 경우 더 쉽게 테스트할 수 있습니다.

하나의 코루틴으로 테스트 구현

runBlockingTestrefreshTitle 호출을 래핑하고 subject.refreshTitle()에서 GlobalScope.launch 래퍼를 삭제합니다.

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

이 테스트에서는 제공된 모조를 사용하여 refreshTitle에서 데이터베이스에 'OK'가 삽입되었는지 확인합니다.

테스트가 runBlockingTest를 호출하면 runBlockingTest에 의해 시작된 코루틴이 완료될 때까지 차단됩니다. 그런 다음 내부에서 refreshTitle를 호출할 때 일반 정지 및 재개 메커니즘을 사용하여 데이터베이스 행이 모조에 추가될 때까지 기다립니다.

테스트 코루틴이 완료되면 runBlockingTest가 반환됩니다.

시간 제한 테스트 작성

네트워크 요청에 짧은 제한 시간을 추가하려고 합니다. 테스트를 먼저 작성한 후 제한 시간을 구현해 보겠습니다. 새 테스트를 만듭니다.

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

이 테스트에서는 제공된 가짜 MainNetworkCompletableFake를 사용합니다. 가짜는 테스트가 계속될 때까지 호출자를 정지하도록 설계된 네트워크 가짜입니다. refreshTitle에서 네트워크 요청을 시도하면 시간 초과를 테스트하려고 하므로 영구적으로 중단됩니다.

그런 다음 별도의 코루틴을 실행하여 refreshTitle을 호출합니다. 이는 제한 시간 테스트의 핵심 부분이며, runBlockingTest에서 만든 것과 다른 코루틴에서 시간 제한이 발생해야 합니다. 그렇게 하면 다음 줄인 advanceTimeBy(5_000)를 호출하여 시간을 5초 앞당기고 다른 코루틴이 시간 초과되도록 할 수 있습니다.

이는 완전한 시간 제한 테스트이며 시간 제한을 구현하면 통과됩니다.

지금 실행하면 다음과 같이 됩니다.

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

runBlockingTest의 기능 중 하나는 테스트가 완료된 후에 코루틴을 유출할 수 없다는 점입니다. 출시 코루틴과 같이 완료되지 않은 코루틴이 있으면 테스트가 끝날 때 테스트가 실패합니다.

시간 제한 추가하기

TitleRepository를 열고 네트워크 가져오기에 5초 시간 제한을 추가합니다. 다음과 같이 withTimeout 함수를 사용하면 됩니다.

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

테스트를 실행합니다. 테스트를 실행하면 모든 테스트를 통과한 것을 확인할 수 있습니다.

다음 연습에서는 코루틴을 사용하여 고차 함수를 작성하는 방법을 알아봅니다.

이 연습에서는 일반 데이터 로드 함수를 사용하도록 MainViewModel에서 refreshTitle를 리팩터링합니다. 코루틴을 사용하는 고차 함수를 빌드하는 방법을 알아봅니다.

refreshTitle의 현재 구현은 작동하지만 항상 스피너를 표시하는 일반 데이터 로드 코루틴을 만들 수 있습니다. 이는 여러 이벤트에 대한 응답으로 데이터를 로드하는 코드베이스에서 유용할 수 있으며 로드 스피너가 일관되게 표시되도록 하려고 합니다.

repository.refreshTitle()를 제외한 모든 행에서 현재 구현을 검토하는 것은 스피너를 표시하고 오류를 표시하기 위해 상용구입니다.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

고차 함수에서 코루틴 사용

MainViewModel.kt에 다음 코드를 추가합니다.

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

이제 이 고차 함수를 사용하도록 refreshTitle()를 리팩터링합니다.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

로드 스피너의 표시와 오류 표시에 관한 로직을 추상화하여 데이터를 로드하는 데 필요한 실제 코드를 단순화했습니다. 스피너를 표시하거나 오류를 표시하는 것은 모든 데이터 로드에 쉽게 일반화되는 반면, 실제 데이터 소스와 대상은 매번 지정해야 합니다.

이 추상화를 빌드하기 위해 launchDataLoad는 정지 람다인 block 인수를 사용합니다. 정지 람다를 사용하여 정지 함수를 호출할 수 있습니다. Kotlin이 이 Codelab에서 사용한 코루틴 빌더 launchrunBlocking를 구현하는 방법입니다.

// suspend lambda

block: suspend () -> Unit

정지 람다를 만들려면 suspend 키워드로 시작합니다. 함수 화살표와 반환 유형 Unit가 선언을 완료합니다.

자체적으로 정지 람다를 선언할 필요가 없지만 이렇게 반복 로직을 캡슐화하는 추상화를 만들면 유용할 수 있습니다.

이 연습에서는 WorkManager의 코루틴 기반 코드를 사용하는 방법을 알아봅니다.

WorkManager란?

Android에는 지연 가능한 백그라운드 작업을 위한 다양한 옵션이 있습니다. 이 연습에서는 WorkManager를 코루틴과 통합하는 방법을 설명합니다. WorkManager는 지연 가능한 백그라운드 작업을 지원하는 호환 가능하고 유연하며 간단한 라이브러리입니다. WorkManager는 Android의 이러한 사용 사례에 권장되는 솔루션입니다.

WorkManager는 상황별 실행과 보장된 실행을 조합하여 적용해야 하는 백그라운드 작업을 위한 아키텍처 구성요소로서 Android Jetpack의 일부입니다. 상황별 실행을 적용하면 WorkManager가 최대한 빨리 백그라운드 작업을 실행합니다. 보장된 실행을 적용하면 WorkManager가 사용자가 앱을 벗어난 경우를 비롯한 다양한 상황에서 로직을 처리하여 작업을 시작합니다.

따라서 WorkManager는 최종적으로 완료해야 하는 작업에 적합합니다.

WorkManager는 아래와 같은 작업에 사용하는 것이 적합합니다.

  • 로그 업로드
  • 이미지에 필터 적용 및 이미지 저장
  • 주기적으로 로컬 데이터를 네트워크와 동기화

WorkManager와 함께 코루틴 사용

WorkManager는 다양한 사용 사례에 따라 기본 ListanableWorker 클래스의 다양한 구현을 제공합니다.

가장 간단한 Worker 클래스를 사용하면 WorkManager에서 동기 작업을 실행할 수 있습니다. 하지만 지금까지 코루틴을 사용하고 함수를 정지하도록 코드베이스를 변환해 왔으므로 WorkManager를 사용하는 가장 좋은 방법은 doWork() 함수를 정지 함수로 정의할 수 있는 CoroutineWorker 클래스를 사용하는 것입니다.

시작하려면 RefreshMainDataWork 앱을 엽니다. 이미 CoroutineWorker를 확장하므로 doWork를 구현해야 합니다.

suspend doWork 함수 내부에서 refreshTitle()를 호출하여 적절한 결과를 반환합니다.

TODO를 완료하면 코드는 다음과 같습니다.

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

CoroutineWorker.doWork()는 정지 함수입니다. 더 간단한 Worker 클래스와 달리 이 코드는 WorkManager 구성에 지정된 Executor에서 실행되지 않지만 대신 coroutineContext 멤버 (기본적으로 Dispatchers.Default)의 디스패처를 사용합니다.

CoroutineWorker 테스트

테스트 없이는 모든 코드베이스가 완성되어야 합니다.

WorkManager를 사용하면 Worker 클래스를 테스트하는 몇 가지 방법을 사용할 수 있습니다. 기존 테스트 인프라에 관해 자세히 알아보려면 문서를 참고하세요.

WorkManager v2.1에서는 ListenableWorker 클래스를 테스트하는 더 간단한 방법을 지원할 수 있는 새로운 API 세트를 도입합니다(따라서 CoroutineWorker). 코드에서는 새로운 API 중 하나인 TestListenableWorkerBuilder를 사용합니다.

새 테스트를 추가하려면 androidTest 폴더 아래 RefreshMainDataWorkTest 파일을 업데이트합니다.

파일의 내용은 다음과 같습니다.

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

테스트에 앞서 가짜 네트워크를 삽입할 수 있도록 WorkManager에 팩토리에 관해 알립니다.

테스트 자체는 TestListenableWorkerBuilder를 사용하여 작업자를 만든 후 startWork() 메서드 호출을 실행할 수 있습니다.

WorkManager는 코루틴을 사용하여 API 디자인을 간소화하는 방법의 한 예에 불과합니다.

이 Codelab에서는 앱에서 코루틴을 사용하는 데 필요한 기본사항을 설명했습니다.

다루는 내용은 다음과 같습니다.

  • 비동기 프로그래밍을 간소화하기 위해 UI 및 WorkManager 작업에서 Android 앱에 코루틴을 통합하는 방법
  • ViewModel 내의 코루틴을 사용하여 기본 스레드를 차단하지 않고 네트워크에서 데이터를 가져와 데이터베이스에 저장하는 방법
  • ViewModel가 완료되면 모든 코루틴을 취소하는 방법

코루틴 기반 코드의 테스트에서는 동작을 테스트하고 테스트에서 suspend 함수를 직접 호출하는 방법을 모두 다루었습니다.

자세히 알아보기

'Kotlin Flow 및 LiveData를 사용하는 고급 코루틴' Codelab에서 Android의 고급 코루틴 사용법을 알아보세요.

Kotlin 코루틴에는 이 Codelab에서 다루지 않은 많은 기능이 있습니다. Kotlin 코루틴에 관해 자세히 알아보려면 JetBrains에서 게시한 코루틴 가이드를 참고하세요. 'Kotlin 코루틴으로 앱 성능 향상'에서도 Android의 코루틴 사용 패턴을 자세히 확인할 수 있습니다.