코루틴 빌더와 코루틴 스코프는 코틀린의 코루틴을 사용할 때 중요한 개념입니다. 이 둘의 차이점을 이해하면 코루틴의 구조와 동작 방식을 더 잘 이해할 수 있습니다. 아래에서 각각의 개념을 설명하고 그 차이점을 강조하겠습니다.
¶ 1. 코루틴 빌더 (Coroutine Builders)
코루틴 빌더는 새로운 코루틴을 생성하고 시작하는 역할을 합니다. 대표적인 코루틴 빌더로는
launch
,async
,runBlocking
,produce
등이 있습니다.
launch: 새로운 코루틴을 시작합니다. 반환 값이 없으며, 기본적으로 부모 코루틴의 컨텍스트를 상속받습니다. 부모가 취소되면 자식도 취소됩니다.
async: 새로운 코루틴을 시작하고, 결과를 반환하는
Deferred
객체를 반환합니다. 이 객체를 통해 결과를 기다릴 수 있습니다.runBlocking: 현재 스레드를 블록하고, 내부의 모든 코루틴이 완료될 때까지 대기합니다. 주로 메인 함수나 테스트에서 사용됩니다.
코루틴 빌더는 보통 상위 코루틴의 문맥을 상속받으며, 코루틴 빌더로 생성된 코루틴은 그 상위 코루틴의 자식으로 동작합니다. 상위 코루틴이 취소되면, 자식 코루틴도 함께 취소됩니다.
¶ 2. 코루틴 스코프 (Coroutine Scope)
코루틴 스코프는 하나 이상의 코루틴을 실행할 수 있는 범위를 정의합니다. 코루틴 스코프를 통해 코루틴들이 그룹화되며, 이 그룹 내의 모든 코루틴들은 같은 부모 코루틴을 가집니다.
coroutineScope
: 이 함수는 현재 스코프 내에서 새로운 코루틴 스코프를 생성합니다. 이 스코프에서 시작된 모든 코루틴이 완료될 때까지coroutineScope
블록은 종료되지 않습니다. 만약 이 스코프 내의 어떤 코루틴이 실패하면, 이 스코프 내의 모든 다른 코루틴들도 취소됩니다.coroutineScope
는 서스펜딩 함수이므로, 블록 내에서 코루틴이 비동기적으로 실행되더라도 함수 자체는 계속 진행되지 않고 대기합니다.
supervisorScope
:coroutineScope
와 비슷하지만, 이 스코프 내의 자식 코루틴 중 하나가 실패하더라도 다른 자식 코루틴에는 영향을 주지 않습니다. 즉, 개별적인 에러 전파를 허용하지 않는다는 점에서coroutineScope
와 차이가 있습니다.¶ 차이점 정리:
역할과 목적:
- 코루틴 빌더: 코루틴을 시작하는 데 사용됩니다. 이 빌더들은 특정 코루틴의 시작 지점 역할을 하며, 코루틴의 결과를 반환하거나(예:
async
) 단순히 작업을 시작합니다(예:launch
).- 코루틴 스코프: 여러 코루틴들을 그룹화하고, 그들이 모두 완료될 때까지 대기하는 범위를 제공합니다. 스코프는 코루틴을 관리하는 데 중점을 둡니다.
구조적 동시성:
- 코루틴 빌더: 부모 코루틴 내에서 자식 코루틴을 생성합니다. 이 자식 코루틴들은 부모와 독립적으로 실행되지만, 부모의 생명주기에 종속됩니다.
- 코루틴 스코프:
coroutineScope
는 내부적으로 자식 코루틴들이 종료될 때까지 기다리므로, 코루틴들이 구조적으로 정리될 수 있게 합니다. 스코프 내의 모든 코루틴이 완료될 때까지 스코프는 종료되지 않습니다.에러 전파:
- 코루틴 빌더:
launch
나async
로 시작된 코루틴에서 에러가 발생하면, 이 에러는 부모 코루틴으로 전파되어 부모가 취소될 수 있습니다.- coroutineScope: 이 스코프 내에서 한 코루틴이 실패하면, 스코프 내의 모든 코루틴들이 취소됩니다. 반면,
supervisorScope
는 에러가 다른 자식 코루틴에 전파되지 않도록 합니다.사용 예시:
- 코루틴 빌더: 주로 특정 작업이나 태스크를 비동기적으로 시작할 때 사용됩니다. 예를 들어, 파일 다운로드나 네트워크 요청 등.
- 코루틴 스코프: 서로 관련된 여러 비동기 작업을 그룹화하고, 이들이 구조적으로 관리되기를 원할 때 사용됩니다. 예를 들어, 여러 네트워크 요청을 병렬로 처리하고, 모든 요청이 완료될 때까지 기다리는 경우.
¶ 예시 코드:
// Coroutine Builder Example fun main() = runBlocking { launch { // 코루틴 빌더 - 새로운 코루틴 시작 delay(1000L) println("World!") } println("Hello,") } // Coroutine Scope Example suspend fun main() = coroutineScope { launch { // 스코프 내에서 코루틴 시작 delay(1000L) println("World!") } println("Hello,") // coroutineScope는 내부의 모든 코루틴이 완료될 때까지 대기 }
- 첫 번째 예제에서는
launch
가 새로운 코루틴을 시작하며, 부모 코루틴(즉,runBlocking
)의 흐름과 독립적으로 실행됩니다.- 두 번째 예제에서는
coroutineScope
내에서 코루틴이 실행되며, 스코프는 내부 코루틴이 모두 완료될 때까지 종료되지 않습니다.이러한 차이점을 통해 코루틴 빌더와 스코프의 역할을 이해할 수 있습니다. 코틀린에서 안전하게 비동기 작업을 관리하려면 이 개념을 잘 이해하는 것이 중요합니다.
async
를 쓸 수 있지만 async
는 스코프를 필요로 함. 그렇다고 GlobalScope
를 쓰는 것은 좋은 방법이 아님.
GlobalScope
: EmptyCoroutineContext
를 가진 빈 스코프일 뿐...public object GlobalScope: CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
GlobalScope
에서 async
를 호출하면?
suspend fun getUserProfile(scope: CoroutineScope): UserProfileData {
val user = scope.async { getUserData() }
val notifications = ascope.async { getNotifications() }
// ...
}
async
에서 예외 터지면 스코프 내의 모든 코루틴이 종료됨 (SupervisorJob
이 아닌 Job
의 경우)cancel
과 같은 메서드 사용해서 스코프를 취소하는 조작을 할 수도 있음coroutineScope
는 스코프를 시작하는 중단 함수이며, 인자로 들어온 함수가 생성한 값을 반환함suspend fun <R> coroutineScope(
block: suspend CoroutineScope.() -> R
): R
async
, launch
와는 다르게 리시버 없이 바로 호출됨coroutineScope
를 호출한 코루틴을 중단시키기 때문에 호출한 코루틴이 작업을 동시에 시작하지 않음 (p.129 코드 참고)coroutineScope
나 스코프에 속한 자식에서 예외가 발생하면 자식 소코플만 취소되고, 위로 다시 예외가 던져짐
대충 코드 짜서 먹여본 ChatGPT
두 코드 모두 getUserProfile()
함수를 사용하여 사용자 프로필 데이터를 비동기적으로 가져오고 있습니다. 하지만 이 함수가 CoroutineScope
의 확장 함수로 정의되었는지 아니면 coroutineScope
블록 내에서 정의되었는지에 따라 중요한 차이점이 발생합니다. 이 차이점은 함수 호출 시의 에러 전파와 자식 코루틴의 관리 방식에 영향을 미칩니다.
suspend fun CoroutineScope.getUserProfile(): UserProfileData {
val user = async { getUserData() }
val notification = async { getNotifications() } // 반드시 예외가 발생
return UserProfileData(user.await(), notification.await())
}
fun main(): Unit = runBlocking {
val details = try {
getUserProfile()
} catch (e: Throwable) {
null
}
val tweets = async { getTweets() }
println(details)
println(tweets)
}
getUserProfile
함수는 CoroutineScope
의 확장 함수로 정의되었습니다. 이는 getUserProfile
이 호출될 때, 호출된 스코프의 컨텍스트를 그대로 사용한다는 의미입니다.getUserProfile()
함수가 runBlocking
의 스코프에서 실행됩니다.notification
코루틴에서 예외가 발생하면, 이 예외는 상위 스코프인 runBlocking
으로 전파됩니다.getUserProfile()
함수뿐만 아니라 main
함수에서 시작한 tweets
코루틴도 취소됩니다.tweets
코루틴은 실행되지 못하고 예외로 인해 프로그램이 종료될 수 있습니다.suspend fun getUserProfile(): UserProfileData = coroutineScope {
val user = async { getUserData() }
val notification = async { getNotifications() } // 반드시 예외가 발생
return@coroutineScope UserProfileData(user.await(), notification.await())
}
fun main(): Unit = runBlocking {
val details = try {
getUserProfile()
} catch (e: Throwable) {
null
}
val tweets = async { getTweets() }
println(details)
println(tweets)
}
getUserProfile
함수는 coroutineScope
블록 내에서 정의되었습니다. coroutineScope
는 함수 내에서 독립적인 코루틴 스코프를 생성합니다.getUserProfile
내에서 시작된 모든 코루틴들은 이 새로운 스코프에 종속되며, 예외가 발생해도 상위 스코프인 runBlocking
에는 영향을 미치지 않습니다.notification
코루틴에서 예외가 발생하면, coroutineScope
내의 모든 코루틴들은 취소됩니다. 하지만 이 예외는 coroutineScope
블록 바깥으로 전파되지 않고, try-catch
블록에서 잡힙니다.getUserProfile()
함수 내에서 예외가 발생해도 상위 스코프인 runBlocking
의 다른 코루틴들(예: tweets
)은 영향을 받지 않고 정상적으로 실행됩니다.tweets
코루틴은 정상적으로 실행되며, tweets
의 결과를 출력할 수 있습니다.에러 전파:
코루틴 관리:
getUserProfile
함수가 호출된 스코프의 컨텍스트를 상속받으며, 해당 스코프에서 발생한 예외는 상위 스코프까지 전파됩니다.getUserProfile
은 독립적인 코루틴 스코프를 생성하여, 예외 발생 시에도 상위 스코프에 영향을 미치지 않습니다.두 코드의 주요 차이점은 에러 전파 방식과 코루틴 실행의 독립성에 있습니다. 코루틴 스코프 내에서 독립적으로 코루틴을 실행하고 예외 처리로 인해 상위 스코프가 영향을 받지 않도록 하려면 coroutineScope
를 사용하는 것이 적합합니다. 반면, 특정 스코프의 컨텍스트를 그대로 사용하고, 예외가 발생했을 때 상위 스코프까지 취소되기를 원한다면 CoroutineScope
의 확장 함수로 정의하는 것이 적합합니다.
coroutineScope
는 중단 메인 함수 본체를 래핑할 때 주로 사용됨 (runBlocking
을 coroutineScope
가 대체)coroutineScope
는 기존의 중단 컨텍스트에서 벗어나 새로운 스코프를 만들고, 부모로 부터 스코프 상속, 구조화된 동시성을 지원함supervisorScope
: coroutineScope
와 비슷하지만 Job
대신 SupervisorJob
사용함withContext
: 코루틴 컨텍스트를 바꿀 수 있는 coroutineScope
임withTimeout
: 타임아웃이 있는 coroutineScope
임runBlocking
제외)
launch
, async
, produce
CoroutineScope
의 확장 함수CoroutineScope
리시버의 코루틴 컨텍스트를 사용Job
을 통해 부모로 전파됨coroutineScope
, supervisorScope
, withContext
, withTimeout
runBlocking
은 코루틴 빌더보다 스코프 함수와 더 비슷해보임.
차이는runBlocking
은 블로킹 함수지만, 코루틴 스코프 함수는 중단 함수임.
runBlocking
은 코루틴 계층에서 가장 상위에 있으며, 코루틴 스코프 함수는 계층 중간쯤에 있는 것임
coroutineScope
와 비슷하지만 스코프의 컨텍스트를 변경할 수 있음withContext(EmptyCoroutineContext)
는 coroutineScope()
와 일치함launch(Dispatchers.Main) {
view.showProgressBar()
withContext(Dispatchers.IO) {
fileRepository.saveData(data)
}
view.hideProgressBar()
}
coroutineScope
와의 차이는 컨텍스트의 Job
을 SupervisorJob
으로 오버라이딩하므로 자식 코루틴이 예외를 던져도 취소되지 않음withContext(SupervisorJob())
은 supervisorScope
와 동일하게 사용할 수 없음suspend fun test(): Int = withTimeout(1500) {
// ...
}
CancellationException
서브 타입인 TimeoutCancellationException
을 던짐 (부모에게 영향 X)withTimeoutOrNull
도 있음. 예외를 던지지 않고, null 을 반환함suspend fun calculateAnswerOrNull(): User? =
withContext(Dispatchers.Default) {
withTimeoutOrNull(1000) {
calculateAnswer()
}
}
val analyticsScope = CoroutineScope(SupervisorJob())
// p.141 하단의 코드와 비교 필요
class ShowUserDataUseCase(
private val repo: UserDataRepository,
private val view: UserDataView,
private val analyticsScope: CotoutineScope
) {
suspend fun showUserData() = coroutineScope {
val name = async { repo.getName() }
val friends = async { repo.getFriends() }
val profile = async { repo.getProfile() }
val user = User( name.await(), friends.await(), profile.await() )
view.show(user)
analyticsScope.launch { repo.notifyProfileShown() }
}
}
근데 앞에서 스코프 인자로 넘기지 말로고 하지 않았나..? 생성자로 주입하는건 뭔가 다른가??