Android 스튜디오의 코루틴 소개

1. 시작하기 전에

이전 Codelab에서는 코루틴에 관해 알아봤습니다. 코루틴을 사용하여 동시 실행 코드를 작성하는 데 Kotlin 플레이그라운드를 사용했습니다. 이 Codelab에서는 Android 앱 및 앱의 수명 주기 내에서 코루틴에 관한 지식을 적용해 봅니다. 새 코루틴을 동시에 실행하는 코드를 추가하고 이를 테스트하는 방법을 알아봅니다.

기본 요건

  • 함수와 람다를 비롯한 Kotlin 언어 기본사항에 관한 지식
  • Jetpack Compose로 레이아웃을 빌드할 수 있는 능력
  • Kotlin으로 단위 테스트를 작성할 수 있는 능력(ViewModel 단위 테스트 작성 Codelab 참고)
  • 스레드 및 동시 실행 작동 방식에 관한 지식
  • 코루틴 및 CoroutineScope에 관한 기본 지식

빌드할 항목

  • 두 플레이어 간의 레이스 진행률을 시뮬레이션하는 Race Tracker 앱. 이 앱을 통해 코루틴의 다양한 측면을 실험하고 자세히 알아볼 수 있습니다.

학습할 내용

  • Android 앱 수명 주기에서 코루틴 사용
  • 구조화된 동시 실행의 원칙
  • 단위 테스트를 작성하여 코루틴을 테스트하는 방법

필요한 항목

  • Android 스튜디오의 최신 안정화 버전

2. 앱 개요

Race Tracker 앱은 레이스를 하는 두 플레이어를 시뮬레이션합니다. 앱 UI는 두 가지 버튼(Start/Pause, Reset)과 플레이어의 진행률을 보여주는 두 개의 진행률 표시줄로 구성됩니다. 플레이어 1과 2는 레이스를 서로 다른 속도로 '달리도록' 설정되어 있습니다. 레이스가 시작되면 플레이어 2가 플레이어 1보다 2배 빠르게 달립니다.

앱에서 코루틴을 사용하여 다음을 확인합니다.

  • 두 플레이어가 모두 동시에 '레이스를 합니다'.
  • 앱 UI가 반응형이며 진행률 표시줄이 레이스 중에 증가합니다.

시작 코드에는 Race Tracker 앱의 UI 코드가 준비되어 있습니다. 이 Codelab 부분의 주요 초점은 Android 앱 내 Kotlin 코루틴을 익히는 것입니다.

시작 코드 가져오기

시작하려면 시작 코드를 다운로드하세요.

GitHub 저장소를 클론하여 코드를 가져와도 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-race-tracker.git
$ cd basic-android-kotlin-compose-training-race-tracker
$ git checkout starter

Race Tracker GitHub 저장소에서 시작 코드를 찾을 수 있습니다.

시작 코드 둘러보기

Start 버튼을 클릭하여 레이스를 시작할 수 있습니다. 레이스가 진행되는 동안 Start 버튼의 텍스트가 Pause로 변경됩니다.

2ee492f277625f0a.png

언제든지 이 버튼을 사용하여 레이스를 일시중지하거나 다시 시작할 수 있습니다.

50e992f4cf6836b7.png

레이스가 시작되면 상태 표시기를 통해 각 플레이어의 진행률을 확인할 수 있습니다. 구성 가능한 StatusIndicator 함수는 각 플레이어의 진행 상태를 표시합니다. LinearProgressIndicator 컴포저블을 사용하여 진행률 표시줄을 표시합니다. 코루틴을 사용하여 진행률 값을 업데이트하게 됩니다.

79cf74d82eacae6f.png

RaceParticipant는 진행률 증분을 위한 데이터를 제공합니다. 이 클래스는 각 플레이어의 상태 홀더이며 참가자의 name, 레이스를 완료하기 위해 도달할 maxProgress, 진행률 증분 간 지연 시간, 레이스의 currentProgress, initialProgress를 유지합니다.

