Kotlin의 코루틴

오븐·2025년 4월 30일

안드로이드

목록 보기
9/9

안드로이드 공부를 할때마다 계속 코루틴 코루틴이라는 단어가 나온다. 모르고 걍 넘어갔다가 코드가 꼬여 애먹은 적이 여러번이다. 이젠 제대로 정리해보려고 한다.

정의

물론 절대적인 정의는 없다. 다만 이해를 위해 정리해본다.

  • 안드로이드 문서: 비동기적으로 실행되는 코드를 간소화하기 위해 사용할 수 있는 동시 실행 설계 패턴
  • 스택오버플로우: 두 개의 서로 다른 루틴 간에 실행 제어가 반환 없이 협력적으로 전달되는 제어 구조
  • 위키피디아(한판): 루틴의 일종으로서, 협동 루틴. 상호 연계 프로그램
  • 위키피디아(영판): 실행의 일시중단, 재개, 서브루틴 생성를 허용하는 협력적 멀티 태스킹
    • 크리스토퍼 D. 마를린이 정의한 특징
      1. 코루틴에 있는 데이터 값은 연속적인 호출 사이에 유지
      2. 실행은 제어가 코루틴을 벗어나면 중단되지만, 나중에 제어가 코루틴에 다시 들어오면 중단된 부분부터 계속 진행

      루틴: 특정 작업을 수행하고 재사용될 수 있는 코드
      서브루틴: 주 루틴보다 작은 루틴. 주 루틴의 일부이며, 다른 서브루틴과는 독립적이다. 반복적으로 호출될 수 있다.

요컨데 여러 루틴들이 서로 협력적으로 동작하는 제어 구조~패턴이다.
그러면 이게 코틀린에서는 어떻게 적용되는지 알아보자.

코틀린에서는...

코틀린은 asyncawait가 아닌 중단 함수suspend function 개념을 사용해 비동기 작업을 실행한다.

기본적 개념

코틀린 공식 문서는 코루틴을 중단 가능한 계산의 인스턴스라고 설명한다. 나머지 코드와 동시에 실행된다는 점에서 스레드와 유사하지만, 엄밀히 말하면 다르다. 스레드는 특정 스레드에 종속되지 않는다. 코루틴은 한 스레드에서 실행을 중단하고 다른 스레드에서 시작할 수 있다.

일반적인 예시

아래는 문서의 공식 코드다.

fun main() = runBlocking { //<-  CoroutineScope
    launch { // 새로 코루틴을 시작
        delay(1000L) // 1초동안 차단x 지연
        println("World!") // 지연 후 출력
    }
    println("Hello") // 이전 코루틴이 지연되는 동안 주요 코루틴은 실행된다
}

결과는

Hello
World!
  • launch: 코루틴 빌더. 다른 코드와 함께 동시적으로 새 코루틴을 실행. 그 때문에 Hello가 먼저 출력
  • delay: 중단 함수. 일정 시간동안 코루틴을 중단

    코루틴 중단은 관련된 스레드를 차단하지 않는다. 하지만 관련 스레드에 있는 다른 코루틴이 실행하는 것을 허락한다.

  • runBlocking: 코루틴이 아닌 main()을 연결하는 코루틴 빌더. 해당 빌더는 실행하는 스레드가 runBlocking{ ... } 내의 모든 코루틴이 실행을 완료할 때까지 스레드를 차단 (물론 스레드 차단은 비용이 크다. 추천x)

함수로 분리 - suspend

위의 코드에서, println("World!")를 밖으로 내보내고 싶은 경우에는 다음과 같이 작성한다.

fun main() = runBlocking { // <- CoroutineScope
    launch { doWorld() }
    println("Hello")
}

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}
  • suspend중단: 코루틴 안에서 일반 함수처럼 사용할 수 있고, 다른 중단 함수를 사용해 코루틴 실행을 중단할 수도 있음

CoroutineScope

자신만의 코루틴스코프를 생성할 수도 있다.

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

...전에 코루틴스코프가 무엇이냐??
코루틴스코프CoroutineScope는 코루틴컨텍스트CoroutineContext 하나만 멤버 속성의 정의하고 있는 인퍼테이스다.

