본문 바로가기

Android/Asynchronous

[Asynchronous] Android의 Coroutine

Android내의 Coroutine전에! 먼저 Coroutine과 Routine이란??

- 루틴(Routine)의 정의

보통 우린 루틴이라고 하면 '특정한 일을 하기 위한 일련의 처리 과정'이라는 의미로 자주 사용한다. 프로그래밍에서의 루틴 또한 비슷한 의미를 가진다. 프로그래밍에서의 루틴은 '특정한 일을 처리하기 위한 일련의 명령'이라는 의미로 우리가 자주 개발 시에 자주 사용해 왔던 함수가 바로 그 루틴이라고 할 수 있다.

 

- 메인 루틴(Main Routine)과 서브 루틴(Sub Routine)

위에서 설명한대로 프로그램은 여러 함수, 즉 여러 루틴으로 나뉘게 된다. 이때 서브 루틴과 메인 루틴으로 나뉘게 되는데 서브 루틴이란 쉽게 얘기하자면 함수 내의 함수를 얘기한다. 메인 루틴은 서브루틴을 실행시키는 최상위 함수라고 이해하면 편할 것이다. 고로 메인루틴이 실행되지 않는다면 내부의 서브루틴 또한 실행되지 않을 것이다.

 

아래는 위의 설명에 관한 간단한 예시코드이다.

fun mainRoutine() {
   subRoutine() // 이 함수는 서브루틴인 것이다!
}

fun subRoutine() {
   println("Hello World!")
}

 

- 코루틴(Coroutine)의 정의

코루틴은 together를 뜻하는 Co와 위에서 설명한 Routine이 합쳐져 만들어진 단어로 코루틴에 대한 본격적인 개념은 1958년 도널드 어빈 커누스(Donald Ervin Knuth)가 자신의 어셈블리 프로그램에 적용하면서 시작되었다. 코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 사용될 수 있는 동시 실행 설계 패턴을 뜻한다.

 

- 코루틴(Coroutine)의 특징

코루틴의 특징을 쉽게 설명해보겠다. 생각해 보면 결국 루틴은 입구와 출구가 매우 명확하다. 즉 메인 루틴이 서브 루틴을 호출하는 순간 서브 루틴의 맨 처음 코드에 접근하여 retrun문을 만나거나 서브루틴을 닫는 괄호를 만난다면 해당 서브루틴은 종료되게 될 것이다. 이렇게 루틴은 단 하나의 입구와 출구를 가질 수 있는 반면에 코루틴은 여러 개의 입구와 출구를 가질수 있다. 

 

아래의 그림을 참고하자!

 

위의 그림은 코루틴을 사용해보지 않은 사람들에게는 혼란스러울 수도 있다. 하지만 결국 코루틴도 하나의 루틴이다. 즉 하나의 함수라는 얘기다. 하지만 함수의 진입점이 여러 개고, 함수를 탈출할 수 있는 탈출점도 여러 개다. 한마디로 코루틴 함수는 일반적인 루틴처럼 return문이나 닫는 괄호를 만나지 않더라도 언제든지 중간에 나갈 수 있고, 언제든지 다시 나갔던 지점으로 들어올 수 있다는 것이다.

 

 

- Android에서의 코틀린 코루틴(Kotlin Coroutine)

위에서 설명했던 코루틴을 코틀린을 공식 언어로 하는 안드로이드 프로그래밍에서도 사용이 가능하다. 코틀린 코루틴은 구글에서 안드로이드 개발 시 공식 권장 사항으로 쓰이고 있는데 그 이유는 안드로이드 개발시 메인 스레드(Main Thread)를 차단하지 않는 것이 중요하기 때문이다.

 

메인 스레드란 안드로이드 내의 UI에 대한 모든 업데이트를 처리하는 단일 스레드로서 네트워크 작업, 데이터베이스 작업같이 오래 걸리는 작업을 기본 스레드에서 호출할 시 앱이 일시 중지 되거나 끊김 또는 정지되는 현상이 나타날 수 있기 때문이다.

 

이와 같은 문제를 줄이기 위해 코틀린 코루틴을 적용하여 비동기적인 프로그래밍을 한다면 장기 실행 작업(Long-running tasks)으로 인한 메인 스레드 블로킹 현상을 줄이고 비동기 작업 중 예외 발생에 따른 메모리 누수를 방지할 수 있기 때문에 공식적으로 코루틴 사용을 권장한다.

 

 

- 그럼 왜 하필 코루틴??

