본문 바로가기

코틀린

안드로이드 코루틴 기본 개념과 활용까지의 모든 것!

코루틴

코루틴(Coroutines) 은 쓰레드(Thread)와 기능적으로는 비슷하지만, 하나의 쓰레드 내에서 여러 개의 코루틴이 실행되는 개념으로 비동기 프로그래밍에 권장되는 동시 실행 설계 패턴입니다.

코루틴은 단일 쓰레드 내에서 여러 개의 코루틴을 실행할 수 있기 때문에, 많은 양의 동시 작업을 처리할 수 있으면서 메모리 절약의 장점이 있습니다.

이유는, 기존 쓰레드는 Context-Switching(CPU가 쓰레드를 점유하면서 실행, 종료를 반복하며 메모리 소모)이 발생하기 때문에 많은 양의 쓰레드를 갖기가 어렵지만

반면에 코루틴은 쓰레드가 아닌 루틴을 일시 중단(suspend) 하는 방식이라 Context-Switching에 비용이 들지 않기 때문입니다.

또한, 지정된 작업 범위 내에서 실행이 되기 때문에 메모리 누수를 방지할 수 있습니다.

 

즉, 코루틴은 쓰레드의 간소화된 버전이라고 할 수 있습니다.

 

코루틴 이미지 도식화

주요 개념

코루틴에 대해 이해하기 위해서 기본적으로 알아야 하는 개념은 아래와 같습니다.

 

- CoroutineScope

- CoroutineContext (Job, Dispatchers)

- CoroutineBuilder (launch, async)

- susfend function

CoroutineScope

코루틴이 실행되는 범위로, 코루틴을 실행하고 싶은 Lifecycle에 따라 원하는 Scope를 생성하여 코루틴이 실행될 작업 범위를 지정할 수 있습니다.

 

▶ 사용자 지정 CoroutineScope : CoroutineScope(CoroutineContext)

    ex) CoroutineScope(Dispatchers.Main) // Dispatchers.Main, Dispatchers.Default, Dispatchers.IO, Job()...

// 메인 쓰레드에서 실행될 사용자 정의 Scope
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
    // 메인 쓰레드 작업
}

// 백그라운드에서 실행될 사용자 정의 Scope
CoroutineScope(Dispatchers.IO).launch {
    // 백그라운드 작업
}

 

GlobalScope : 앱이 실행될 때부터 종료될 때까지 실행

// 앱의 라이프사이클동안 실행될 Scope
GlobalScope.launch {
    // 백그라운드로 전환하여 작업
    launch(Dispatchers.IO) {

    }

    // 메인쓰레드로 전환하여 작업
    launch(Dispatchers.Main) {

    }
}


안드로이드 Jetpack 라이브러리(AAC)에서는 코루틴을 쉽게 사용할 수 있도록 각 Lifecycle에 맞는 Scope를 제공해주고 있습니다.

ViewModelScope : ViewModel 대상, ViewModel이 제거되면 코루틴 작업이 자동으로 취소됩니다.

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // ViewModel이 제거되면 코루틴도 자동으로 취소됩니다.
        }
    }
}


LifecycleScope : Lifecycle 객체 대상(Activity, Fragment, Service...), Lifecycle이 끝날 때 코루틴 작업이 자동으로 취소됩니다.

class MyActivity : AppCompatActivity() {
    init {
        lifecycleScope.launch {
            // Lifecycle이 끝날 때 코루틴 작업이 자동으로 취소됩니다.
        }
   }
}


liveData : LiveData 대상, LiveData가 활성화되면 실행을 시작하고 비활성화되면 자동으로 취소됩니다.

val user: LiveData<User> = liveData {
    val data = repository.loadUser() // suspend function
    emit(data)
}

 

CoroutineContext

코루틴 작업을 어떤 쓰레드에서 실행할 것인지에 대한 동작을 정의하고 제어하는 요소입니다.

주요 요소로는 JobDispatchers가 있습니다.

 

Job : 코루틴을 고유하게 식별하고, 코루틴을 제어합니다. 

val job = CoroutineScope(Dispatchers.IO).launch {
    // 비동기 작업
}

job.join()      // 작업이 완료되기까지 대기
job.cancel()    // 작업 취소

 