다음 섹션에서는 코루틴을 사용하여 앱 UI를 차단하지 않고 레이스 진행률을 시뮬레이션하는 기능을 구현합니다.

3. 레이스 진행률 구현

플레이어의 currentProgress를 레이스의 총 진행률을 반영하는 maxProgress와 비교하고 delay() 정지 함수를 사용하여 진행률 증분 사이에 약간의 지연을 추가하는 run() 함수가 필요합니다. 이 함수는 다른 정지 함수 delay()를 호출하므로 suspend 함수여야 합니다. 또한 이 Codelab의 후반부에 코루틴에서 이 함수를 호출합니다. 함수를 구현하려면 다음 단계를 따르세요.

  1. 시작 코드의 일부인 RaceParticipant 클래스를 엽니다.
  2. RaceParticipant 클래스 내에서 새 suspend 함수 run()을 정의합니다.
class RaceParticipant(
    ...
) {
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        
    }
    ...
}
  1. 레이스 진행률을 시뮬레이션하려면 currentProgressmaxProgress 값(100으로 설정됨)에 도달할 때까지 실행되는 while 루프를 추가합니다.
class RaceParticipant(
    ...
    val maxProgress: Int = 100,
    ...
) {
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        while (currentProgress < maxProgress) {
            
        }
    }
    ...
}
  1. currentProgress 값은 initialProgress(0)로 설정되어 있습니다. 참가자의 진행률을 시뮬레이션하려면 while 루프 내부의 progressIncrement 속성 값만큼 currentProgress 값을 증분합니다. progressIncrement의 기본값은 1입니다.
class RaceParticipant(
    ...
    val maxProgress: Int = 100,
    ...
    private val progressIncrement: Int = 1,
    private val initialProgress: Int = 0
) {
    ...
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        while (currentProgress < maxProgress) {
            currentProgress += progressIncrement
        }
    }
}
  1. 레이스에서 다양한 진행률 간격을 시뮬레이션하려면 delay() 정지 함수를 사용합니다. progressDelayMillis 속성의 값을 인수로 전달합니다.
suspend fun run() {
    while (currentProgress < maxProgress) {
        delay(progressDelayMillis)
        currentProgress += progressIncrement
    }
}

방금 추가한 코드를 보면 Android 스튜디오의 delay() 함수 호출 왼쪽에 아이콘이 표시됩니다(아래 스크린샷 참고). 11b5df57dcb744dc.png

이 아이콘은 함수가 정지되었다가 나중에 재개될 수 있는 정지 지점을 나타냅니다.

다음 다이어그램과 같이 코루틴이 지연 시간을 완료하기 위해 기다리는 동안에는 기본 스레드가 차단되지 않습니다.

a3c314fb082a9626.png

코루틴은 원하는 간격 값으로 delay() 함수를 호출한 후 실행을 정지하지만 차단하지는 않습니다. 지연이 완료되면 코루틴은 실행을 재개하고 currentProgress 속성의 값을 업데이트합니다.

4. 레이스 시작

사용자가 Start 버튼을 누르면 개발자는 두 플레이어의 각 인스턴스에서 run() 정지 함수를 호출하여 '레이스를 시작'해야 합니다. 이렇게 하려면 run() 함수를 호출하는 코루틴을 실행해야 합니다.

코루틴을 실행하여 레이스를 트리거할 때는 두 참가자의 다음 측면을 확인해야 합니다.

  • Start 버튼을 클릭하자마자 달리기를 시작합니다. 즉, 코루틴이 실행됩니다.
  • Pause 또는 Reset 버튼을 클릭하면 각각 달리기를 일시중지하거나 중지합니다. 즉, 코루틴이 취소됩니다.
  • 사용자가 앱을 닫으면 취소가 적절히 관리됩니다. 즉, 모든 코루틴이 취소되고 수명 주기에 결합됩니다.

