지난 1편에서 코루틴 탄생 배경과 기본 개념을 간단히 살펴보았습니다.
이번 글에서는 코루틴의 기초 API를 좀 더 구체적으로 파악하고, 비동기 로직을 간결하게 작성할 수 있는 방법을 예제를 통해 알아보겠습니다.
핵심 키워드
• suspend 함수
• launch
• async / await
• runBlocking
• CoroutineScope
1. suspend 함수 복습하기
1.1 suspend란 무엇인가
suspend는 코루틴 환경에서 “일시 중단 가능한 함수”를 의미합니다.
내부적으로 비동기 I/O나 지연(Delay) 작업을 수행할 때, 스레드를 고정적으로 점유하지 않고 일시 중단(suspend)했다가, 필요한 시점에 다시 재개(resume)하여 계속 코드를 실행할 수 있게 해줍니다.
suspend fun fetchData(): String {
// 예: 네트워크 호출
// 일시 중단되었다가, 결과를 받으면 재개
return "Server Response"
}
• suspend 함수는 반드시 코루틴 스코프 안에서 호출해야 합니다.
• 안드로이드 환경에서는 lifecycleScope나 viewModelScope 등에서 suspend 함수를 사용하며, 테스트나 간단한 예제에서는 runBlocking으로 스코프를 만들 수 있습니다.
2. runBlocking으로 살펴보는 기본 예제
2.1 runBlocking이란?
runBlocking은 메인 스레드에서 코루틴을 사용해볼 때, 해당 코루틴이 종료될 때까지 블로킹(Blocking)으로 기다려주는 함수입니다.
실제 앱보다는 테스트나 간단한 실습 목적으로 자주 쓰입니다.
fun main() = runBlocking {
println("Start")
val result = fetchData()
println("Received: $result")
println("End")
}
suspend fun fetchData(): String {
delay(1000L) // 1초 대기 (일시 중단)
return "Server Response"
}
• 이 예제에서는 “Start”가 출력된 후 fetchData()에서 1초간 일시 중단됩니다.
• 이후 “Received: Server Response”가 출력되고, 마지막으로 “End”가 표시됩니다.
• runBlocking이 전체 코루틴이 끝날 때까지 메인 스레드를 블로킹해주므로, 프로그램이 모든 처리를 마친 뒤 종료됩니다.
안드로이드 앱 코드에서는 runBlocking을 직접 사용하는 일이 드물고, 대신 코루틴 스코프(예: lifecycleScope, viewModelScope)를 활용해 동작합니다.
3. launch로 코루틴 실행하기
3.1 launch의 기본 구조
coroutineScope.launch {
// 이 안에서 비동기 작업을 수행
// 반환값은 없으며, 별도의 Job 형태로 동작
}
• launch는 결과를 반환하지 않는 일회성 비동기 작업에 적합합니다.
• launch가 반환하는 값은 Job 객체이며, job.cancel() 등을 통해 코루틴을 취소할 수 있습니다.
3.2 여러 launch 동시 실행 예시
runBlocking {
launch(Dispatchers.IO) {
println("IO 1 start")
delay(1000)
println("IO 1 end")
}
launch(Dispatchers.IO) {
println("IO 2 start")
delay(500)
println("IO 2 end")
}
println("Main block end")
}
• 먼저 “Main block end”가 출력된 뒤, 0.5초가 지난 후 “IO 2 end”, 1초 뒤 “IO 1 end”가 차례대로 나타납니다.
• runBlocking이 해당 스코프 내부의 모든 코루틴이 끝날 때까지 대기하기 때문에, 두 launch 블록이 완료된 이후 프로그램이 종료됩니다.
이처럼 launch는 동시에 여러 비동기 작업을 실행하기 편리하며, 결과값이 필요 없는 상황에 자주 사용됩니다.
4. async와 await로 결과 받기
4.1 async의 구조
val deferred = coroutineScope.async {
// 비동기로 작업 수행 후
// 최종 결과를 반환
"Result Data"
}
val result = deferred.await()
• async는 내부 연산 결과값을 반환할 수 있습니다. (Deferred<T> 타입)
• 결과가 필요할 때 await()를 호출하면, 해당 코루틴이 끝날 때까지 일시 중단된 후 결과를 얻습니다.
4.2 병렬 연산 예제
runBlocking {
val deferred1 = async(Dispatchers.IO) { getUserInfo() }
val deferred2 = async(Dispatchers.IO) { getProfileImage() }
// 두 코루틴을 병렬로 실행
// 각 결과를 받아 합친 뒤 최종 데이터 생성
val user = deferred1.await()
val image = deferred2.await()
println("User: $user, ProfileImage: $image")
}
1. async(Dispatchers.IO)를 통해 각각 비동기 API를 호출하고, 병렬로 처리하게 만듭니다.
2. await()는 해당 코루틴이 완료될 때까지 일시 중단되었다가 결과를 돌려줍니다.
3. 최종적으로 user 정보와 image 정보를 합쳐서 필요한 로직을 수행할 수 있습니다.
4.3 launch vs. async
• launch: 결과를 반환하지 않는 단발성 비동기 작업
• 반환 타입: Job
• 작업이 끝나도 별도의 결과값을 반환하지 않음
• async: 결과값이 필요한 병렬 연산에 적합
• 반환 타입: Deferred<T>
• await()를 통해 비동기 처리 결과를 받을 수 있음
5. CoroutineScope 활용하기
5.1 실무에서 주로 사용하는 스코프
안드로이드 환경에서 runBlocking은 거의 쓰이지 않고, 다음과 같은 스코프를 자주 사용합니다.
1. GlobalScope: 앱 실행 중 계속 살아있는 전역 스코프(권장되지 않음)
2. lifecycleScope: Activity/Fragment의 Lifecycle에 맞춰 코루틴을 자동으로 취소
3. viewModelScope: ViewModel이 해제될 때 함께 코루틴을 취소
4. 직접 생성한 CoroutineScope: CoroutineScope(Dispatchers.Default) 등으로 필요에 맞춰 만들 수 있음
class MyViewModel : ViewModel() {
fun loadData() = viewModelScope.launch {
val data = fetchData()
// UI 로직 처리
}
}
• viewModelScope.launch를 사용하면, ViewModel이 해제될 때 자동으로 해당 코루틴이 취소되어 메모리 누수를 예방할 수 있습니다.
5.2 coroutineScope { }와 supervisorScope { }
• coroutineScope { }
• 블록 내부에서 launch, async로 만든 자식 코루틴이 모두 종료될 때까지 대기합니다.
• 자식 코루틴 중 하나에서 예외가 발생하면 부모 스코프 전체가 취소될 수 있습니다.
• supervisorScope { }
• 자식 코루틴 중 하나가 실패해도, 나머지 자식들은 독립적으로 동작합니다.
• 추후 Structured Concurrency(3편)에서 자세히 다룰 예정입니다.
마무리 및 다음 예고
이번 글에서는 코루틴의 기초 API인 suspend, launch, async, 그리고 runBlocking을 중심으로 살펴보았습니다.
간단히 요약하면 다음과 같습니다.
1. suspend 함수: 일시 중단 가능한 함수로, 코루틴 환경에서 효율적인 비동기 처리를 지원
2. launch: 결과가 필요 없는 비동기 작업에 적합, 반환값은 Job
3. async: 비동기 연산 결과를 받아야 할 때 사용, 반환값은 Deferred<T>
4. runBlocking: 예제나 테스트 목적일 때만 사용, 실제 안드로이드 코드는 lifecycleScope나 viewModelScope 등이 주를 이룸
5. CoroutineScope: 안드로이드 실무에서는 생명주기(Lifecycle)나 ViewModel 범위에 맞춰 코루틴을 관리
다음 3편에서는 Structured Concurrency와 관련된 개념을 좀 더 깊이 있게 다룰 예정입니다.
“부모-자식 코루틴” 구조가 어떻게 동작하는지, 예외나 취소가 어떤 경로로 전파되는지 등을 구체적인 예제로 살펴보도록 하겠습니다.
시리즈 전체 목차
1. 코루틴 기초(suspend, CoroutineScope, Dispatcher)
2. Structured Concurrency와 코루틴 스코프 관리
5. Flow 심화(Flow, StateFlow, SharedFlow)
6. 안드로이드 실무 적용(Retrofit, Room, ViewModel)
7. 동시성 문제 해결(Mutex, Semaphore)
'코딩' 카테고리의 다른 글
[코루틴 (4)] 예외 처리와 취소(Cancellation) (0) | 2025.01.17 |
---|---|
[코루틴 (3)] Structured Concurrency와 코루틴 스코프 (0) | 2025.01.17 |
[코루틴 (1)] 코루틴 탄생 배경과 기본 개념 (0) | 2025.01.15 |
고급 스크립팅 & 커스텀 플러그인 (0) | 2025.01.15 |
CI/CD 파이프라인 연동 (2) | 2025.01.14 |