[Android] Coroutine

Minji Jeong·2022년 5월 5일
0

Android

목록 보기
12/39
post-thumbnail
저번 포스팅에서 Coroutine과 RxJava에 대해 포스팅하기 전에 근본이 되는 비동기 프로그래밍(Asynchronous Programming)에 대해 소개했었다. 이번엔 코틀린으로 비동기 프로그래밍을 하면 빼먹을 수 없는, 안드로이드 개발자라면 필수적으로 써봐야 할 라이브러리인 Coroutine에 대해 작성해보고자 한다.

What is Coroutine in Android?

안드로이드 앱이 실행되면 메인화면(Main Thread = UI Thread)이 실행되고, 기본적으로 요청을 동기적으로 하나씩 처리한다. Main Thread에선 보통 UI 작업들을 하는데, UI작업 뿐만 아니라 모든 작업들을 동기적으로 처리할 경우 Main Thread가 하는 일이 너무 많아져 앱의 퍼포먼스와 속도가 저하될 수 있다.

하지만 비동기 프로그래밍을 사용하면 많은 작업들을 병렬적으로 처리할 수 있고, 곧 Main Thread의 무거운(ex Network, Database) 작업들을 백그라운드에서 따로 처리할 수 있게 된다. 이렇게되면 Main Thread에서는 UI 처리만 담당하면 되기 때문에, 앱의 퍼포먼스와 속도를 향상시킬 수 있다.

안드로이드에서 비동기 프로그래밍을 구현할 때 사용하는 라이브러리로는 대표적으로 AsyncTask, RxJava, RxKotlin, Coroutine이 있다. 이 중 AsyncTask 는 메모리 누수 문제와 버전별 일관적이지 않은 동작으로 인해 예전에 deprecated 되었고, 대체 라이브러리로 RxJava나 coroutine이 많이 사용되고 있다. 물론 다 훌륭한 라이브러리지만, RxJava 및 RxKotlin에 비해 API가 더 간단하고, 코드를 더 간략하게 짤 수 있는 Coroutine을 사용해보기로 했다. (상대적으로 말해서 그렇지, 물론 coroutine도 쉽지 않다!).

Coroutine

코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 안드로이드에서 사용할 수 있는 동시 실행 설계 패턴이다. 코루틴은 하나의 작업과 같고, 단일스레드를 사용하기 때문에 가볍다(여러개의 코루틴들이 단일스레드 위에서 동작하기 때문에 같은 수의 스레드를 실행하는 것보다 훨씬 더 가벼움). 따라서 다수의 스레드 관리를 직접 해주지 않아도 되고, 스레드가 중지되지 않고 계속 실행될 수 있기 때문에 성능 향상을 기대할 수 있고 여러 스레드를 사용하는 것보다 리소스를 훨씬 덜 소모한다. 코루틴이 가진 장점들을 더 구체적으로 설명하면 다음과 같다.

  1. 가벼움

    일시 중단(suspension)이 가능하기 때문에 단일 스레드는 기존의 상태를 유지하며 여러개의 코루틴을 실행할 수 있다.
    이게 어떻게 코루틴이 가벼운 걸 설명할 수 있는지 예시를 들자면, 스레드는 OS에 의한 컨텍스트 스위칭(Context Switching)을 통해 동시성을 보장한다. 스레드A, 스레드B가 있다고 가정해보자. 스레드A에서 작업1을 진행중이었는데, 도중에 작업2를 실행해야해서 작업1을 중단시켰다. 이후 작업2는 스레드B에서 수행되고, 이때 CPU가 메모리 영역을 스레드A에서 스레드B로 전환하는 컨텍스트 스위칭이 일어난다.
    그렇다면 코루틴은 어떨까? A,B 2개의 코루틴이 하나의 스레드1 위에서 실행된다고 가정해보자. A코루틴을 실행하던 중 B코루틴을 실행한다면 스레드1은 A코루틴의 상태를 저장 및 일시중지하고 B코루틴을 실행한다. 이후 A코루틴을 다시 실행할 때 저장해둔 A코루틴의 상태를 불러와 실행해주기만 하면 된다. 따라서 단일 스레드 내에서 여러개의 코루틴이 실행될 때는 작업에 전환에 있어 코루틴 객체들만 교체하면 되기 때문에 OS 수준의 컨텍스트 스위칭이 필요 없다.
  2. 메모리 누수 방지

    범위(Scope) 내에서 작업을 실행하기 위해 구조화된 동시성(structured concurrency)을 사용하고, 이로 인해 메모리 누수를 방지할 수 있다.
    🙄 구조화된 동시성 ?
    코루틴은 구조화된 동시성을 보장한다. Top Level 코루틴을 만들지 말고, 부모 코루틴의 자식 코루틴들을 여러개 만들어 관리하는 것이다. 예를 들어 부모 코루틴이 어떤 이유로든 취소되면 모든 자식 코루틴들이 취소된다. 자식코루틴에서 exception이 던져져서 취소되면, exception은 부모 코루틴으로 전파되어서 부모 코루틴을 취소시킨다. 자식 코루틴이 명시적인 취소로 인해 취소되면 부모 코루틴으로 취소가 전파되지 않는다.
  3. Jetpack과의 통합

    많은 Jetpack 라이브러리들이 코루틴을 지원하고 있고, 일부 라이브러리는 구조화된 동시성에 사용할 수 있는 코루틴 scope도 제공하고 있다.

