Android 개발/멀티스레딩 & 비동기 프로그래밍

(5분 읽기) Kotlin 코루틴을 활용한 비동기 처리 완벽 정리🚀 (초보 개발자용)

안드하는잡스 2025. 3. 4. 23:05

안드로이드 앱을 개발하다 보면 네트워크 요청, 데이터베이스 조회, UI 이벤트 처리 등 비동기 작업을 자주 수행해야 해.
비동기 처리를 잘못하면 **콜백 지옥(Callback Hell)**에 빠지고, 앱이 느려지거나 튕기는 문제가 생길 수도 있어.

이때 **Kotlin 코루틴(Coroutines)**을 사용하면 비동기 작업을 쉽고, 가독성 높게 처리할 수 있어!
오늘은 초보 개발자도 쉽게 이해할 수 있도록 코루틴을 활용한 비동기 처리에 대해 정리해볼게. 💡


🎯 비동기 처리란? (동기 vs 비동기 이해하기)

비동기(Asynchronous)는 말 그대로 **"동시에 실행하지 않는 것"**이야.
비동기 처리를 이해하려면 동기(Synchronous) 처리와 비교하면 쉬워.

 

동기 처리(Synchronous)란?

동기 처리는 한 작업이 끝날 때까지 다음 작업이 대기하는 방식이야.

💡 예제: 동기 코드

fun main() {
    println("1. 데이터를 불러오는 중...")
    Thread.sleep(3000)  // 3초 동안 대기 (네트워크 요청 가정)
    println("2. 데이터 로드 완료!")
}

 

📌 결과

1. 데이터를 불러오는 중...
(3초 대기)
2. 데이터 로드 완료!

문제점: 첫 번째 작업이 끝날 때까지 다음 코드가 실행되지 않아 앱이 멈춘 것처럼 느껴질 수 있어. 😰


비동기 처리(Asynchronous)란?

비동기 처리는 한 작업이 끝나길 기다리지 않고, 다른 작업을 동시에 진행하는 방식이야.

💡 예제: 기본적인 비동기 처리 (콜백)

fun fetchData(callback: (String) -> Unit) {
    Thread {
        Thread.sleep(3000)  // 3초 후 데이터 반환
        callback("2. 데이터 로드 완료!")
    }.start()
}

fun main() {
    println("1. 데이터를 불러오는 중...")
    fetchData { result -> println(result) }
    println("3. 다른 작업 실행 중...")
}

 

📌 결과

1. 데이터를 불러오는 중...
3. 다른 작업 실행 중...  // fetchData()가 실행되는 동안 다른 코드가 먼저 실행됨
(3초 후)
2. 데이터 로드 완료!

 

  • 장점: 앱이 멈추지 않고, UI가 자연스럽게 동작해!
  • 단점: 콜백이 중첩될수록 가독성이 떨어지고 **콜백 지옥(Callback Hell)**이 발생할 수 있음.

🔥 코루틴을 활용한 비동기 처리 방법

1️⃣ suspend 함수로 비동기 코드 작성하기

suspend 키워드를 사용하면 함수 내에서 비동기 작업을 일시 중단했다가 재개할 수 있어.

💡 예제: suspend 함수를 사용한 비동기 처리

import kotlinx.coroutines.*

suspend fun fetchData(): String {
    delay(3000)  // 3초 후 데이터 반환
    return "2. 데이터 로드 완료!"
}

fun main() = runBlocking {
    println("1. 데이터를 불러오는 중...")
    val result = fetchData()  // 비동기 함수 실행 (delay 동안 다른 코드 실행 가능)
    println(result)
}

 

 

📌 결과

1. 데이터를 불러오는 중...
(3초 후)
2. 데이터 로드 완료!

✔ delay(3000)을 사용하면 Thread.sleep()처럼 스레드를 멈추지 않고 비동기 대기 가능!
✔ suspend 함수는 일반 함수에서 직접 호출할 수 없고, 코루틴 블록(runBlocking) 내에서 실행해야 함


2️⃣ launch로 여러 비동기 작업 동시에 실행하기

💡 예제: launch를 이용한 병렬 실행

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000)
        println("2. 비동기 작업 1 완료!")
    }

    launch {
        delay(2000)
        println("3. 비동기 작업 2 완료!")
    }

    println("1. 메인 스레드 실행 중...")
}

 

📌 결과

1. 메인 스레드 실행 중...
(1초 후)
2. 비동기 작업 1 완료!
(2초 후)
3. 비동기 작업 2 완료!

✔ launch를 사용하면 여러 개의 비동기 작업을 동시에 실행할 수 있어!


3️⃣ async와 await을 이용한 결과 반환

💡 예제: async를 활용한 비동기 데이터 처리

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred1 = async {
        delay(1000)
        "2. 데이터 1 로드 완료!"
    }

    val deferred2 = async {
        delay(2000)
        "3. 데이터 2 로드 완료!"
    }

    println("1. 데이터 로딩 시작...")
    println(deferred1.await())  // 1초 후 결과 출력
    println(deferred2.await())  // 2초 후 결과 출력
}

 

📌 결과

1. 데이터 로딩 시작...
(1초 후)
2. 데이터 1 로드 완료!
(2초 후)
3. 데이터 2 로드 완료!

✔ async는 launch와 다르게 결과를 반환할 수 있음!
✔ await()을 사용하면 비동기 작업이 완료될 때까지 기다렸다가 결과를 받아옴


🚀 코루틴 + Flow: 실시간 데이터 스트림 처리

코루틴은 단일 작업을 처리하는 데 강력하지만, 스트림 형태의 데이터(예: 실시간 센서, WebSocket, Room DB)를 처리하려면 Flow가 필요해!

💡 예제: Flow를 이용한 데이터 스트림 처리

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun fetchData(): Flow<String> = flow {
    for (i in 1..3) {
        delay(1000)
        emit("데이터 $i 로드 완료!")
    }
}

fun main() = runBlocking {
    fetchData().collect { println(it) }  // Flow 데이터 수집
}

 

📌 결과

(1초 후) 데이터 1 로드 완료!
(2초 후) 데이터 2 로드 완료!
(3초 후) 데이터 3 로드 완료!

✔ emit()을 사용해 데이터를 계속 전달할 수 있음
✔ collect()를 사용해 Flow 데이터를 실시간으로 수집 가능


🎯 결론: 코루틴을 사용하면 비동기 처리가 쉬워진다!

동기 코드처럼 읽히면서도 비동기 실행 가능
launch, async를 활용하면 여러 개의 비동기 작업을 쉽게 관리 가능
Flow를 이용하면 실시간 데이터 스트림 처리까지 가능

이제 코루틴을 활용해서 더 가독성 높고, 효율적인 비동기 코드를 작성해보자! 🚀