[Android] Coroutine

MariGold·어제

[Android]

목록 보기
11/12
post-thumbnail

Android 개발을 하다보면 "이 작업 메인 스레드에서 처리해도 되나?", "비동기 처리를 더 깔끔하게 할 수 없을까?"같은 고민이 생기기 마련입니다. 특히 네트워크 요청, 디스크 IO, 데이터 변환 같은 시간이 오래 걸리는 작업은 UI를 멈추게 만들 위험이 있기 때문에 비동기 처리가 필수적입니다.

과거에는 이런 문제를 해결하기 위해 콜백·RxJava 등을 사용했지만, Kotlin이 도입되면서 완전히 다른 방식이 등장했습니다. 바로 코루틴(Coroutines) 입니다.

이번 글에서는 코루틴이 무엇인지, 왜 필요한지, 기본 개념 및 예제, 그리고 Compose와 함께 사용할 때의 팁까지 정리해보겠습니다.


🚀 Coroutine(코루틴)이란?

코루틴은 Kotlin에서 제공하는 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 설계 패턴입니다. 쉽게 말하면 스레드를 직접 만들지 않고 비동기 작업을 작성할 수 있게 도와주는 일종의 경량 스레드입니다.

코루틴의 핵심 특징

  • 가볍다
    -> 수천 개의 코루틴도 스레드 몇 개만으로 처리 가능
    -> 성능 + 메모리 효율
  • 중단 (suspend)
    -> 실행을 잠시 멈추고 다시 이어서 실행 가능
    -> 비동기 작업을 동기 코드처럼 자연스럽게 작성
  • 구조화된 동시성
    -> 코루틴의 생명주기를 스코프로 관리하여 누수없는 안전한 비동기 처리
  • 캔슬 지원
    -> 필요 없어진 작업은 즉시 취소하여 네트웤, 요청, 무한 루프 등 불필요한 연산 절약

🔧 기본 사용법

val scope = CoroutineScope(Dispatchers.Main)

suspend fun fetchData(): String {
    delay(1000) // 네트워크 요청처럼 1초 지연된다고 가정
    return "완료!"
}

scope.launch {
    // 여기서부터 코루틴 실행
    val result = fetchData()
    println(result)
}

코루틴 안에서 시간이 오래 걸리는 작업은 suspend 함수로 분리합니다. suspend 함수는 중단이 가능한 함수로, 중단한다는 것은 스레드를 점유하지 않으면서 작업이 잠시 멈췄다가 이어서 실행될 수 있다는 의미입니다.


⚙️ 코루틴 디스패처

코루틴이 어떤 스레드에서 실행될지 결정하는 요소입니다.

  • Dispatcher.Main
    -> UI 작업(Compose 포함)
    -> 메인 스레드에서 실행
  • Dispatcher.IO
    -> 네트워크·파일 IO 작업
    -> 네트워크 요청, 데이터베이스 쿼리, 파일 읽기/쓰기
  • Dispatcher.Default
    -> CPU 집중 계산 작업
    -> JSON 파싱, 정렬, 필터링 등
  • Dispatcher.Unconfined
    -> 특수한 케이스 (일반적으로 사용 비추천)

🚀 withContext란?

코루틴 내에서 작업의 성격에 따라 디스패처를 전환해야할 때가 있습니다. 이 때, withContext를 사용합니다.

suspend fun loadUserData(): User {
    // Main 디스패처에서 시작
    val userId = getCurrentUserId()
    
    // IO 작업을 위해 디스패처 전환
    val user = withContext(Dispatchers.IO) {
        database.getUserById(userId) // DB 조회
    }
    
    // 다시 Main으로 돌아옴
    return user
}

withContext는 지정한 디스패처로 전환 후, 블록 내 작업이 끝나면 다시 원래 디스패처로 돌아옵니다. 단, 아래의 코드 예시와 같이 디스패처 전환을 자주하면 안됩니다.

// ❌ 나쁜 예: 불필요한 디스패처 전환 반복
suspend fun processItems(items: List<Item>) {
    items.forEach { item ->
        withContext(Dispatchers.IO) { // 매번 전환!
            saveToDatabase(item)
        }
    }
}

// ✅ 좋은 예: 한 번만 전환
suspend fun processItems(items: List<Item>) {
    withContext(Dispatchers.IO) { // 한 번만 전환
        items.forEach { item ->
            saveToDatabase(item)
        }
    }
}

디스패처 전환을 자주 하면 안 되는 이유

  • 컨텍스트 전환 비용
    -> 디스패처를 바꿀 때마다 스레드 전환 비용이 발생합니다. 수백 ~ 수천 번 반복되면 성능 저하가 발생합니다.
  • 불필요한 오버헤드
    -> withContext는 새로운 코루틴을 만들고, 작업을 스케줄링하고, 다시 돌아오는 과정을 거칩니다.

따라서 작업의 단위가 클 때 디스패치를 전환하고, 반복문 안에서는 가급적 전환하지 않습니다.


📌 구조화된 동시성

코루틴이 다른 비동기 라이브러리와 다른 핵심 개념입니다.

coroutineScope {
    launch { ... }
    launch { ... }
}

부모 스코프가 살아 있는 동안, 자식 코루틴이 모두 끝날 때까지 함께 관리됩니다.
→ 예측 가능한 생명주기 관리 가능

Android에서는 주로 다음 스코프를 사용합니다

  • viewModelScope (ViewModel)
  • lifecycleScope (Activity/Fragment)
  • rememberCoroutineScope (Compose)
@Composable
fun MyScreen() {
    val scope = rememberCoroutineScope()

    Button(onClick = {
        scope.launch {
            delay(1000)
            println("완료!")
        }
    }) {
        Text("실행")
    }
}

⚡ 코루틴을 쓰면 좋은 상황

  • 네트워크 요청
  • 데이터베이스 쿼리
  • 파일 IO
  • 스크롤 이벤트 처리
  • 무거운 계산 작업
  • 반복적으로 갱신되는 데이터 처리

❗ 코루틴 사용 시 주의점

  • GlobalScope 사용 금지 (누수 위험)
  • 무한 루프 반복 시 반드시 isActive 체크
  • 디스패처를 잘못 쓰면 UI가 멈출 수 있음
  • suspend 함수를 남용하면 설계가 복잡해질 수 있음
  • 반복문 안에서 불필요하게 withContext 호출하지 말 것
  • Dispatchers.Unconfined는 특별한 경우가 아니면 사용하지 말 것

🎯 마무리

코루틴은 Kotlin에서 비동기 작업을 가장 깔끔하고 안정적으로 처리할 수 있는 핵심 도구입니다.
이전의 콜백 지옥, 복잡한 Rx 체이닝에 비해 훨씬 읽기 쉽고, 스레드 관리도 직접 하지 않아도 됩니다.

Compose와 함께 사용할 때도 최적의 방식이므로, 앱이 점점 비동기 연산을 많이 사용할수록 코루틴은 필수가 됩니다.


💡 참고자료

Android 공식 문서 - 코루틴

profile
많은 것을 알아가고 싶은 Android 주니어 개발자

0개의 댓글