| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- Functional Programming
- 코틀린멀티플랫폼
- factory method
- define
- ㅋㅁ
- 코루틴
- 빌터패턴
- Kotlin
- 함수형프로그래밍
- kotlin multiplatform
- android designsystem
- Design Pattern
- 안드로이드 디자인시스템
- kmp
- 옵저버 패턴
- 팩토리 메소드
- 디자인패턴 #
- Coroutines
- compose
- 디자인패턴
- 코틀린
- PrototypePattern
- 추상팩토리패턴
- 추상 팩토리
- builderPattern
- Observer Pattern
- designPattern
- material3
- Abstract Factory
- 프로토타입 패턴
- Today
- Total
오늘도 더 나은 코드를 작성하였습니까?
11장 코루틴 스코프 함수 - 1 본문
여러개의 엔드포인트에서 동시에 데이터를 얻어야하는 중단함수.
suspend fun getUserProfile(): UserProfileData {
val user = getUserData()
val notifications = getNotifications()
return UserProfileData(
user = user,
notifications = notifications
)
}
동시에 사용자 데이터와 노티정보를 가져오려면 async{ ... } 빌더를 이용해야 한다.
async => Coroutinescope를 필요로 한다.
1. GlobalScope (하면안됨)
suspend fun getUserProfile(): UserProfileData {
val user = GlobalScope.async { getUserData() }
val notifications = GlobalScope.async { getNotifications() }
return UserProfileData(
user = user.await(),
notifications = notifications.await()
)
}
GlobalScope는 EmptyCoroutineContext를 가진 스코프일 뿐이다.
GlobalScope에서 async{ ... }를 호출하여 코루틴을 생성하면 부모와 아무런 관련이 없는 코루틴이 생성된다.
부모로 부터 scope를 상속 받지 않는다.
부모가취소 되어도 자식은 취소되지 않고, 작업이 끝날때까지 자원이 낭비 된다.
2. 스코프를 인자로 넘겨주기 (하면안됨)
suspend fun getUserProfile(
scope: CoroutineScope,
): UserProfileData {
val user = scope.async { getUserData() }
val notifications = scope.async { getNotifications() }
return UserProfileData(
user = user.await(),
notifications = notifications.await()
)
}
스코프가 함수의 인자로 전달되면 예상하지 못한 부작용이 발생할 수 있습니다.
1. async 에서 예외가 발생하면 모든 스코프가 닫히게 된다.
2. 함수가 cancel 메서드를 사용해 스코프를 취소하는 등 스코프를 조작할 수도 있다.
import kotlinx.coroutines.*
fun main(): Unit = runBlocking {
val details = try {
getUserDetails()
} catch (e: Error) {
null
}
val tweets = async { getTweets() }
println("User: $details")
println("Tweets: ${tweets.await()}")
}
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 "marcinmoskala"
}
suspend fun getTweets(): List<Tweet> {
return listOf(Tweet("Hello, world"))
}
suspend fun CoroutineScope.getUserDetails(): Details{
val userName = async { getUserName() }
val followers = async { getFollowersNumber() }
return Details(
name = userName.await(),
followers = followers.await()
)
}
//Execution failed for task ':MainKt.main()'.
runBlocking (Root)
└── getUserDetails (Scope)
├── async (userName)
└── async (followers) <-- 여기서 Error 발생!
- 에러 발생: followers 코루틴에서 Error가 던져집니다.
- 부모 보고: 자식인 followers는 부모(getUserDetails 스코프)에게 "나 죽었어!"라고 에러를 보고합니다.
- 부모의 반응 (중요): 부모는 일반적인 Job 체계이므로 자식의 에러를 받자마자 자신을 취소 상태로 바꿉니다.
- 형제 취소: 부모가 취소 상태가 되었으므로, 아직 실행 중인 다른 자식(userName)에게 취소 신호를 보냅니다.
- 상위 전파: 부모는 받은 에러를 다시 자신의 부모(runBlocking)에게 던집니다.
- try-catch 무력화: runBlocking 레벨에서 try-catch가 있더라도, 이미 코루틴 트리 전체에 취소 신호가 퍼졌기 때문에 정상적인 복구가 불가능해지고 프로그램이 종료됩니다.
try-catch가 무용지물인 이유
코드에서 val details = try { getUserDetails() } catch (e: Error) { null }라고 작성했지만, getUserDetails 내부에서 async로 실행된 코루틴은 별도의 제어 흐름을 가집니다.
- async 내부에서 터진 에러는 await()을 호출할 때 던져지기도 하지만, 그전에 이미 코루틴 계층 구조를 타고 부모를 취소시킵니다.
- 부모인 runBlocking이 자식의 에러 때문에 취소 상태가 되면, 밖에서 감싼 try-catch는 의미가 없어집니다. 이미 전체 작업 흐름이 파괴되었기 때문입니다.
coroutineScope
coroutineScope는 스코프를 시작하는 "중단함수" 인자로 들어온 함수가 생성한 값을 반환한다.
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
새로운 코루틴을 생성하지만, 새로운 코루틴이 끝날때 까지 coroutineScope를 호출한 코루틴을 중단한다.
=> 호출한 코루틴은 작업을 동시에 시작하지 않는다.
fun main(): Unit = runBlocking {
val a = coroutineScope {
delay(1000)
10
}
println("a is calculated")
val b = coroutineScope {
delay(1000)
20
}
println(a)
println(b)
}
//a is calculated
//10
//20
새롭게 생성된 스코프는 coroutineContext를 상속받지만, 컨텍스트의 Job을 오버라이딩한다. 스코프는 부모가 해야할 책임을 이어 받는다.
- 부모로부터 컨텍스트를 상속받습니다.
- 자신의 작업을 끝내기 전까지 모든 자식을 기다립니다.
- 부모가 취소되면 자식들 모두 취소합니다.
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
suspend fun longTask() = coroutineScope {
launch {
delay(1000L)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 1")
}
launch {
delay(2000L)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 2")
}
}
fun main() = runBlocking(CoroutineName("Parent")) {
println("Before")
longTask()
println("After")
}
//Before
//[Parent] Finished task 1
//[Parent] Finished task 2
//After
suspend fun longTask() = coroutineScope {
launch {
delay(1000L)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 1")
}
launch {
delay(2000L)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 2")
}
}
fun main() = runBlocking {
val job = launch(CoroutineName("Parent")) {
longTask()
}
delay(1500L)
job.cancel()
}
// [Parent] Finished task 1
*코루틴 빌더와 달리 coroutineScope 또는 스코프에 속한 자식들이 예외가 발생하면, 다른 자식들이 취소되고 다시 예외가 던져집니다.
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main(): Unit = runBlocking<Unit>{
val details = try {
getUserDetails()
} catch (e: ApiException) {
null
}
val tweets = async { getTweets() }
println("User: $details")
println("Tweets: ${tweets.await()}")
}
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 followers = async { getFollowersNumber() }
Details(
name = userName.await(),
followers = followers.await()
)
}
중단함수에서 병렬로 작업을 수행할 경우 coroutineScope를 사용하는것이 좋다.
coroutineScope는 메인 함수 본체를 래핑할 때 주로 사용됩니다.
coroutineScope함수는 기존의 중단 컨텍스트에서 벗어난 새로운 스코프를 만듭니다. 부모로부터 스코프를 상속받고 구조화된 동시성을 지원한다.
1. "기존 중단 컨텍스트에서 벗어난 새로운 스코프"
coroutineScope 블록 안에서 일어나는 일들을 하나의 그룹으로 묶는다는 뜻입니다.
- 독립된 울타리: coroutineScope 내부에서 여러 개의 async나 launch를 실행하면, 이들은 이 블록이라는 울타리 안에 갇힙니다.
- 전원 대기: 이 블록은 내부의 모든 자식 코루틴이 끝날 때까지 부모를 기다리게 만듭니다. 즉, **"내 안의 자식들이 다 끝날 때까지 이 줄(Line) 아래로는 못 지나간다"**는 경계선 역할을 합니다.
2. "부모로부터 스코프(컨텍스트)를 상속받고"
완전히 남남이 되는 게 아닙니다. 부모가 가진 **유전자(CoroutineContext)**를 그대로 물려받습니다.
- Dispatcher 상속: 부모가 메인 스레드(Dispatchers.Main)에서 실행 중이었다면, coroutineScope 내부의 코루틴들도 기본적으로 메인 스레드에서 돌아갑니다.
- 로그 및 설정 유지: 부모 코루틴에 설정된 이름이나 디버깅 정보가 그대로 이어집니다.
3. "구조화된 동시성(Structured Concurrency)을 지원한다"
이것이 가장 중요한 '책임감'의 문제입니다. 아래 그림 같은 계층 구조를 형성합니다.
- 동반 자살(에러 전파): 만약 coroutineScope 안의 자식 중 하나가 에러(예외)를 내면, coroutineScope는 **"우리 집안은 끝났어!"**라며 다른 자식들을 모두 취소시키고 부모에게 에러를 보고합니다.
- 완료 보장: 부모 코루틴은 coroutineScope 블록이 다 끝나기 전에는 절대로 먼저 종료되지 않습니다.
'Coroutine > 코틀린 코루틴' 카테고리의 다른 글
| 10장 예외처리 (0) | 2026.02.09 |
|---|---|
| 9장 취소 (0) | 2026.02.01 |
| 8장 Job과 자식 코루틴 기다리기. (0) | 2026.01.26 |
| 7장 코루틴 컨텍스트 (1) | 2026.01.19 |
| 6장 코루틴 빌더 (0) | 2026.01.05 |
