본문 바로가기
안드로이드 코틀린

[Kotlin][Android] 코루틴(Coroutine) 사용하기

by teamnova 2025. 6. 26.
728x90

안녕하세요.

이번 글에서는 코루틴(Coroutine)의 개념을 정확하게 이해하고, 실제 안드로이드 앱에서 코루틴을 활용하는 방법을 예제와 함께 살펴보겠습니다.

 

1. 코루틴(Coroutine)

1-1. 코루틴이란?

코루틴(Coroutine)은 함수의 실행을 일시 중단(suspend)하거나, 중단된 지점에서 다시 실행(resume)할 수 있는 비동기 프로그래밍 기법입니다.
코틀린(Kotlin)에서는 코루틴을 통해 비동기 작업(예: 네트워크 요청, 파일 입출력 등)을 간결하고 효율적으로 처리할 수 있습니다.
코루틴을 활용하면 복잡한 콜백 구조 없이, 동기 코드와 유사한 방식으로 비동기 로직을 작성할 수 있다는 장점이 있습니다.

1-2. 비동기란?

비동기(Asynchronous)란, 어떤 작업이 실행되는 동안 해당 작업의 완료를 기다리지 않고, 다른 작업을 동시에 진행할 수 있는 프로그래밍 방식입니다.
예를 들어, 네트워크에서 데이터를 받아오는 작업이 오래 걸릴 때, 메인 스레드는 사용자 인터페이스(UI)를 계속해서 응답할 수 있어 앱이 멈추거나 느려지지 않습니다.
비동기 처리는 주로 네트워크 통신, 데이터베이스 접근, 파일 입출력 등 시간이 오래 걸릴 수 있는 작업에서 필수적으로 사용됩니다.

1-3. 코루틴의 작동원리와 비동기 작업의 관계

코루틴은 전통적인 스레드 기반의 비동기 처리와는 다른, 경량화된 협력적(concurrent) 실행 단위입니다.

코루틴의 작동원리를 이해하려면 다음과 같은 주요 특징을 알아야 합니다.

(1) 일시 중단(Suspension)과 재개(Resumption)

코루틴은 실행 도중에 suspend 지점(예: delay(), 네트워크 요청 등)에서 스스로 일시 중단할 수 있습니다.
중단 시점에서 현재까지의 상태(로컬 변수, 실행 위치 등)를 저장하고, 필요한 시점에 그 상태 그대로 다시 이어서 실행할 수 있습니다.
이 과정은 개발자가 명시적으로 스레드를 생성하거나 관리하지 않아도, 코틀린 런타임이 내부적으로 상태를 관리해주기 때문에 매우 효율적입니다.

(2) 협력적 멀티태스킹(Cooperative Multitasking)

코루틴은 자발적으로(suspend 지점에서)만 실행을 멈춥니다.
이는 기존의 선점형(preemptive) 스레드와 달리, OS가 임의로 실행을 중단시키지 않습니다.
따라서 코루틴 내부에서 CPU를 오랜 시간 점유하는 연산을 계속하면, 다른 코루틴의 실행이 지연될 수 있습니다.
비동기 작업(네트워크, 파일 I/O 등) 또는 delay() 같은 suspend 함수를 적절히 사용해야 합니다.

(3) 컨티뉴에이션(Continuation)

코루틴의 상태 저장 및 재개는 컨티뉴에이션(Continuation)이라는 개념으로 구현됩니다.
코루틴이 중단되면, 해당 시점의 실행 컨텍스트(변수, 호출 스택 등)를 컨티뉴에이션 객체에 저장합니다.
이후 비동기 작업이 완료되면, 컨티뉴에이션을 통해 중단된 지점부터 다시 실행이 이어집니다.

(4) 스레드와의 관계

코루틴은 실제로는 스레드 위에서 동작하지만, 여러 개의 코루틴이 하나의 스레드에서 번갈아가며 실행될 수 있습니다.
코루틴은 매우 가볍기 때문에, 수천~수만 개의 코루틴을 동시에 실행해도 스레드보다 훨씬 적은 리소스를 사용합니다.
Dispatcher를 통해 코루틴이 어느 스레드에서 실행될지 정할 수 있습니다.

(5) 비동기 작업과의 연결

코루틴은 비동기 작업(예: 네트워크 요청)이 완료될 때까지 일시 중단(suspend) 상태로 대기하다가, 결과가 준비되면 자동으로 재개(resume)됩니다.
이 과정은 콜백 기반의 비동기 처리와 달리, 코드가 순차적으로 작성되어 가독성과 유지보수성이 크게 향상됩니다.

(6) 예시로 보는 작동 흐름

아래는 코루틴의 작동 흐름을 간단히 나타낸 예시입니다.

scope.launch {
    val data = fetchData() // suspend 함수, 네트워크 요청
    process(data)
}

suspend fun fetchData(): String {
    // 네트워크 요청을 보냄
    // 이 시점에 코루틴은 중단되고, 네트워크 응답을 기다림
    // 응답이 도착하면, 코루틴은 자동으로 이 지점부터 다시 실행됨
    return response
}
  • 위 예제에서 fetchData()가 네트워크 요청을 보내고 응답을 기다리는 동안,
    코루틴은 일시 중단(suspend)되어 리소스를 거의 사용하지 않습니다.
  • 응답이 도착하면, 코루틴은 중단된 지점에서 자동으로 재개(resume)되어 이후 코드를 실행합니다.