본격적으로 코루틴을 사용해보기 전, 코루틴의 구성요소에는 어떤것들이 있는지 알아보자. 먼저 코루틴은 구조적 동시성을 위해 반드시 CoroutineScope내에서 실행되어야 한다. CoroutineScope는 말 그대로 코루틴이 실행되는 '범위'다. CoroutineScope는 스코프 내에서 실행된 모든 코루틴들을 추적할 수 있고, 취소할 수도 있다. 만약 스코프가 취소되면, 내부에서 실행중이던 모든 코루틴들도 취소되는 것이다. 하나의 코루틴 스코프 내의 코루틴들은 '순차적으로' 실행된다. CoroutineScope는 용도에 따라 여러개의 종류가 있다.

  1. CoroutineScope
    필요할 때 선언하고 종료할 수 있다.
  2. GlobalScope
    앱이 종료될 때까지 사용 가능하다. 싱글톤이기 때문에 별도의 생명주기 관리가 필요없지만, 네트워크 문제로 코루틴이 중단되거나 지연되는 경우에도 계속 실행되면서 리소스를 낭비할 수 있기 때문에 꼭 필요할 때만 사용해야한다.
  3. ViewModelScope
    뷰모델에서 사용하는 코루틴 스코프다. 만약 코루틴이 뷰모델과 연계되어 있다면, 뷰모델의 생명주기에 맞추어 쉽게 코루틴을 관리할 수 있다.
  4. LifeCycleScope
    Lifecycle 객체 대상인 Activity, Fragment, Service 등의 생명주기에 맞추어 사용할 수 있는 코루틴 스코프다.

코루틴 스코프를 생성할 때는 파라미터로 CoroutineContext를 전달해야 한다. CoroutineContext는 코루틴이 실행될 컨텍스트로, 우리는 CoroutineContext를 통해 코루틴의 목적에 맞게 실행될 특정 스레드풀을 지정해줘야 한다.

  1. Dispatchers.Main
    메인스레드로, 안드로이드 UI 작업이나 suspend 함수를 처리할 때 사용한다.
  2. Dispatchers.IO
    IO 작업 시 최적화된 스레드로, 네트워크 처리나 데이터베이스 관련 작업을 할 때 주로 사용한다.
  3. Dispatchers.Default
    그 외에 CPU 사용량이 많은 작업(정렬이나 JSON 파싱 작업)을 할 때 사용한다.
private val scope by lazy { CoroutineScope(Dispatchers.Main) }
private val scope by lazy { CoroutineScope(Dispatchers.IO) }

생성된 코루틴 스코프에서 작업들을 실행하기 위해선 코루틴 빌더가 필요하다. 코루틴 빌더는 launch{}, async{}, runBlocking{} 2개의 메서드를 사용할 수 있다. 둘 다 코루틴을 실행하지만, 결과를 반환하는가 반환하지 않는가에 차이가 있다.

  1. launch{}
    내부적으로 코루틴을 만들어서 반환하는데, 결과가 아닌 job이라는 객체가 반환된다. 반환받은 job 객체를 활용해 코루틴을 취소할 수 있고, 완료될 때까지 기다릴 수도 있다.
	fun main() {

		GlobalScope.launch {
    		delay(1000L) //suspend 함수
        	println("World!")
    	}

		println("Hello, ")
    	Thread.sleep(2000L) //UI가 차단되는 함수
	}
    출력:
    Hello, 
    World!
	fun main() {

	GlobalScope.launch {
    	delay(3000L) 
        println("World!")
	}

	println("Hello, ")
	Thread.sleep(2000L) 
	}
    
    출력:
    Hello,
