부적절한 예들로부터 솔루션을 찾아보자.
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()
)
}
GlobalScope
는 EmptyCoroutineContext
이다.
그렇다면 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에도 전파가 되어 결국 함수 전체의 로직이 깨지는 것을 확인할 수 있었다.
새로운 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)]