ABOUT ME

-

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

    코루틴 개요

    코틀린 코루틴(Coroutine)은 비동기 코드를 마치 동기 코드처럼 순차적으로 작성할 수 있게 해주는 도구임

    기존 안드로이드 개발에서는 AsyncTask나 RxJava로 비동기 처리를 해왔지만, 콜백이 중첩되면서 코드가 복잡해지는 문제가 있었음

    코루틴을 사용하면 네트워크 요청, DB 조회 등 시간이 걸리는 작업을 간결하고 읽기 쉬운 코드로 처리할 수 있음

    구글은 2019년 이후 안드로이드 공식 문서에서 AsyncTask 대신 코루틴 사용을 권장하고 있으며, 현재 대부분의 신규 안드로이드 프로젝트에서 코루틴이 기본으로 사용됨



    핵심 개념 정리

    코루틴을 이해하려면 세 가지 핵심 개념을 먼저 파악해야 함

    CoroutineScope는 코루틴이 실행되는 범위를 정의하며, 스코프가 종료되면 그 안의 코루틴도 함께 취소됨

    Dispatcher는 코루틴이 어떤 스레드에서 실행될지를 결정하는 역할을 함

    • Dispatchers.Main: UI 스레드에서 실행 — 화면 갱신, 뷰 업데이트에 사용
    • Dispatchers.IO: 파일 읽기, 네트워크, DB 등 I/O 작업에 최적화
    • Dispatchers.Default: CPU 연산이 많은 작업(정렬, 파싱 등)에 사용

    launchasync는 코루틴을 시작하는 두 가지 방법임

    launch는 결과값을 반환하지 않는 코루틴을 시작하고, asyncDeferred 객체를 통해 결과값을 받을 수 있음



    기본 예제

    가장 간단한 코루틴 예제부터 시작함

    아래 코드는 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를 사용함

    asyncDeferred<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) 패턴으로 단계적으로 확장하는 것이 효과적임

    안드로이드 개발에서 코루틴은 이제 선택이 아닌 필수 기술이므로, 이번 예제들을 직접 실행해보며 익숙해지는 것을 권장함

Designed by Tistory.