첫 번째 Codelab에서는 정지 함수는 다른 정지 함수에서만 호출할 수 있다는 사실을 알아보았습니다. 컴포저블 내에서 안전하게 정지 함수를 호출하려면 LaunchedEffect() 컴포저블을 사용해야 합니다. LaunchedEffect() 컴포저블은 컴포지션에 유지되는 한 제공된 정지 함수를 계속 실행합니다. 구성 가능한 LaunchedEffect() 함수를 사용하여 아래의 모든 항목을 실행할 수 있습니다.

  • LaunchedEffect() 컴포저블을 사용하면 컴포저블에서 정지 함수를 안전하게 호출할 수 있습니다.
  • LaunchedEffect() 함수가 컴포지션을 시작하면 매개변수로 전달된 코드 블록으로 코루틴이 실행됩니다. 컴포지션에 유지되는 동안에는 제공된 정지 함수를 실행합니다. 사용자가 RaceTracker 앱에서 Start 버튼을 클릭하면 LaunchedEffect()가 컴포지션을 시작하고 코루틴을 실행하여 진행률을 업데이트합니다.
  • 코루틴은 LaunchedEffect()가 컴포지션을 종료하면 취소됩니다. 앱에서 사용자가 Reset/Pause 버튼을 클릭하면 LaunchedEffect()가 컴포지션에서 삭제되고 기본 코루틴이 취소됩니다.

RaceTracker 앱의 경우 디스패처를 명시적으로 제공할 필요가 없습니다. LaunchedEffect()에서 처리하기 때문입니다.

레이스를 시작하려면 각 참가자의 run() 함수를 호출하고 다음 단계를 실행하세요.

  1. com.example.racetracker.ui 패키지에 있는 RaceTrackerApp.kt 파일을 엽니다.
  2. RaceTrackerApp() 컴포저블로 이동하여 raceInProgress 정의 다음 줄에서 LaunchedEffect() 컴포저블 호출을 추가합니다.
@Composable
fun RaceTrackerApp() {
    ...
    var raceInProgress by remember { mutableStateOf(false) }

    LaunchedEffect {
    
    }
    RaceTrackerScreen(...)
}
  1. playerOne 또는 playerTwo 인스턴스가 다른 인스턴스로 교체되는 경우 LaunchedEffect()가 기본 코루틴을 취소하고 다시 실행해야 하도록 하려면 playerOneplayerTwo 객체를 LaunchedEffectkey로 추가합니다. 텍스트 값이 변경될 때 Text() 컴포저블이 재구성되는 방식과 마찬가지로 LaunchedEffect()의 키 인수가 변경되면 기본 코루틴이 취소되고 다시 실행됩니다.
LaunchedEffect(playerOne, playerTwo) {
}
  1. playerOne.run()playerTwo.run() 함수 호출을 추가합니다.
@Composable
fun RaceTrackerApp() {
    ...
    var raceInProgress by remember { mutableStateOf(false) }

    LaunchedEffect(playerOne, playerTwo) {
        playerOne.run()
        playerTwo.run()
    }
    RaceTrackerScreen(...)
}
  1. if 조건으로 LaunchedEffect() 블록을 래핑합니다. 이 상태의 초깃값은 false입니다. 사용자가 Start 버튼을 클릭하고 LaunchedEffect()가 실행되면 raceInProgress 상태 값이 true로 업데이트됩니다.
if (raceInProgress) {
    LaunchedEffect(playerOne, playerTwo) {
        playerOne.run()
        playerTwo.run() 
    }
}
  1. 레이스를 완료하려면 raceInProgress 플래그를 false로 업데이트합니다. 이 값은 사용자가 Pause를 클릭할 때도 false로 설정됩니다. 이 값이 false로 설정되면 LaunchedEffect()는 실행된 모든 코루틴이 취소되도록 합니다.
LaunchedEffect(playerOne, playerTwo) {
    playerOne.run()
    playerTwo.run()
    raceInProgress = false 
}
  1. 앱을 실행하고 Start를 클릭합니다. 다음 동영상과 같이 플레이어 1은 플레이어 2가 달리기를 시작하기 전에 레이스를 완료합니다.

fa0630395ee18f21.gif

이는 공정한 레이스로 보이지 않습니다. 다음 섹션에서는 두 플레이어가 동시에 레이스를 할 수 있도록 동시 실행 작업을 실행하고 개념을 이해하며 이 동작을 구현하는 방법을 알아봅니다.