2. 코루틴을 쓸 때 알아야할 기본 개념

2-1. Dispatcher(디스패처)

Dispatcher는 코루틴이 어떤 스레드에서 실행될지 결정하는 역할을 합니다.
대표적으로 아래와 같은 Dispatcher가 있습니다.

  • Dispatchers.Main: 메인(UI) 스레드에서 실행. UI 업데이트 등 메인 스레드에서만 가능한 작업에 사용.
  • Dispatchers.IO: 파일 입출력, 네트워크 통신 등 I/O 작업에 최적화된 스레드에서 실행.
  • Dispatchers.Default: CPU 연산이 많은 작업(예: 대규모 데이터 처리, 계산)에 적합한 스레드에서 실행.
  • Dispatchers.Unconfined: 특정 스레드에 종속되지 않고, 호출한 컨텍스트에 따라 유동적으로 실행.

2-2. Scope(스코프)

Scope는 코루틴의 생명주기를 관리하는 역할을 합니다.
코루틴이 언제 시작되고, 언제 종료될지 관리하며,
대표적으로 아래와 같은 Scope가 있습니다.

  • CoroutineScope: 직접 생성해서 사용할 수 있는 범용 코루틴 스코프.
  • lifecycleScope: 안드로이드의 Activity나 Fragment의 생명주기에 맞춰 코루틴을 관리.
  • viewModelScope: ViewModel의 생명주기에 맞춰 코루틴을 관리.

Scope를 올바르게 사용하면, 화면이 종료되거나 ViewModel이 소멸될 때 불필요한 코루틴을 자동으로 취소할 수 있어 메모리 누수나 예기치 않은 동작을 방지할 수 있습니다.

2-3. Job(잡)

Job은 코루틴의 실행 단위를 의미하는 객체로, 코루틴의 상태(진행 중, 완료, 취소 등)를 추적하고 제어할 수 있습니다.
Job을 통해 코루틴을 명시적으로 취소하거나, 여러 코루틴의 완료를 함께 기다릴 수 있습니다.

2-4. launch와 async

  • launch: 반환값이 필요 없는 코루틴을 생성할 때 사용합니다. launch로 생성된 코루틴은 Job 객체를 반환합니다.
  • async: 반환값이 필요한 비동기 작업에 사용합니다. async는 Deferred 객체를 반환하며, await()를 통해 결과값을 얻을 수 있습니다.

2-5. suspend 함수

suspend 함수는 코루틴 내에서 일시 중단(suspend)될 수 있는 함수를 의미합니다.
이 함수는 반드시 코루틴 또는 다른 suspend 함수 내에서만 호출할 수 있습니다.
대표적으로 delay(), 네트워크 요청 등 비동기 작업이 suspend 함수로 구현됩니다.

 

3. 코루틴 기본 사용법

3-1. 코루틴 라이브러리 추가

코루틴을 사용하려면 build.gradle(:app) 파일에 아래 의존성을 추가해야 합니다.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'

3-2. 코루틴 실행 기본 패턴

코루틴은 Scope와 Dispatcher를 지정하여 아래와 같이 실행할 수 있습니다.

// 1. CoroutineScope 생성 (메인 스레드에서 실행)
val scope = CoroutineScope(Dispatchers.Main)

// 2. launch를 통해 코루틴 실행
scope.launch {
    // 이 영역에서 비동기 작업 수행
}

3-3. delay와 suspend 함수 예시

delay는 지정한 시간(ms)만큼 일시 중단하는 suspend 함수입니다.
예를 들어, 1초 대기 후 결과를 반환하는 suspend 함수는 아래와 같이 작성할 수 있습니다.

scope.launch {
    val result = mySuspendFunction()
    // result 사용
}

// suspend 함수 정의
suspend fun mySuspendFunction(): String {
    delay(1000) // 1초 대기
    return "코루틴 완료"
}

 

4. 실전 예제: 버튼 클릭 시 2초 기다렸다가 메시지 띄우기

4-1. activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="시작" />

    <TextView
        android:id="@+id/tvResult"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="결과가 여기에 표시됩니다"
        android:layout_marginTop="24dp"/>
</LinearLayout>

4-2. MainActivity.kt

package com.example.coroutineexample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {

    // 코루틴의 생명주기를 관리할 Job 객체 생성
    private val job = Job()
    // 메인 스레드에서 코루틴을 실행할 Scope 생성
    private val coroutineScope = CoroutineScope(Dispatchers.Main + job)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val btnStart = findViewById<Button>(R.id.btnStart)
        val tvResult = findViewById<TextView>(R.id.tvResult)

        btnStart.setOnClickListener {
            // launch를 통해 코루틴 실행
            coroutineScope.launch {
                tvResult.text = "2초 대기 중..."

                // delay는 코루틴 내에서만 사용 가능하며, 지정 시간(ms) 동안 일시 중단
                delay(2000)

                // 2초 후 결과 표시
                tvResult.text = "완료!"
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 액티비티가 종료될 때 코루틴도 함께 취소하여 메모리 누수 방지
        job.cancel()
    }
}