Skip to content

코루틴 스코프 함수

Published: at 오전 05:28

Table of contents

Open Table of contents

코루틴 스코프 함수가 소개되기 전에 사용한 방법들

여러 개의 엔드포인트에서 데이터를 동시에 얻어야하는 중단 함수가 있을 때 가장 바람직한 방법을 보기전에 차선책부터 살펴봅시다.

중단 함수에서 중단 함수를 호출하기

문제는 작업이 동시에 진행되지 않는다는 점입니다. (sync)

하나의 엔드포인트에서 1초씩 걸린다면 함수가 끝나는데 1초 대신 2초가 걸립니다.

suspend fun getArticlesForUser(
  userToken: String?,
): List<ArticleJson> {
  val articles = getArticles(userToken) // (1초후)
  val user = userService.getUser(userToken) // (1초후)
  return articles
}

두 개의 중단 함수를 동시에 실행하려면 각각 aysnc로 래핑해야 합니다. 하지만 async는 스코프를 필요로 하며 GlobalScope를 사용하는 것은 좋은 방법이 아닙니다.

// 잘못된 구현
suspend fun getArticlesForUser(
  userToken: String?,
): List<ArticleJson> = coroutineScope {
  val articles = async { getArticles(userToken) }
  val user = async { userService.getUser(userToken) }
  articles.await()
}

GlobalScope에서 async를 호출하면 부모 코루틴과 아무런 관계가 없습니다. 이 때 코루틴은

가장 중요한 결과는 다음과 같습니다.

스코프를 인자로 넘기기

suspend fun getArticlesForUser(
  userToken: String?,
  scope: CoroutineScope,
): List<ArticleJson> {
  val articles = scope.async { getArticles(userToken) }
  val user = scope.async { userService.getUser(userToken) }
  return articles.await()
}

이 방법은 취소가 가능하며 적절한 단위 테스트를 추가할 수 있다는 점에서 좀 더 나은 방식이라 할 수 있습니다.

문제는 스코프가 함수에서 함수로 전달되어야 한다는 것입니다.

스코프가 함수로 전달되며 예상하지 못한 부작용이 발생할 수 있습니다.

예를 들면, async에서 예외가 발생하면 모든 스코프가 닫히게 됩니다. (SupervisorJob이 아닌 Job을 사용한다고 가정합니다)

또한 스코프에 접근하는 함수가 cancel 메서드를 사용해 스코프를 취소하는 등 스코프를 조작할 수도 있습니다. 이러한 접근 방식은 다루기 어려울 뿐만 아니라 잠재적으로 위험하다고 볼 수 있습니다.

data class Details(val name: String, val: followers: Int)
data class Tweet(val text: String)

fun getFollowersNumber(): Int = throw Error("Service exception")

suspend fun getUserName(): String {
    delay(1000)
    return "John Doe"
}

suspend fun getTweets(): List<Tweet> {
    return listOf(Tweet("Hello, world!"))
}

suspend fun CoroutineScope.getDetails(): Details {
    val name = async { getUserName() }
    val followers = async { getFollowersNumber() }
    return Details(name.await(), followers.await())
}

fun main() = runBlocking {
    val details = try {
        getDetails()
    } catch (e: Exception) {
        null
    }
    val tweets = async { getTweets() }

    println("Details: $details")
    println("Tweets: ${tweets.await()}")
}

위 코드를 보면 사용자 세부사항을 들고 오는 데 문제가 있더라도 최소한 Tweets는 가져올 수 있습니다.

하지만 getFollowersNumber에서 예외가 발생하면 async를 종료시키고 전체 스코프가 종료되는 걸로 이어져 프로그램이 끝나버리게 됩니다.

예외가 발생하면 종료되는 대신 예외를 그대로 던지는 함수가 더 낫습니다.

coroutineScope

coroutineScope는 스코프를 시작하는 중단 함수이며, 인자로 들어온 함수가 생성한 값을 반환합니다.

suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

asynclaunch와는 다르게 coroutineScope의 본체는 리시버 없이 곧바로 호출됩니다.

coroutineScope는 새로운 코루틴을 생성하지만 새로운 코루틴이 끝날 때까지 coroutineScope를 호출한 코루틴을 중단하기 때문에 호출한 코리틴이 작업을 동시에 시작하지는 않습니다.

두 delay 호출 모두 runBlocking을 중단시키는 다음 예를 봅시다.

fun main() = runBlocking {
    val a = coroutineScope {
        delay(1000)
        10
    }
    println("a is calculated")
    val b = coroutineScope {
        delay(1000)
        20
    }
    println(a) // 10
    println(b) // 20
}
// (1초 후)
// a is calculated
// (1초 후)
// 10
// 20

생성된 스코프는 바깥의 스코프에서 coroutineContext를 상속받지만 컨텍스트의 Job을 오버라이딩 합니다. 따라서 생성된 스코프는 부모가 해야할 책임을 이어받습니다.

코루틴 스코프 함수

스코프를 만드는 다양한 함수가 있으며, coroutineScope와 비슷하게 작동합니다. supervisorScopecoroutineScope와 비슷하지만, Job 대신 SupervisorJob을 사용합니다.

withContext는 코루틴 컨텍스트를 바꿀 수 있는 coroutineScope입니다. withTimeout은 제한 시간이 지나면 취소되는 coroutineScope입니다.

코루틴 스코프 함수는 코루틴 빌더와 혼동되지만 두 함수는 개념적으로나 사용함에 있어서 아주 다르기 때문에 쉽게 구분할 수 있습니다.

두 함수의 특징을 비교한 다음 표를 보면 차이점이 명확하게 드러납니다.

코루틴 빌더(runBlocking 제외)코루틴 스코프 함수
launch, async, producecoroutineScope, supervisorScope, withContext, withTimeout
CoroutineScope의 확장함수중단 함수
CoroutineScope 리시버의 코루틴 컨텍스트를 사용중단 함수의 컨티뉴에이션 객체가 가진 코루틴 컨텍스트를 사용
예외는 Job을 통해 부모로 전파됨일반 함수와 같은 방식으로 예외를 던짐
비동기인 코루틴을 시작함코루틴 빌더가 호출된 곳에서 코루틴을 시작함

withContext

withContextcoroutineScope와 비슷하지만, 스코프의 컨텍스트를 변경할 수 있다는 점에서 다릅니다.

withContext의 인자로 컨텍스트를 제공하면 부모 스코프의 컨텍스트를 대체합니다.

withContext 함수는 기존 스코프와 컨텍스트가 다른 코루틴 스코프를 설정하기 위해 주로 사용됩니다.

디스패처와 함께 종종 사용되곤 합니다.

launch(Dispatchers.Main) {
    view.showLoading()
    withContext(Dispatchers.IO) {
        repository.saveData(data)
    }
    view.hideLoading()
}

coroutineScope의 작동하는 방식이 async { }.await() 와 비슷하지만, 가장 큰 차이는 async는 스코프를 필요로 하지만

coroutineScopewithContext는 해당 함수를 호출한 중단점에서 스코프를 들고 온다는 점입니다. 두 경우 모두 async await를 곧바로 호출하는 방법 대신

coroutineScopewithContext를 사용하는 편이 좋습니다.