5. 구조화된 동시 실행

코루틴을 사용하여 코드를 작성하는 방식을 구조화된 동시 실행이라고 합니다. 이러한 프로그래밍 스타일은 코드의 가독성과 개발 시간을 개선합니다. 구조화된 동시 실행이라는 개념은 코루틴에 계층 구조가 있다는 것입니다. 즉, 작업이 하위 작업을 실행하고 이 하위 작업이 또 그 하위 작업을 차례로 실행할 수 있습니다. 이 계층 구조의 단위를 코루틴 범위라고 합니다. 코루틴 범위는 항상 수명 주기와 연결되어야 합니다.

코루틴 API는 이러한 구조화된 동시 실행을 준수하도록 설계되어 있습니다. 정지로 표시되지 않은 함수에서는 정지 함수를 호출할 수 없습니다. 이 제한사항을 통해 launch와 같은 코루틴 빌더에서 정지 함수를 호출할 수 있습니다. 이러한 빌더는 결과적으로 CoroutineScope에 연결됩니다.

6. 동시 실행 작업 실행

  1. 두 참가자가 모두 동시에 레이스를 할 수 있도록 하려면 별도의 코루틴 두 개를 실행하고 각 run() 함수 호출을 이러한 코루틴 내부로 이동합니다. launch 빌더를 사용하여 playerOne.run() 호출을 래핑합니다.
LaunchedEffect(playerOne, playerTwo) {
    launch { playerOne.run() }
    playerTwo.run()
    raceInProgress = false 
}
  1. 마찬가지로 launch 빌더를 사용하여 playerTwo.run() 함수 호출을 래핑합니다. 이러한 변경으로 앱은 동시에 실행되는 코루틴 두 개를 실행합니다. 이제 두 플레이어가 동시에 레이스를 할 수 있습니다.
LaunchedEffect(playerOne, playerTwo) {
    launch { playerOne.run() }
    launch { playerTwo.run() }
    raceInProgress = false 
}
  1. 앱을 실행하고 Start를 클릭합니다. 레이스가 시작될 것으로 예상했으나 예상과 달리 버튼의 텍스트가 다시 Start로 즉시 변경됩니다.

c46c2aa7c580b27b.png

두 플레이어가 모두 레이스를 완료하면 Race Tracker 앱은 Pause 버튼의 텍스트를 Start로 다시 설정해야 합니다. 그러나 지금은 플레이어가 레이스를 완료할 때까지 기다리지 않고 코루틴을 실행한 후 즉시 앱이 raceInProgress를 업데이트합니다.

LaunchedEffect(playerOne, playerTwo) {
    launch {playerOne.run() }
    launch {playerTwo.run() }
    raceInProgress = false // This will update the state immediately, without waiting for players to finish run() execution.
}

raceInProgress 플래그는 다음과 같은 이유로 즉시 업데이트됩니다.

  • launch 빌더 함수가 playerOne.run()을 실행하는 코루틴을 실행하고 즉시 반환하여 코드 블록의 다음 줄을 실행합니다.
  • playerTwo.run() 함수를 실행하는 두 번째 launch 빌더 함수에서도 동일한 실행 흐름이 발생합니다.
  • 두 번째 launch 빌더가 반환되면 즉시 raceInProgress 플래그가 업데이트됩니다. 이렇게 하면 버튼 텍스트가 즉시 Start로 변경되고 레이스가 시작되지 않습니다.

코루틴 범위

coroutineScope 정지 함수는 CoroutineScope를 만들고 지정된 정지 블록을 현재 범위를 사용하여 호출합니다. 범위는 LaunchedEffect() 범위에서 coroutineContext를 상속받습니다.

지정된 블록과 블록의 모든 하위 코루틴이 완료되는 즉시 범위가 반환됩니다. RaceTracker 앱의 경우 두 참가자 객체가 모두 run() 함수 실행을 완료하면 반환됩니다.

  1. playerOneplayerTworun() 함수가 raceInProgress 플래그를 업데이트하기 전에 실행을 완료하도록 하려면 두 launch 빌더를 모두 coroutineScope 블록으로 래핑합니다.
