Coroutine scope functions

참치돌고래·2022년 12월 2일
0
post-custom-banner

부적절한 예들로부터 솔루션을 찾아보자.

concurrent 하지 않은 suspending function 호출

suspend fun getUserProfile() : UserProfileData{
    val user = getUserData()
    val notification = getNotifications()
    
    return UserProfileData(
        user = user,
        notification = notification
    )
}

해당 코드를 비동기적으로 호출하여 합하려고 한다. async는 scope가 필요하기 때문에,
Globalscope를 통해 async함수를 호출한다. (좋지 못한 솔루션)

suspend fun getUserProfile() : UserProfileData{
    val user = GlobalScope.async { getUserData()}
    val notification = GlobalScope.async {getNotifications()}

    return UserProfileData(
        user = user.await(),
        notification = notification.await()
    )
}

GlobalScopeEmptyCoroutineContext이다.

  • 부모가 없어 cancelled 불가
  • 어떠한 부모로부터도 상속불가능 (always run defualt dispatcer -> 최적화문제)
  • memory leak, redundant CPU usage
  • 테스트 난이도의 증가

그렇다면 scope를 파라미터화 시켜 변경하는 것은 어떨까?

suspend fun getUserProfile(
    scope : CoroutineScope
) : UserProfileData{
    val user = scope.async { getUserData()}
    val notification = scope.async {getNotifications()}

    return UserProfileData(
        user = user.await(),
        notification = notification.await()
    )
}

위의 해결법보다는 unit test와 cancellation이 가능하다는 점에서는 보다 낫다고 할 수 있다. 하지만, 여전히 scope가 함수 간 전달이 필요하다. 이러한 상황은 예기치 못한 side effect가 발생할 수 있다. (async에서 exception발생 시 해당 전체 함수 중지)

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(500)
    return "marcimoskala"
}

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

suspend fun CoroutineScope.getUserDetails() : Details {
    val userName = async { getUserName() }
    val followersNumber = async { getFollowersNumber() }
    return Details(userName.await(), followersNumber.await())
}

fun main() = runBlocking {
    val details = try{
        getUserDetails()
    }catch (e : Error){
        null
    }
    val tweets = async { getTweets() }
    println("User : $details")
    println("Tweet : ${tweets.await()}")
}

output

Exception in thread "main" java.lang.Error: Service exception

getFollowersNumber에서 exception이 발생해 async가 깨졌다. 하지만 이러한 exception은 다른 async에도 전파가 되어 결국 함수 전체의 로직이 깨지는 것을 확인할 수 있었다.

올바른 해결책 : coroutineScope

새로운 coroutine이 끝날 때까지, 이전의 coroutine을 중지한다.
coroutineScope는 suspending context를 생성하고, 해당 scope의 부모로부터 상속을 받는다.
concurrency 하게 짜여지는 것을 지원한다.
coroutineScope를 통해 위의 문제를 해결해보자.

data class Details(val name: String, val followers : Int)
data class Tweet(val text : String)
class ApiException(
    val code: Int,
    message : String
) : Throwable(message)

fun getFollowersNumber() : Int =
    throw ApiException(500, "Service unavailable")

suspend fun getUserName() : String {
    delay(500)
    return "marcimoskala"
}

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

suspend fun getUserDetails() : Details = coroutineScope {
    val userName = async { getUserName() }
    val followersNumber = async { getFollowersNumber() }
    Details(userName.await(), followersNumber.await())
}

fun main() = runBlocking<Unit>{
    val details = try{
        getUserDetails()
    }catch (e : ApiException){
        null
    }
    val tweets = async { getTweets() }
    println("User : $details")
    println("Tweet : ${tweets.await()}")
}

output

User : null
Tweet : [Tweet(text=Hello, world)]
profile
안녕하세요
post-custom-banner

0개의 댓글