코루틴 라이브러리가 제공하는 세 가지 필수적인 코루틴 빌더를 탐색해보자 🔍
launch, runBlocking, async 세 가지의 코루틴 빌더를 제공함launch가 작동하는 방식은 thread 함수를 호출하여 새로운 스레드르 시작하는 것과 비슷함
// launch 사용 예제
fun main() {
GloablScope.launch {
delay(1000L)
println("World!")
}
GloablScope.launch {
delay(1000L)
println("World!")
}
GloablScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
}
// Hello,
// (1초 후)
// World!
// World!
// World!
launch 함수는 CoroutineScope 인터페이스의 확장 함수CoroutineScope 인터페이스는 부모 코루틴과 자식 코루틴 사이의 관계를 정립하기 위한 목적으로 사용되는 구조화된 동시성 (structured concurrency)의 핵심❗실제 현업에서는
GlobalScope의 사용 지양
❗
main함수 마지막에sleep이 없었다면 코루틴을 실행하자마자 끝나게 되며, 코루틴이 실행될 기회도 없음 ⏳
launch가 작동하는 방식은 데몬 스레드와 어느 정도 비슷하지만 훨씬 가벼움// 데몬 스레드 사용 예제
fun main() {
thread(isDaemon = true) {
delay(1000L)
println("World!")
}
thread(isDaemon = true) {
delay(1000L)
println("World!")
}
thread(isDaemon = true) {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
}
runBlocking은 코루틴이 중단될 때 현재 스레드를 중단시켜 Thread.sleep과 유사한 작동을 함fun main() {
runBlocking {
delay(1000L)
println("World!")
}
runBlocking {
delay(1000L)
println("World!")
}
runBlocking {
delay(1000L)
println("World!")
}
println("Hello,")
}
// (1초 후)
// World!
// (1초 후)
// World!
// (1초 후)
// World!
// Hello,
// 아래 함수와 결과가 동일
fun main() {
Thread.sleep(1000L)
println("World!")
Thread.sleep(1000L)
println("World!")
Thread.sleep(1000L)
println("World!")
println("Hello,")
}
runBlocking이 사용되는 경우runBlocking의 현재runBlocking은 `코루틴 빌더로 중요하게 사용되었지만 현재는 거의 사용되지 않음
유닛테스트에서는 코루틴을 가상 시간으로 실행시키는 runTest가 주로 사용됨
대신 suspend를 붙여 중단 함수로 만드는 방법을 주로 사용함
async는 launch와 비슷하지만 값을 생성하기 위한 코루틴 빌더Deferred<T> 타입 반환 (여기서 T는 생성되는 값의 타입)await()를 통해 값이 준비될 때까지 대기 가능fun main() = runBlocking {
val resultDeferred: Deferred<Int> = GlobalScope.async {
delay(1000L)
42
}
// 다른 작업 수행
val result: Int = resultDeferred.await() // (1초 후)
println(result) // 42
// 간단하게 작성할 수도 있음
println(resultDeferred.await()) // 42
async 빌더는 호출되자마자 코루틴을 즉시 시작Deferred는 값이 생성되면 해당 값을 내부에 저장하기 때문에 await에서 값이 반환되는 즉시 값을 사용할 수 있음await을 호출하면 값이 나올 때까지 기다리게 됨fun main() = runBlocking {
val res1 = GlobalScope.async {
delay(1000L)
"Text 1"
}
val res2 = GlobalScope.async {
delay(3000L)
"Text 2"
}
val res3 = GlobalScope.async {
delay(2000L)
"Text 3"
}
println(res1.await())
println(res2.await())
println(res3.await())
}
// (1초 후)
// Text 1
// (2초 후)
// Text 2
// Text 3
‼️
async는 값을 생성할 때 사용되며, 값이 필요하지 않을 때는launch를 써야 함
GlobalScope를 사용하면 부모-자식 관계 없이 코루틴이 독립적으로 실행되어 프로그램 종료를 막을 수 없음GlobalScope가 필요한 이유는 무엇일까?
runBlocking는 block 파라미터가 리시버 타입이 CoroutineScope인 함수형 타입
fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Deferred<T>
fun main() = runBlocking {
this.launch {
delay(1000L)
println("World!")
}
launch {
delay(2000L)
println("World!")
}
println("Hello,")
}
// Hello,
// (1초 후)
// World!
// (1초 후)
// World!
구조화된 동시성이라는 관계가 성립됨runBlocking은 CoroutineScope의 확장 함수가 아니므로 자식이 될 수 없고, 루트 코루틴으로만 사용될 수 있음. 따라서 runBlocking은 다른 코루틴과 쓰임새가 다름
// Android
class NetworkUserRepository(
private val api: UserApi,
): UserRepository {
suspend fun getUser(): User = api.getUser().toDomainUser()
}
class NetworkNewsRepository(
private val api: NewsApi,
private val settings: SettingsRepository,
): NewsRepository {
suspend fun getNews(): List<News> = api.getNews()
.map { it.toDomainNews() }
suspend fun getNewsSummary(): List<News> {
val type = settings.getNewsSummaryType()
return api.getNewsSummary(type)
}
}
class MainPresenter(
private val view: MainView,
private val userRepo: UserRepository,
private val newsRepo: NewsRepository,
): BasePresenter {
fun onCreate() {
scope.launch {
val user = userRepo.getUser()
view.showUserData(user)
}
scope.launch {
val news = async {
newsRepo.getNews()
.sortedByDescending { it.date }
}
val newsSummary = async {
newsRepo.getNewsSummary()
}
view.showNews(newsSummary.await(), news.await())
}
}
}
// Backend (Spring WebFlux)
@Controller
class UserController(
private val tokenService: TokenService,
private val userService: UserService,
) {
@GetMapping("/me")
suspend fun findUser(
@PathVariable userId: String,
@RequestHeader("Authorization") authorization: String,
): UserJson {
val userId = tokenService.readUserId(authorization)
val user = userService.findUserById(userId)
return user.toJson()
}
}
중단 함수에서는 스코프를 어떻게 처리할까?
레포지토리 함수에서 비동기적으로 두 개의 자원(사용자 데이터와 글 목록)을 가지고 오는 상황
사용자가 볼 수 있는 글만 반환하고 싶다고 가정
async를 호출하려면 스코프가 필요하지만 함수에 스코프를 넘기고 싶지 않는 경우
suspend fun getArticleForUser(
userToken: String?,
): List<ArticleJson> = coroutineScope {
val articles = async { articleRepository.getArticles() }
val user = userService.getUser(userToken)
articles.await()
.filter { canSeeOnList(user, it) }
.map { toArticleJson(it) }
}
suspend fun main(): Unit = coroutineScope {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
// Hello,
// (1초 후)
// World!