LaunchedEffect(playerOne, playerTwo) {
    coroutineScope {
        launch { playerOne.run() }
        launch { playerTwo.run() }
    }
    raceInProgress = false
}
  1. 에뮬레이터나 Android 기기에서 앱을 실행합니다. 다음과 같은 화면이 표시됩니다.

598ee57f8ba58a52.png

  1. Start 버튼을 클릭합니다. 플레이어 2가 플레이어 1보다 더 빠르게 달립니다. 두 플레이어의 진행률이 100%에 도달해 레이스가 완료되면 Pause 버튼 라벨이 Start로 변경됩니다. Reset 버튼을 클릭하여 레이스를 재설정하고 시뮬레이션을 다시 실행할 수 있습니다. 레이스는 다음 동영상에 나와 있습니다.

c1035eecc5513c58.gif

실행 흐름은 다음 다이어그램에 나와 있습니다.

cf724160fd66ff21.png

  • LaunchedEffect() 블록이 실행되면 컨트롤이 coroutineScope{..} 블록으로 전송됩니다.
  • coroutineScope 블록은 두 코루틴을 동시에 실행하고 실행이 완료될 때까지 기다립니다.
  • 실행이 완료되면 raceInProgress 플래그가 업데이트됩니다.

coroutineScope 블록은 블록 내부의 모든 코드가 실행을 완료한 후에만 반환되고 계속 진행합니다. 블록 외부에 있는 코드의 경우 동시 실행의 존재 여부는 단순한 구현 세부정보가 됩니다. 이 코딩 스타일은 동시 프로그래밍에 대해 구조화된 접근 방식을 제공하며 구조화된 동시 실행이라고 합니다.

레이스가 완료된 후 Reset 버튼을 클릭하면 코루틴이 취소되고 두 플레이어의 진행률은 0으로 재설정됩니다.

사용자가 Reset 버튼을 클릭할 때 코루틴이 취소되는 방식을 확인하려면 다음 단계를 따르세요.

  1. 다음 코드와 같이 run() 메서드의 본문을 try-catch 블록에 래핑합니다.
suspend fun run() {
    try {
        while (currentProgress < maxProgress) {
            delay(progressDelayMillis)
            currentProgress += progressIncrement
        }
    } catch (e: CancellationException) {
        Log.e("RaceParticipant", "$name: ${e.message}")
        throw e // Always re-throw CancellationException.
    }
}
  1. 앱을 실행하고 Start 버튼을 클릭합니다.
  2. 진행률이 어느 정도 증가한 후 Reset 버튼을 클릭합니다.
  3. Logcat에 다음 메시지가 출력되는지 확인합니다.
Player 1: StandaloneCoroutine was cancelled
Player 2: StandaloneCoroutine was cancelled

7. 단위 테스트를 작성하여 코루틴 테스트

코루틴을 사용하는 단위 테스트 코드는 주의가 필요합니다. 비동기로 실행될 수 있고 여러 스레드에서 발생할 수 있기 때문입니다.

테스트에서 정지 함수를 호출하려면 코루틴이 있어야 합니다. JUnit 테스트 함수 자체는 정지 함수가 아니므로 runTest 코루틴 빌더를 사용해야 합니다. 이 빌더는 kotlinx-coroutines-test 라이브러리의 일부이며 테스트를 실행하도록 설계되었습니다. 빌더는 새 코루틴에서 테스트 본문을 실행합니다.

runTestkotlinx-coroutines-test 라이브러리의 일부이므로 종속 항목을 추가해야 합니다.

종속 항목을 추가하려면 다음 단계를 완료하세요.

  1. Project 창에서 app 디렉터리에 있는 앱 모듈의 build.gradle.kts 파일을 엽니다.

e7c9e573c41199c6.png

  1. 파일 내에서 dependencies{} 블록이 표시될 때까지 아래로 스크롤합니다.
  2. testImplementation 구성을 사용하여 종속 항목을 kotlinx-coroutines-test 라이브러리에 추가합니다.