val job1 = Job()
CoroutineScope(job1 + Dispatchers.Main).launch {
    // 메인 쓰레드 작업

    launch(Dispatchers.IO) {
        // 비동기 작업
    }

    withContext(Dispatchers.Default) {
        // 비동기 작업
    }
}

val job2 = CoroutineScope(Dispatchers.IO).launch {
    // 비동기 작업
}

job1.cancel() // job1이 연결된 코루틴 작업 취소

 

 

Dispatchers : 코루틴을 어떤 쓰레드에서 실행할 것인지에 대한 동작을 지정합니다.

Dispatchers.Main : 안드로이드의 메인 쓰레드로, UI 작업을 위해 사용해야 합니다.
예를 들어, UI를 구성하거나 LiveData를 업데이트 할 때 사용됩니다.

Dispatchers.IO : 네트워크, 디스크 I/O 실행에 최적화되어 있습니다.
예를 들어, Retrofit으로 네트워크 통신을 하거나, File이나 Room 데이터베이스에서 데이터를 읽고/쓸 때 사용됩니다. 

Dispatchers.Default : CPU 사용량이 많은 무거운 작업 처리에 최적화 되어 있습니다.
예를 들어, 데이터를 가공하거나 복잡한 연산, JSON 파싱을 할 때 주로 사용됩니다. 

CoroutineBuilder

위에서 설정한 CoroutineScope와 CoroutineContext를 통해 비로소! 드디어! 코루틴을 실행시켜주는 함수입니다.

주요 요소로는 launchasync가 있습니다.

 

launch : Job 객체이며, 결과값을 반환하지 않습니다.

실행 후 결과값이 필요 없는 모든 작업은 launch를 사용하여 실행할 수 있습니다.

CoroutineScope(Dispatchers.Main).launch {
    // 결과값이 필요없는 작업
}

 

async : Deferred 객체이며, 결과값을 반환합니다.

await() 함수를 사용하여, 코루틴 작업의 최종 결과값을 반환합니다.

val deferred = CoroutineScope(Dispatchers.Main).async {
    // 결과값
    "Hello Coroutine!"
}

val message = deferred.await() // await()함수로 결과값 반환
println(message)

 

withContext : async와 동일하게 결과값을 반환하며, async와의 차이점은 await()을 호출할 필요가 없다는 것입니다.

async{ }.await() 과 동일하다고 보면 됩니다.

코루틴 내부나 susfend 함수 안에서 구현이 가능하며, 콜백이 필요 없이 코드의 쓰레드 풀을 제어할 수 있기 때문에 네트워크 요청이나 DB 조회 같은 작업에 주로 사용합니다.

init {
    viewModelScope.launch {       // Dispatchers.Main
        val user = getUserInfo()  // Dispatchers.Main
    }
}

suspend fun getUserInfo(): User =                   // Dispatchers.Main
        withContext(Dispatchers.IO) {               // Dispatchers.IO
            val response = apiService.getUserInfo() // Dispatchers.IO
            if (response.isSuccessful) {            // Dispatchers.IO
                return@withContext response.body()  // Dispatchers.IO
            } else {                                // Dispatchers.IO
                return@withContext null             // Dispatchers.IO
            }                                       // Dispatchers.IO
        }                                           // Dispatchers.Main

susfend function

코루틴 안에서만 실행할 수 있는 코루틴 전용 메소드입니다.

susfend 메소드는 일반적인 곳에서 호출할 수 없으며, 반드시 코루틴 안에서만 호출이 가능합니다.

이유는 코루틴의 실행이 일시중단(susfend) 되거나 다시 재개(resume) 될 수 있기 때문에, 컴파일러에게 이 메소드는 코루틴 안에서 실행할 메소드임을 정의하기 위해 메소드명 앞에 "suspend" 를 붙여줘야 합니다.

 

suspend fun getUser(): User {
    ...
    return user
}

 

코루틴을 사용하실 땐 세 가지 순서만 기억하세요!

1. 어떤 쓰레드에서 실행할 것 인지 Dispatchers 를 정하고 (Dispatchers.Main, Dispatchers.IO, Dispatchers.Default)

2. 코루틴이 실행될 Scope를 정하고 (CoroutineScope, ViewModelScope, LifecycleScope, liveData...)

3. launch 또는 async로 코루틴을 실행 시키면 됩니다!

ContextScope를 만들고, Builder를 이용하여 코루틴을 실행!