위 예제의 경우, World!는 3초 뒤에 출력되도록 했지만 메인스레드가 2초 뒤에 종료되기 때문에 World!가 출력될 수 없다. 이 경우 반환받은 Job 객체를 활용하여 해결할 수 있다.
	fun main () = runBlocking {

		val job = GlobalScope.launch {
			delay(3000L)
            println("World!")
        }

		println("Hello,")
        job.join() //job이 완료될 때까지 기다림
    }
    
    출력:
    Hello,
    World!
	fun main() = runBlocking {

		val job = launch {
    		repeat(1000){
        		println("job: I'm sleeping $i...")
            	delay(500L)
       		}
    	}

		delay(1300L)
    	println("main: I'm tired of waiting!")
    	job.join()
    	println("main : quit")
	}

	출력:
    job: I'm sleeping 0...
    job: I'm sleeping 1...
    job: I'm sleeping 2...
    job: I'm sleeping 3...
    job: I'm sleeping 4...
    ...
    main: I'm tired of waiting!
    main : quit
위의 예제는 0.5초 간격으로 1000번의 작업을 모두 다 실행한 뒤에 종료될 것이다. 만약 여기서 cancel()을 호출한다면, 메인스레드가 1.3초 뒤에 종료되어 작업이 도중에 종료된다.
	fun main() = runBlocking {

		val job = launch {
    		repeat(1000){
        		println("job: I'm sleeping $i...")
            	delay(500L)
       		}
    	}

		delay(1300L)
    	println("main: I'm tired of waiting!")
    	job.cancel()
    	job.join()
    	println("main : quit")
	}

	출력:
    job: I'm sleeping 0...
    job: I'm sleeping 1...
    job: I'm sleeping 2...
    main: I'm tired of waiting!
    main : quit
  1. async{}
    await()라는 suspend 함수를 사용하여 결과를 Deferred로 감싸서 반환한다. Deferred 객체는 await() 함수를 통해 코루틴이 끝날 때까지 결과를 기다렸다가 결과를 반환한다.
	val deferred : Deferred<String> = async {
            var i = 0
            while (i < 10) {
                delay(500)
                i++
            }
            "result"
        }
	val msg = deferred.await() //결과 반환
	println(msg) // 결과가 반환되면 result 출력
  1. runBlocking{}
    launch와는 다르게 자신을 호출하는 스레드를 블로킹한다.

Job은 launch{}로 생성된 코루틴의 상태를 관리하는 용도로 사용하고 결과값을 리턴받을 수 없으나, Deferred는 async 블록 내 수행된 결과를 원하는 시점에 반환받을 수 있다는 차이가 있다. 기본적으로 Deferred는 Job을 상속받아 구현되었기 때문에 Job의 기능을 사용할 수 있다.

suspend fun?

코루틴은 기본적으로 일시중단이 가능하고, 코루틴의 일시 중단 기능은 코루틴 스코프 내에서만 가능하다. suspend fun은 일시 중단이 가능한 함수로, 코루틴 스코프 또는 suspend fun 함수 내에서만 사용할 수 있다.

coroutineScope.launch {
	runTask()
}
...
suspend fun runTask() : String = "completed"

References

https://www.geeksforgeeks.org/kotlin-coroutines-on-android/
https://www.section.io/engineering-education/comparing-rxkotlin-to-kotlin-coroutines/
StackOverFlow - RxJava vs Coroutines
https://blog.danlew.net/2021/01/28/rxjava-vs-coroutines/
https://developer.android.com/kotlin/coroutines?hl=ko&gclsrc=aw.ds&gclid=CjwKCAjw682TBhATEiwA9crl31Df7c3zT08jFjwYU-DWg4hlj7ii7W88nnN2D9zPRUHBqcakN70z3xoCtRkQAvD_BwE#dependency
Structured Concurrency
https://thgus13.tistory.com/25
https://proandroiddev.com/kotlin-coroutines-in-android-summary-1ed3048f11c3

profile
Mobile Software Engineer

0개의 댓글