-
코틀린 코루틴 핵심 정리(개념부터 예제까지)-고슴도치 군단카테고리 없음 2026. 5. 8. 17:00

코루틴 개요
코틀린 코루틴(Coroutine)은 비동기 코드를 마치 동기 코드처럼 순차적으로 작성할 수 있게 해주는 도구임
기존 안드로이드 개발에서는 AsyncTask나 RxJava로 비동기 처리를 해왔지만, 콜백이 중첩되면서 코드가 복잡해지는 문제가 있었음
코루틴을 사용하면 네트워크 요청, DB 조회 등 시간이 걸리는 작업을 간결하고 읽기 쉬운 코드로 처리할 수 있음
구글은 2019년 이후 안드로이드 공식 문서에서 AsyncTask 대신 코루틴 사용을 권장하고 있으며, 현재 대부분의 신규 안드로이드 프로젝트에서 코루틴이 기본으로 사용됨
핵심 개념 정리
코루틴을 이해하려면 세 가지 핵심 개념을 먼저 파악해야 함
CoroutineScope는 코루틴이 실행되는 범위를 정의하며, 스코프가 종료되면 그 안의 코루틴도 함께 취소됨
Dispatcher는 코루틴이 어떤 스레드에서 실행될지를 결정하는 역할을 함
Dispatchers.Main: UI 스레드에서 실행 — 화면 갱신, 뷰 업데이트에 사용Dispatchers.IO: 파일 읽기, 네트워크, DB 등 I/O 작업에 최적화Dispatchers.Default: CPU 연산이 많은 작업(정렬, 파싱 등)에 사용
launch와 async는 코루틴을 시작하는 두 가지 방법임
launch는 결과값을 반환하지 않는 코루틴을 시작하고,async는Deferred객체를 통해 결과값을 받을 수 있음
기본 예제
가장 간단한 코루틴 예제부터 시작함
아래 코드는
runBlocking내에서 코루틴을 실행하는 기본 형태이며, 입문 단계에서 동작 원리를 파악하기에 적합함import kotlinx.coroutines.* fun main() = runBlocking { launch { delay(1000L) println("1초 후 실행") } println("즉시 실행") // 출력: "즉시 실행" → 1초 후 "1초 후 실행" }delay()는 Thread.sleep()과 달리 스레드를 블로킹하지 않고 코루틴만 일시 중단함이 차이가 코루틴의 핵심 장점이며, 하나의 스레드에서 수천 개의 코루틴을 동시에 실행할 수 있는 이유임
여러 코루틴을 병렬로 실행하고 모두 완료될 때까지 기다리려면
launch를 여러 개 사용한 뒤joinAll()을 호출함fun main() = runBlocking { val job1 = launch { delay(1000L); println("작업1 완료") } val job2 = launch { delay(500L); println("작업2 완료") } joinAll(job1, job2) println("모든 작업 완료") }
async와 Deferred 활용
결과값이 필요한 비동기 작업에는
async를 사용함async는Deferred<T>타입을 반환하며,await()를 호출하는 시점에 결과값을 받아옴아래 예제는 두 API 호출을 동시에 실행해 응답 시간을 절반으로 줄이는 패턴임
suspend fun fetchUserName(): String { delay(1000L) // 실제 API 호출로 교체 return "김철수" } suspend fun fetchUserScore(): Int { delay(1000L) return 98 } fun main() = runBlocking { val nameDeferred = async { fetchUserName() } val scoreDeferred = async { fetchUserScore() } // 두 작업이 병렬 실행 → 총 소요 시간 약 1초 (순차 실행 시 2초) val name = nameDeferred.await() val score = scoreDeferred.await() println("$name 님의 점수: $score") }순차 실행이었다면 2초가 걸렸을 작업을 병렬 처리로 약 1초 만에 완료할 수 있음
실제 프로젝트에서 여러 API를 동시에 호출해야 할 때 이 패턴이 자주 사용됨
안드로이드 ViewModel 활용 예제
안드로이드 실무에서는
viewModelScope를 이용해 코루틴을 관리하는 것이 표준 패턴임viewModelScope를 사용하면 ViewModel이 소멸될 때 코루틴이 자동으로 취소되어 메모리 누수를 방지할 수 있음아래 예제는 Retrofit 등의 네트워크 라이브러리 호출을 코루틴으로 처리하는 일반적인 구조임
class MainViewModel : ViewModel() { private val _uiState = MutableLiveData<String>() val uiState: LiveData<String> = _uiState fun loadUserData() { viewModelScope.launch { _uiState.value = "로딩 중..." val result = withContext(Dispatchers.IO) { // 네트워크 요청 또는 DB 조회 (여기서는 예시) fetchDataFromServer() } // withContext 완료 후 Main 스레드에서 자동 실행 _uiState.value = result } } private suspend fun fetchDataFromServer(): String { delay(2000L) // 실제 Retrofit suspend fun으로 교체 return "서버에서 가져온 사용자 데이터" } }withContext(Dispatchers.IO)는 해당 블록만 IO 스레드에서 실행하고, 블록이 끝나면 원래 스코프(Main)로 자동 복귀함이 덕분에 스레드 전환 코드를 별도로 작성하지 않아도 되며, 코드 흐름이 직관적으로 유지됨
Fragment나 Activity에서는
lifecycleScope를 사용하는 방식도 있으나, 화면 회전 등 구성 변경 시 작업이 취소될 수 있으므로 ViewModel + viewModelScope 조합이 더 안정적임
예외 처리와 취소
코루틴에서 예외 처리는 일반 코드와 동일하게
try-catch로 처리함단,
CancellationException은 코루틴 취소 시 자동으로 발생하는 예외로, catch 블록에서 잡지 않는 것이 원칙임fun loadData() { viewModelScope.launch { try { val result = withContext(Dispatchers.IO) { riskyNetworkCall() // IOException 등 발생 가능 } _uiState.value = result } catch (e: IOException) { _uiState.value = "네트워크 오류. 다시 시도해주세요." } // CancellationException은 catch하지 않음 } }CoroutineExceptionHandler를 사용하면 스코프 전체에 공통 예외 처리기를 등록할 수 있음val handler = CoroutineExceptionHandler { _, exception -> Log.e("ViewModel", "Unhandled exception: ${exception.message}") } viewModelScope.launch(handler) { // 예외 처리가 필요한 코루틴 작업 }코루틴 취소는
Job.cancel()로 명시적으로 처리하거나,viewModelScope처럼 라이프사이클과 연동된 스코프에서 자동으로 처리됨
핵심 요약
코틀린 코루틴은 비동기 코드를 순차적으로 작성할 수 있게 해주는 안드로이드 개발의 핵심 도구임
launch는 반환값 없는 코루틴,async는 반환값 있는 코루틴에 사용하며, 안드로이드에서는viewModelScope와 함께 사용하는 것이 권장됨withContext(Dispatchers.IO)로 I/O 작업을 분리하고, 결과는Dispatchers.Main에서 처리하는 패턴이 가장 자주 쓰이는 기본 구조임코루틴을 처음 접한다면
launch + delay기본 예제부터 시작해,viewModelScope + withContext(Dispatchers.IO)패턴으로 단계적으로 확장하는 것이 효과적임안드로이드 개발에서 코루틴은 이제 선택이 아닌 필수 기술이므로, 이번 예제들을 직접 실행해보며 익숙해지는 것을 권장함