코루틴컨텍스트는 코루틴을 어떻게 처리 할지에 대한 정보를 가지고 있는 객체다.

위에서는 coroutineScope를 사용해 자신만의 범위scope를 생성했다. { ... } 안의 모든 함수가 실행되기 전까지 종료되지 않는다.

출력은 맨 위의 예시 코드와 같다.

스코프와 동시실행

// 순차적으로 코드를 실행한다
fun main() = runBlocking {
    doWorld()
    println("Done")
}

// 동시에 두 부분을 실행한다
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

명시적 Job

실행하는 코루틴 빌더는 코루틴 핸들인 Job객체를 반환한다. 이를 이용해 실행된 코루틴의 완료를 명시적으로 기다릴 수 있다.

val job = launch { // 새 코루틴을 실행하고 Job에 대한 참조를 유지
    delay(1000L)
    println("World!")
}
println("Hello")
job.join() // 코루틴이 완료될 때까지 기다린다
println("Done") 

출력은 다음과 같다.

Hello
World 1
World 2
Done

이제 아주 기본적인 것들은 이렇다... 이제 다음으로 넘어가자니 문서가 자 여기부터는 물이 정말 깊어를 시전했다. 우선 몇번 읽어보고 정리하려고 한다....

쓰이는 함수들...

안드로이드에서도 종종 쓰이는 관련 함수들을 정리했다.

stateIn

fun <T> Flow<T>.stateIn(
    scope: CoroutineScope, 
    started: SharingStarted, 
    initialValue: T
): StateFlow<T>

콜드 플로우를 핫 스테이트 플ㄹ우로 변환하는 함수다. 주어진 CoroutineScope를 기준으로 시작해, 가장 최근 방출된emitted 값을 공유한다.

콜드 플로우: 내놓으라고 말해야 내 입에 집어넣어줌
핫 플로우: 내놓으라고 말하기 전에 이거 새거라며 내 입에 집어넣음

일부 상태 값에 대한 업데이트를 제공하는 콜드 플로우가 있고, 생성 및 유지 관리 비용이 많이 들지만 가장 최근 상태 값을 수집해야 하는 구독자가 여러 명 있는 경우에 유용

  • started: 코루틴 공유의 시작은 해당 파라미터로 제어. 다음과 같은 옵션 有
    - Eagerly: 첫번째 구독자가 나타나기 전에도 시작. 이 경우 replay 파라미터에서 지정한 개수 외의 값은 즉시 삭제
    • Lazily: 첫번쩨 구독자가 나타난 후에 시작. 첫번째 구독자는 모든 방출된 값을 가질 수 있으나, 그 다음 구독자는 최근 값만 가질 수 있음
    • WhileSubscribed(): 첫번째 구독자가 나타난 후에 시작, 마지막 구독자가 사라지면 즉시 멈춤. stopTimeoutMillis를 통해 마지막 구독자가 사라지고 공유 코루틴이 중지될 때까지의 지연 시간을 설정할 수 있음

withContext

suspend fun <T> withContext(
    context: CoroutineContext, 
    block: suspend CoroutineScope.() -> T
): T
  • 주어진 코투틴 컨텍스트로 구체화된 중단 블록을 호출, 종료하면 결과를 반환
  • 다른 코루틴 컨텍스트에서 특정 코드를 실행할 수 있음 (특히 다른 디스패처에서 무언가를 실행하고 싶을 때 사용)

디스패처Dispatcher: 코루틴이 실행하는데 사용할 스레드를 결정하는 매커니즘

  • 기본 디스패처에 여러 스레드가 있고, 일부 코드를 실행한 다음, 그 결과값을 사용해 UI를 업데이트를 할 때 -> 결과가 필요하기 때문에 사용
    val result = someComputation()
     withContext(Dispatchers.Main) {	updateUI(result)	}
  • 선행하는 일부 코드가 동시적이지 않은 경우에도 가능
scope.launch(Dispatchers.Default) {
    val result = someComputation()
    withContext(Dispatchers.Main) {
        updateUI(result)
    }
}

출처

profile
하루에 한번 정권 찌르기

0개의 댓글