코딩하는 개굴이

[Kotlin/안드로이드] Coroutine에 대하여 (feat. 동시성 프로그래밍) 본문

안드로이드/Coroutine

[Kotlin/안드로이드] Coroutine에 대하여 (feat. 동시성 프로그래밍)

개굴이모자 2022. 9. 23. 23:44
반응형
시작하기 전에, 해당 글에서는 본인이 헷갈리는 부분이 다소 많았기 때문에 부가 설명을 많이 추가했음을 먼저 알리고싶다.

코루틴은 무엇일까?

코루틴은 시작된 스레드를 중단하지 않으면서 비동기적으로 실행되는 코드로, AsyncTask처럼 스레드의 직접 관리가 필요하지 않고, 기존의 다중 스레드보다 효율적으로 동작하는 장점이 있다.

# 코루틴은 Thread를 중단하지 않는다? 스레드를 중단하지 않는다는 것이 어떤 의미일까?

우선, 코루틴1을 실행 중인 상태에서 코루틴2가 실행 될 때를 가정해보자.

이때, 실행 중인 Thread를 중지시키면서 Context Switching을 수행하는 것이 아닌 잠시 코루틴1을 멈춰두었다가 다시 실행할 때 이전 상태를 불러와 다시 스레드에서 이를 수행한다.

따라서, 코루틴은 Thread의 중단 없이 루틴을 돌리며 적은 자원을 소모하는 장점이 존재한다.

 

코루틴은 서로가 서로를 호출하는 관계로, 코틀린에 도입된 동시성 프로그래밍의 개념이기도하다.

 

# Kotlin 의
동시성 프로그래밍
애플리케이션이 두개 이상의 Thread에서 실행될 때, Thread 간의 통신 및 동기화를 이용해
입력에 대해 항상 같은 결과를 내놓지만, 실행 순서는 유연성이 있는 프로그래밍을 의미한다.

[동시성 프로그래밍이 어려운 이유]
1. 레이스 컨디션
2. 원자성 위반 (작업이 사용하는 Data의 접근에 간섭이 없음을 보장해야한다.)
3. 교착 상태와 라이브 락 (Data의 접근을 막거나 기다리는 처리 시, 의존이 발생할 수 있다.)
(라이브락이란 교착 상태로 인해 정상 상태로 돌아오지 못하는 것을 말한다.)

위같은 현상이 발생하지 않게하면서 동시성 프로그래밍을 하는 것은 어려운 일이다.

그러나, Kotlin 에서는 이러한 동시성 프로그래밍을 위해 아래와 같은 특징을 제공하고 있기에 동시성 프로그래밍을 수월하게 구현할 수 있다.

[코틀린의 동시성 프로그래밍]
1. Non-Blocking (Suspend computation을 이용해 Thread의 실행은 블록킹하지 않으면서 실행을 잠시 중단하고 Thread는 다른 연산에 사용 할 수 있다.)
2. 동시성을 쉽게 구현 가능한 함수/기본형을 제공

 


 

Coroutine Scope

모든 코루틴은 코루틴 스코프라는 것에서 시작되어야한다. 그 이유는 스코프가 Activity, Fragment 등의 생명 주기에 따라서 Destroy 시 코루틴이 한번에 취소되어 메모리 누수를 방지할 수 있기 때문이다.

그렇기 때문에 코루틴 스코프는 코루틴이 작동하는 범위를 설정하는 역할을 수행한다고 볼 수도 있다.

 

Coroutine Scope는 코루틴이 작동하는 범위를 설정하고 블록을 제어하는 단위를 정의하는 인터페이스의 형태로 Global Scope, Main Scope 등이 존재하며 이들은 모두 Coroutine Scope 인터페이스를 상속 받고 있다.

 

GlobalScope

Singleton이기에 프로그램의 어디서나 제어/동작이 가능하다. Application 자체의 Lifecycle 과 함께 동작하기 때문에 별도의 Lifecycle 관리가 필요하지 않다. 따라서, Application의 시작부터 종료까지 실행되는 Top-Level의 코루틴 즉, 전체 Lifecycle 내에서 Active 상태여야 할 때 사용해야한다.

 

GlobalScope에서 시작된 코루틴은 구조적인 동시성을 따르지 않기 때문에, 네트워크의 지연 등으로 코루틴이 중단/Response 대기의 경우에도 계속 기다리며 Resource를 소모한다.

MainScope

UI 컴포넌트를 위한 Scope로, 해당 Scope 에서 만들어진 모든 코루틴을 MainThread에서 실행하고, Dispatcher는 Main을 갖는다.

ViewModelScope

ViewModel을 대상으로 하는 CoroutineScope로, VM이 제거되면 코루틴 작업도 자동으로 취소된다.

LifecycleScope

Lifecycle 객체 대상인 Activity, Fragment, Service 등의 Lifecycle을 따르는 Scope로, 해당 Lifecycle이 끝날 때, 자동으로 작업이 취소되며 의존성 추가가 필요하다. (라이브러리 Implementation 필요)

Lifecycle 의 상태를 받아 그에 따른 처리도 가능하다.

 


Coroutine Dispatcher

코루틴을 Dispatcher에 전송하면 Dispatcher는 자신이 관리하는 Thread Pool 내의 스레드 부하 상황에 맞춰 코루틴을 배분한다.  즉, 시작하거나 재개할 스레드를 결정하는 역할로, Coroutine Dispatcher 인터페이스를 상속받고 있다.
안드로이드에는 Dispatcher가 생성되어있어 별도로 생성하거나 정의할 필요가 없다. 안드로이드에 기본으로 생성되어있는 Dispatcher는 아래와 같다.

 

Dispatchers.Main

Android 메인 스레드에서 코루틴을 실행하는 Dispatcher로, UI와 상호 작용하는 작업을 실행하기 위해서만 사용해야한다.

Dispatchers.IO

DB 혹은 Network 작업을 실행하는데 최적화 되어있는 Dispatcher

Dispatchers.Default

CPU를 많이 사용하는 작업을 기본 스레드 외부에서 실행하도록 최적화 되어있는 Dispatcher로, 정렬 작업이나 JSON 파싱 작업 등에 최적화 되어있다.

MainScope(Dispatchers.Main).launch {
	//필요한 작업 수행
}

 


 

Coroutine Builder

Coroutine을 실행할 때 사용하는 여러 함수들을 뜻한다. launch(), async(), runBlocking() 등이 있다.

  • launch(): 결과가 없는 코루틴을 생성하는 빌더로, Job 인스턴스를 반환하며 생성된 코루틴을 제어하는데 사용한다. 
    (변환된 Job으로 코루틴을 제어하는데 cancel(), join() 등을 사용한다.)
  • async(): launch와 다르게 결과를 가지는 코루틴 빌더로, 반환하는 Deferred 인스턴스로 코루틴을 제어하고 결과를 받을 수 있다. (결과를 받는 것은 Deferred의 await() 함수를 통해 원하는 시점에 코루틴 영역의 결과를 받을 수 있다. -> 지연 객체)
  • withContext(): async 처럼 결과값을 반환하는 빌더로 async는 Deferred 객체로 결과값을 원하는 시점에 await 함수를 통해 결과값을 얻지만 withContext는 결과 T를 그 자리에서 반납한다.
  • runBlocking(): 코루틴의 실행이 끝날때까지 현재 스레드를 차단한다. Main Thread에서 호출 시, ANR이 발생할 수 있고 코루틴의 장점인 병렬/비동기의 장점 활용이 불가하므로 일반 비즈니스 로직에 주로 사용되지 않는다.

 

참고 링크

반응형
Comments