메인 스레드 블로킹을 줄이기 위한 비동기 프로그래밍패턴에는 앞서 설명한 코루틴만이 존재하는 것이 아니다. 바로 콜백 패턴(Callback Pattern)인데 콜백을 사용하게 된다면 백그라운드 스레드에서 장기 실행 작업을 수행할 수 있다. 작업이 완료되는 순간 콜백이 호출되어 메인 스레드에서 결과를 알려주게 된다.

 

하지만 이러한 콜백에는 치명적인 단점이 있는데, 유명한 콜백 지옥이다.

콜백 지옥이란 콜백으로 받은 데이터로  또 다른 콜을 발생시키고 그 응답으로 받은 결과로 또 콜을 발생시키는, 안티 패턴이라고 할 수 있다.

 

아래는 비동기적인 프로그래밍을 위한 콜백 패턴 사용시 맞닥뜨릴 수 있는 콜백 지옥의 예시 코드이다.

fun fetchData() {
	MyApiClient.getDataFromServer(object : MyCallback1 {
    	override fun onSuccess(data1: Data1) {
        	MyApiClient.getMoreData(data, object : MyCallback2 {
            	override fun onSuccess(data2: Data2) {
                	MyApiClient.processData(data2, object : MyCallback3 {
                    	override fun onSuccess(result: Result) {
                        	updataUI(result)
                            }
                            
                            override fun onError(error: String) {
                            	showError(error)
                                }
                            })
                        }
                        
                        override fun onError(error: String) {
                        	showError(error)
                        }
                    })
                 }
                 
                 override fun onError(error: String) {
                 	showError(error)
                 }
             })
        }

코비의 Kotin Coroutine 콜백 예제 코드

 

위의 코드는 서버의 리스폰스를 토대로 API를 호출하는 깊이가 상당히 긴 코드인데 콜백형식으로 getDataFromServer의 Result를 토대로 다시 getMoreData함수를 부르는 코드이다. 만약 요구하는 API의 동작이 많아지면 많아질수록 깊이 또한 비례해서 매우 깊어질뿐더러 다른 프로그래머가 한 번에 이해하기에는 가독성이 무척이나 떨어진다. 이런 코드를 콜백 지옥이라 부른다.

 

하지만 만약 위의 콜백 기반의 코드순차 코드변경할 수 있다면 어떨까? 이때 사용하는 것이 바로 코루틴이다.

 

자 그럼 위에서 맞이한 콜백 패턴의 코드에 한번 코루틴을 적용해서 리팩토링 해보자!

fun fetchData() {
    CoroutineScope(Dispatcher.IO).launch {
        val data1 = MyApiClient.getDataFromServer()

        val data2 = MyApiClient.getMoreData(data1)

        val result = MyApiClient.proccessData(data2)

        updateUI(result)
    }
}

코비의 Kotin Coroutine 코루틴 적용 예제 코드

 

무수히 길었던 코드가 매우 간결해진 것을 볼 수 있다. 예외처리 부분은 제외하고 적용했지만 그걸 고려하더라도 콜백 패턴을 적용한 코드보다 코루틴을 적용한 코드가 훨씬 간결할 것이다.

 

코틀린 코루틴을 제대로 이해하기 위해선 위의 코드중 우리는 CoroutineScope 같은 코루틴 스코프(Coroutine Scope)와 Dispatcher같은 코루틴 컨텍스트(Coroutine Context) 마지막으로 launch와 같은 코루틴 빌더(Coroutine Builder)에 대해서 알아봐야한다.

 

 

- 코루틴 스코프(Coroutine Scope)

코루틴의 활동 범위로서 사용자가 원하는 범위의 스코프를 설정하여, 그에 대한 LifeCycle을 가진 코루틴을 생성할 수 있게 해주는 것을 말한다. 쉽게 말해 코루틴이 실행되는 영역을 말한다. 

MainScope : Main UI에 대해 사용할 수 있는 스코프

GlobalScope : 앱 전체의 LifeCycle을 가지는 스코프

LifecycleScope : Activity와 Fragment의 LifeCycle을 가지는 스코프

ViewModelScope : ViewModel의 LifeCycle을 가지는 스코프

LiveData : 호출시기의 LifeCycle을 가지는 스코프

 

- 코루틴 콘텍스트(Coroutine Context)

해당 코루틴의 실행되는 환경에 대한 정보를 담고 있는 것을 말한다. 코루틴 실행 환경에 대한 쉽게 말해 key와 element를 가지고 있는 map이라고 이해하면 좋을 것 같다.

Dispatcher : 코루틴이 어떤 스레드에서 실행될지 결정하는 역할을 하는 콘텍스트 다양한 Dispatcher를 지원한다.

- Main : UI 업데이트나 사용자 입력처리 등 메인 스레드에서 실행되어야 하는 작업에 최적화되어있다.