plugins {
    ...
}

android {
    ...
}

dependencies {
    ...
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
  1. build.gradle.kts 파일 상단의 알림바에서 Sync Now를 클릭하여 다음 스크린샷과 같이 가져오기와 빌드를 완료합니다.

1c20fc10750ca60c.png

빌드가 완료되면 테스트를 작성할 수 있습니다.

레이스를 시작하고 완료하기 위한 단위 테스트 구현

레이스 진행률이 레이스의 여러 단계에서 올바르게 업데이트되도록 하려면 단위 테스트에서 다양한 시나리오를 다뤄야 합니다. 이 Codelab에서는 두 가지 시나리오를 다룹니다.

  • 레이스 시작 후 진행률
  • 레이스 완료 후 진행률

레이스 시작 후 레이스 진행률이 올바르게 업데이트되는지 확인하려면 raceParticipant.progressDelayMillis 시간이 지난 후 현재 진행률이 1로 설정되어 있음을 어설션해야 합니다.

테스트 시나리오를 구현하려면 다음 단계를 따르세요.

  1. 테스트 소스 세트에 있는 RaceParticipantTest.kt 파일로 이동합니다.
  2. 테스트를 정의하려면 raceParticipant 정의 후에 raceParticipant_RaceStarted_ProgressUpdated() 함수를 만들고 @Test 주석을 답니다. 테스트 블록은 runTest 빌더에 배치해야 하므로 표현식 문법을 사용하여 runTest() 블록을 테스트 결과로 반환합니다.
class RaceParticipantTest {
    private val raceParticipant = RaceParticipant(
        ...
    )

    @Test
    fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    }
}
  1. 읽기 전용 expectedProgress 변수를 추가하고 1로 설정합니다.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
}
  1. 레이스 시작을 시뮬레이션하려면 launch 빌더를 사용하여 새 코루틴을 실행하고 raceParticipant.run() 함수를 호출합니다.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
}

raceParticipant.progressDelayMillis 속성의 값은 레이스 진행률이 업데이트되기 전에 걸리는 시간을 결정합니다. progressDelayMillis 시간이 지난 후 진행률을 테스트하려면 테스트에 몇 가지 형식의 지연을 추가해야 합니다.

  1. advanceTimeBy() 도우미 함수를 사용하여 raceParticipant.progressDelayMillis 값만큼 시간을 진행합니다. advanceTimeBy() 함수는 테스트 실행 시간을 줄이는 데 도움이 됩니다.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
}
  1. advanceTimeBy()는 지정된 시간에 예약된 작업을 실행하지 않으므로 runCurrent() 함수를 호출해야 합니다. 이 함수는 현재 시간에 대기 중인 작업을 실행합니다.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
    runCurrent()
}
  1. 진행률이 업데이트되도록 하려면 assertEquals() 함수 호출을 추가하여 raceParticipant.currentProgress 속성의 값이 expectedProgress 변수의 값과 일치하는지 확인합니다.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
    runCurrent()
    assertEquals(expectedProgress, raceParticipant.currentProgress)
}
  1. 테스트를 실행하여 통과하는지 확인합니다.

레이스가 완료된 후 레이스 진행률이 올바르게 업데이트되는지 확인하려면 레이스가 완료될 때 현재 진행률이 100으로 설정되어 있음을 어설션해야 합니다.

테스트를 구현하려면 다음 단계를 따르세요.

  1. raceParticipant_RaceStarted_ProgressUpdated() 테스트 함수 다음에 raceParticipant_RaceFinished_ProgressUpdated() 함수를 만들고 @Test 주석을 답니다. 함수는 runTest{} 블록에서 테스트 결과를 반환해야 합니다.
class RaceParticipantTest {
    ...

