여러 개의 엔드포인트에서 데이터를 동시에 얻어야 하는 중단 함수를 떠올려보자.
가장 바람직한 방법을 보기 전에 차선책부터 살펴보자! 🎯
// 데이터를 동시에 가져오지 않고, 순차적으로 가져옴
suspend fun getUserProfile(): UserProfileData {
val user = getUserData() // (1초 후)
val notifications = getNotifications() // (1초 후)
return UserProfileData(
user = user,
notifications = notifications,
)
}
GlobalScope 사용// 이렇게 구현하면 안 됩니다! (GlobalScope 사용은 바람직하지 않음)
suspend fun getUserProfile(): UserProfileData {
val user = GlobalScope.async { getUserData() }
val notifications = GlobalScope.async { getNotifications() }
return UserProfileData(
user = user.await(), // (1초 후)
notifications = notifications.await(),
)
}
GlobalScope에서 async를 호출하면 부모 코루틴과 아무런 관계가 없음. 이때 async 코루틴은
async 내부의 함수가 실행 중인 상 태가 되므로 작업이 끝날 때까지 자원이 낭비됨// 이렇게 구현하면 안 됩니다!
suspend fun getUserProfile(
scope: CoroutineScope
): UserProfileData {
val user = scope.async { getUserData() }
val notifications = scope.async { getNotifications() }
return UserProfileData(
user = user.await(), // (1초 후)
notifications = notifications.await(),
)
}
// 또는
// 이렇게 구현하면 안 됩니다!
suspend fun CoroutineScope.getUserProfile(): UserProfileData {
val user = async { getUserData() }
val notifications = async { getNotifications() }
return UserProfileData(
user = user.await(), // (1초 후)
notifications = notifications.await()
)
}
async에서 예외가 발생하면 모든 스코프가 닫히게 됨cancel 메서드를 사용해 스코프를 취소하는 등 스코프를 조작할 수 있음coroutineScope는 스코프를 시작하는 중단 함수이며, 인자로 들어온 함수가 생성한 값을 반환
suspend fun <R> coroutineScope(
block: suspend CoroutineScope.() -> R
): R
coroutineScope함수는 새로운 코루틴을 생성하지만 새로운 코루틴이 끝날 때까지 호출한 코루틴을 중단하기 때문에 호출한 코루틴이 작업을 동시에 시작하지는 않음
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
생성된 스코프는 컨텍스트의 Job을 오버라이딩하여 생성된 스코프는 부모가 해야할 책임을 이어받음
// CoroutineName이 부모에서 자식으로 전달됨
suspend fun longTask() = coroutinecScope {
launch {
delay(1000)
val name = coroutineContext[CoroutineName]?.name
printIn("[$name] Finished task 1")
}
launch {
delay(2000)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 2")
}
}
fun main() = runBlocking(CoroutineName("Parent")) {
println("Before")
longTask()
println("After")
}
// Before
// (1초 후)
// [Parent] Finished task 1
// (1초 후)
// [Parent] Finished task 2
// After
// 부모가 취소되면 아직 끝나지 않은 자식 코루틴이 전부 취소됨
suspend fun longTask() = coroutinecScope {
launch {
delay(1000)
val name = coroutineContext[CoroutineName]?.name
printIn("[$name] Finished task 1")
}
launch {
delay(2000)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 2")
}
}
fun main() = runBlocking {
val job = launch(CoroutineName("Parent")) {
longTask()
}
delay(1500)
job.cancel()
}
// [Parent] Finished task 1
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 "marcinmoskala"
}
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("Tweets: ${tweets.await()}")
}
// User: null
// Tweets: [Tweet(text=Hello, world)]
중단함수에서 병렬로 작업을 수행할 경우,
coroutineScope를 사용하는 것이 좋음
suspend fun getUserProfile(): UserProfileData =
coroutinescope {
val user = async { getUserData() }
val notifications = async { getNotifications() }
UserProfileData(
user = user.await(),
notifications = notifications.await(),
)
}
coroutineScope는 중단 메인 함수 본체를 래핑할 때 주로 사용됨
suspend fun main(): Unit = coroutinescope {
launch {
delay(1000)
println("World")
}
println("Hello, ")
}
// Hello
// (1초 후)
// World
coroutineScope는 기존의 중단 컨텍스트에서 벗어난 새로운 스코프를 만듦
부모로부터 스코프를 상속받고 구조화된 동시성을 지원
| 코루틴 빌더 (runBlocking 제외) | 코루틴 스코프 함수 |
|---|---|
| launch, async, produce | coroutineScope, supervisorScope,withContext, withTimeout |
| CoroutineScope의 확장함수 | 중단 함수 |
| CoroutineScope 리시버의 코루틴 컨텍스트로 사용 | 중단 함수의 컨티뉴에이션 객체가 가진 코루틴 컨텍스트를 사용 |
| 예외는 Job을 통해 부모로 전파됨 | 일반 함수와 같은 방식으로 예외를 던짐 |
| 비동기인 코루틴을 시작함 | 코루틴 빌더가 호출된 곳에서 코루틴을 시작함 |
coroutineScope와 비슷하지만 스코프의 컨텍스트를 변경할 수 있다는 점에서 다름withContext의 인자로 컨텍스트를 제공하면 부모 스코프의 컨텍스트를 대체fun CoroutineScope.log(text: String) {
val name = this.coroutineContext[CoroutineName]?.name
println("[$name】 $text")
}
fun main() = runBlocking(CoroutineName("Parent")) {
log("Before")
withContext(CoroutineName("Child 1")) {
delay(1000)
log("Hello 1")
}
withContext(CoroutineName("Child 2")) {
delay(1000)
log("Hello 2")
}
log ("After")
}
// [Parent] Before
// (1초 후)
// [Child 1] Hello 1
// (1초 후)
// [Child 2] Hello 2
// [Parent] After
CoroutineScope를 만들고 지정된 중단 함수를 호출한다는 점에서 coroutineScope와 비슷Job을 SupervisorJob으로 오버라이딩 해서 자식 코루틴이 예외를 던지더라도 취소되지 않음fun main() = runBlocking {
println("Before")
supervisorScope {
launch {
delay(1000)
throw Error()
}
launch {
delay(2000)
println("Done")
}
}
println("After")
}
// Before
// (1초 후)
// 예외가 발생합니다...
// (1초 후)
// Done
// After
supervisorScope대신withContext(SupervisorJob())을 사용할 수 있는가?
withContext(SupervisorJob())을 사용하면 withContext는 여전히 기존에 가지고 있던 Job을 사용하며 SupervisorJob()이 해당 잡의 부모가 됨withContext 또한 예외를 던지기 때문에 SupervisorJob()은 사실상 쓸모 없게 됨coroutineScope와 다름TimeoutCancellationException을 던짐suspend fun test(): Int = withTimeout(1500) {
delay(1000)
println("Still thinking")
delay(1000)
println("Done!")
42
}
suspend fun main(): Unit = coroutineScope {
try {
test()
} catch (e: TimeoutCancellationException) {
println("Cancelled")
}
delay(1000)
// 'test' 함수가 취소되었기 때문에,
// 타임아웃 시간을 늘려도 아무런 도움이 되지 않습니다.
}
// (1초 후)
// Still thinking
// (0.5초 후)
// Cancelled
withTimeout은 테스트할 때 특히 유용함
class Test {
@Test
fun testTime2() = runTest {
withTimeout(1000) {
// 1000ms보다 적게 걸리는 작업
delay(900) // 가상 시간
}
}
@Test(expected = TimeoutCanceUationException::class)
fun testTime1() = runTest {
withTimeout(1000) {
// 1000ms보다 오래 걸리는 작업
delay(1100) // 가상 시간
}
}
@Test
fun testTime3() = runBlocking {
withTimeout(1000) {
// 그다지 오래 걸리지 않는 일반적인 테스트
delay(900) // 실제로 900ms만큼 기다립니다.
}
}
}
코루틴 빌더 내부에서
TimeoutCancellationException을 던지면 해당 코루틴만 취소가 되고 부모에게는 영향을 주지 않음
suspend fun main(): Unit = coroutineScope {
launch { // 1
launch { // 2, 부모에 의해 취소됩니다.
delay(2000)
println("Will not be printed")
}
withTimeout(1000) { // 이 코루틴이 launch를 취소
delay(1500)
}
}
launch { // 3
delay(2000)
println("Done")
}
}
// (2초 후)
// Done
withTimeout의 완화된 형태로 withTimeoutOrNull이 있으며, 이 함수는 예외를 던지지 않음.suspend fun calculateAnswerOrNull(): User? =
withContext(Dispatchers.Default) {
withTimeoutOrNull(1000) {
calculateAnswer()
}
}
launch를 호출하는 방법이 자주 사용됨class ShowUserDataUseCase(
private val repo: UserDataRepository,
private val view: UserDataView,
) {
suspend fun showUserData() = coroutineScope {
val name = async { repo.getName() }
val friends = async { repo.getFriends() }
val profile = async { repo.getProfile() }
val user = User(
name = name.await(),
friends = friends.await(),
profile = profile.await()
)
view.show(user)
launch { repo.notifyProfileShown() }
}
}
(문제점 1)
coroutineScope가 사용자 데이터를 보여 준 뒤launch로 시작된 코루틴이 끝나기를 기다려야하므로launch에서 함수의 목적과 유의미한 작업을 한다고 보기 어려움
(문제점 2) 분석을 위한 호출이 실패하면 전체 과정이 취소될 수 있음.
ex)getProfile에서 예외가 발생하면getName과getFriends또한 응답이 쓸모 없어짐
// 핵심 동작에 영향을 주지 않는 추가적인 연산은 다른 스코프에서 시작하는 것이 나음
val analyticsScope = CoroutineScope(SupervisorJob())
class ShowUserDataUseCase(
private val repo: UserDataRepository,
private val view: UserDataView,
private val analyticsScope: Coroutinescope,
) {
suspend fun showUserData() = coroutinescope {
val name = async { repo.getName() }
val friends = async { repo.getFriends() }
val profile = async { repo.getProfile() }
val user = User(
name = name.await(),
friends = friends.await(),
profile = profile.await()
)
view.show(user)
analyticsScope.launch { repo.notifyProfileShown() }
}
}