- IO : 네트워크 요청, 파일 입출력 등 I/O 작업에 최적화되어있다. 백그라운드 스레드에서 실행된다.

- Default : 대기시간이 없는 지속적인 작업에 최적화 되어있다. IO와 같이 백그라운드 스레드에서 실행된다.

- Unconfined : 현재 스레드에서 실행된다.

 

Job :  코루틴의 실행단위를 관리하는 객체, 코루틴의 상태를 가지고 있다. 코루틴 빌더의 launch를 사용했을 때 리턴 값을 의미한다.

- 코루틴 상태 추적 : 코루틴이 현재 어떤 상태에 있는지 확인할 수 있다. isActive, isComplted, isCancel led등의 속성을 통해 코루틴이 어떤 상태인지 추적할 수 있다.

- 코루틴 상태 관리 : start(), join(), canceldAndJoin(), cancel()등 메서드를 활용해 본인 및 자식 코루틴 상태 관리가 가능하다.

 

CoroutineExceptionHandler : Job 내부에서 에러가 발생했을 때, 에러를 처리할 수 있는 콘텍스트이다.

 

- 코루틴 빌더(Coroutine Builder)

비동기적인 작업을 선언하고 실행하기 위한 함수, 새로운 코루틴을 생성하는 역할을 한다.

Launch : 가장 일반적으로 사용되는 코루틴 빌더로, 비동기 작업을 코루틴으로 실행하고 결과를 반환하지 않을 때 사용한다. 주로 비동기 작업을 시작하고 블록에서 리턴값을 사용하지 않을 때 사용된다.

Async : 코루틴에서 값을 반환하고자 할 때 사용하는 코루틴 빌더이다. Launch와 달리 비동기 작업이 완료되면 결괏값을 반환한다. 결과 또는 예외를 포함하는 Deffered객체를 반환하며, await 함수를 통해 결과값을 얻을 수 있다.

runBlocking : 코루틴을 블로킹하여 실행해 결과를 기다리고자 할 때 사용한다.

withContext : 코루틴의 실행 콘텍스트를 변경하고자 할 때 사용된다. 주로 IO나 Main Dispatcher를 변경하여 다른 스레드나 메인(UI) 스레드에서 코루틴을 실행하고자 할 때 사용한다.

기타 함수 : produce, actor, flow, sequence 등의 빌더 함수가 있다. 

 

- suspend fun

자 이제 이러한 코루틴을 순차 코드로 간단히 작성하기 위해 Kotlin에서는 suspend라는 키워드를 제공한다. suspend 키워드를 함수 앞에 기재하여 이 함수는 코루틴이 적용되어 있다는 것을 나타낼 수 있다. 

 

아래는 간단한 예시 코드이다.

fun fetchData() {
    CoroutineScope(DIspatchers.IO).launch {
        val data1 = MyApiClient.getDataFromServer()

        val data2 = MyApiClient.getMoreData(data1)

        val result = MyApiClient.getDataFromServer(data2)

        updateUI(result)
    }
}

// suspend 키워드가 추가된 updateUI 함수 
suspend fun updateUI(result: Result) {
    Text(
        text = "${result}"
    )
}

suspend 키워드 적용 예제 코드

 

위와 같이 네트워크를 통해 사용자의 데이터를 가져온 후 UI에 적용하는 코드를 생각해 보겠다. 코틀린 코루틴을 알기 전에는 이와 같은 동작을 처리하기 위해서 스레드나 콜백 함수를 이용하여 처리해야 했을 것이다. 하지만 이런 방법은 앞서 설명한 것처럼 복잡도가 높아질수록 예외 처리 및 콜백 지옥과 같은 문제에 봉착하게 된다. 하지만 코틀린 코루틴에서는 함수 앞에 suspend 키워드를 붙이고, 비동기 작업을 마치 순차코드 작성하듯이 짤 수 있도록 도와준다!

 

글 쓰는 능력이 좋지 않아 이해하기 어려울 수 있다. 이해하기 어렵다면 이것만 기억하면 된다. 코루틴 중 코틀린 코루틴은 안드로이드 개발 시 각종 비동기 처리를 편리하게 해주는 가벼운 스레드라는 것이다!

 

긴 글 읽어주셔서 감사합니다. 오류가 있거나 질문이 있으시다면 댓글이나 메일 부탁드립니다.

 

참고한 자료

https://kotlinlang.org/docs/coroutines-guide.html

https://www.youtube.com/watch?v=eJF60hcz3EU&t=1590s
https://www.youtube.com/watch?v=PaGOJ3887Js&t=388s

 

'Android > Asynchronous' 카테고리의 다른 글

[Asynchronous] suspend 넌 코루틴이 아니야  (0) 2024.08.12