    @Test
    fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
        ...
    }

    @Test
    fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    }
}
  1. 새 코루틴을 실행하는 launch 빌더를 사용하고 그 안에 raceParticipant.run() 함수 호출을 추가합니다.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
}
  1. 레이스 완료를 시뮬레이션하려면 advanceTimeBy() 함수를 사용하여 디스패처의 시간을 raceParticipant.maxProgress * raceParticipant.progressDelayMillis만큼 진행합니다.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
}
  1. runCurrent() 함수 호출을 추가하여 대기 중인 작업을 실행합니다.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
    runCurrent()
}
  1. 진행률이 올바르게 업데이트되도록 하려면 assertEquals() 함수 호출을 추가하여 raceParticipant.currentProgress 속성의 값이 100과 같은지 확인합니다.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
    runCurrent()
    assertEquals(100, raceParticipant.currentProgress)
}
  1. 테스트를 실행하여 통과하는지 확인합니다.

도전과제 해 보기

ViewModel의 단위 테스트 작성 Codelab에서 설명된 테스트 전략을 적용합니다. 해피 패스, 오류 사례, 경계 사례를 다루는 테스트를 추가합니다.

직접 작성한 테스트를 솔루션 코드에서 제공되는 테스트와 비교해 보세요.

8. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 다음 git 명령어를 사용하면 됩니다.

git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-race-tracker.git
cd basic-android-kotlin-compose-training-race-tracker

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

솔루션 코드를 보려면 GitHub에서 확인하세요.

9. 마무리

축하합니다. 코루틴을 사용하여 동시 실행을 처리하는 방법을 알아봤습니다. 코루틴은 기본 스레드를 차단하여 앱이 응답하지 않도록 할 수 있는 장기 실행 작업을 관리하는 데 도움이 됩니다. 단위 테스트를 작성하여 코루틴을 테스트하는 방법도 배웠습니다.

다음은 코루틴의 이점입니다.

  • 가독성: 코루틴으로 작성하는 코드를 통해 코드 줄을 실행하는 시퀀스를 명확하게 이해할 수 있습니다.
  • Jetpack 통합: Compose, ViewModel과 같은 많은 Jetpack 라이브러리에는 완전한 코루틴 지원을 제공하는 확장 프로그램이 포함되어 있습니다. 일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공합니다.
  • 구조화된 동시 실행: 코루틴을 사용하면 동시 실행 코드를 안전하고 쉽게 구현할 수 있고, 불필요한 상용구 코드가 제거되며, 앱에서 실행한 코루틴이 손실되거나 리소스를 낭비하지 않도록 할 수 있습니다.

요약

  • 코루틴을 사용하면 새로운 스타일의 프로그래밍을 배우지 않고도 동시에 실행되는 장기 실행 코드를 작성할 수 있습니다. 코루틴은 순차적으로 실행되도록 설계되었습니다.
  • suspend 키워드는 함수 또는 함수 유형을 표시하여 코드 명령 집합을 실행, 일시중지, 재개하는 데 사용할 수 있는지 나타내기 위해 사용합니다.
  • suspend 함수는 다른 정지 함수에서만 호출할 수 있습니다.
  • launch 또는 async 빌더 함수를 사용하여 새 코루틴을 시작할 수 있습니다.
  • 코루틴 컨텍스트, 코루틴 빌더, 작업, 코루틴 범위, 디스패처는 코루틴 구현을 위한 주요 구성요소입니다.
  • 코루틴은 디스패처를 사용하여 실행에 사용할 스레드를 결정합니다.
  • 작업은 코루틴의 수명 주기를 관리하고 상위-하위 관계를 유지하여 구조화된 동시 실행을 보장하는 데 중요한 역할을 합니다.
  • CoroutineContext는 작업 및 코루틴 디스패처를 사용하여 코루틴의 동작을 정의합니다.
  • CoroutineScope는 작업을 통해 코루틴의 전체 기간을 제어하고 하위 요소 및 그 하위 요소에 취소와 기타 규칙을 재귀적으로 적용합니다.
  • 실행, 완료, 취소, 실패는 코루틴 실행의 일반적인 4가지 작업입니다.
  • 코루틴은 구조화된 동시 실행 원칙을 따릅니다.

자세히 알아보기