본문 바로가기
Android/Kotlin

Android Kotlin Coroutines 사용하기

by 2Plus 2020. 5. 12.

 

 기존에는 async task를 하기 위해서 스레드, 실행, 콜백 등을 구현했다. 물론 이를 구현하는 데 있어서 큰 불편함은 없지만 코드를 많이 짜야 하고 지저분해지기가 쉽다. 특히나 콜백을 많이 사용하면 코드를 볼 때 이해하는 데에도 시간이 많이 걸리게 될 수 있다. Kotlin에서는 Coroutines(코루틴)을 사용하여 async task(비동기 작업)를 편하게 할 수 있다. 이번 글에서는 Coroutine에 대해 알아볼 것이다.

 

 

Coroutines (코루틴)

 Coroutine은 비동기 작업을 순차적으로 진행해주는 점에 있어서는 thread와 비슷한 개념이다. 비슷하지만 thread에 비해 더 가볍고 많은 기능이 있어서 더 좋다고 볼 수 있다. 유지 관리 면에 있어서도 훨씬 편하고 단일 쓰레드에서도 많은 Coroutine이 실행될 수 있다. 함수 선언부 앞에 suspend 키워드를 사용하여 suspended function을 지정해줄 수 있고, Coroutine 부분에서 해당 함수 부분이 비동기로 진행된다.

 

 

 간단한 예시로 위와 같이 사용하는 개념이다. coroutineScope 부분은 그대로 사용하는 것이 아니라 어떠한 Coroutine 스코프라는 뜻에서 표기해둔 것이다. 실제 사용시에는 위처럼 그대로 사용하면 안 된다. getName() 함수에서 시간이 걸려서 비동기 작업이 필요하다면 위와 같이 선언하고, Coroutine 스코프 내에서 해당 함수를 사용하고 결과를 이용하는 부분을 사용하면 비동기 처리로 진행된다. 사용에 있어서 한 가지 주의할 점은 suspend 키워드로 선언된 함수는 다른 suspend 함수나 Coroutine 내에서만 호출이 가능하다는 점이다.

 

 

Builder

 

 안드로이드 앱으로 예시를 작성하여 보면 위와 같다. GlobalScope의 launch를 사용하여 Coroutine scope를 만들어 사용했다. GlobalScope의 launch를 사용하였기 때문에, 전역 범위에서 Coroutine을 시작한 것이다. 앱의 수명이 유지되는 동안 유지되는 스코프이다. 가장 사용하기 편한 것이 launch이지만, async, runBlocking, buildSequence 등과 같은 것들도 있다. Coroutine 라이브러리는 확장성이 좋고 사용자가 필요한 만큼 정의할 수도 있다. 우선 위 예시의 실행 결과는 다음과 같다.

 

 코드에서 작성한 대로 Coroutine 부분이 비동기로 진행되는 동안 onCreate 함수는 끝까지 다 실행되었고, 2초 뒤에 Coroutine 부분의 동작이 완료되면 텍스트가 변경되는 것을 확인할 수 있다.

 

 

Context

 Coroutine 빌더는 인자로 CoroutineContext를 받을 수 있다. 따로 지정해주지 않으면 기본적으로는 EmptyCoroutineContext를 사용하도록 되어 있다. Context는 간단하게 생각하면 Coroutine이 실행되는 규칙이라고 볼 수 있다. Coroutine이 실행되는 방법을 정의해서 사용할 수도 있고, Coroutine 진행 중에 발생한 예외를 처리하는 방법도 사용할 수 있다. 다음의 코드를 보면 이해를 할 수 있다.

 

 

 위와 같이 하면 정상적으로 동작하기 때문에 실행했을 때의 모습은 앞의 예시 코드의 결과와 동일하다. 코드를 한 번 살펴보면, 빌더에 context를 지정하여 직접 만든 예외 핸들러를 사용하여 IO 디스패처로 Coroutine을 실행하고 직접 생성한 Job()에 바인딩하는 식으로 동작하는 것을 확인할 수 있다. 만약에 예외가 발생한다면 직접 만든 예외 핸들러가 동작할 것이다. Coroutine은 주어진 스레드에서 제한되어 동작하거나 스레드 풀을 사용하여 동작할 수 있다. 라이브러리에서 제공되는 이미 구현된 옵션들이 있기 때문에 이러한 것들을 잘 사용하면 좋다. 위의 Dispatchers.IO의 경우는 네트워크나 파일 시스템 작업에 최적화된 것이다. 경우에 따라서 필요한 것을 사용하고, 적합한 것이 없다면 직접 만들어서 사용하면 된다.

 

 

Job

 앞선 Context 설명에서 Job()을 사용하는 부분이 있었다. 여기에 대해서 조금 더 정리해보기 위해서 단락을 나눴다. 

 

 Job은 백그라운드에서 수행해야 할 작업의 인스턴스이다. parent-child 계층으로 정렬될 수 있고, 부모 작업이 취소되면 자식 작업들도 당연히 바로 취소된다. 앞에서 봤던 예시 코드에서 Job()을 Coroutine의 컨텍스트로 전달함으로써 Coroutine의 부모 작업을 원하는 곳에서 제어할 수 있다. Coroutine이 동작하고 있는 동안 job.cancel()을 사용하면 코루틴 실행까지 바로 종료시킬 수 있다.

 

 

Scope

 앞의 예시들에서는 GlobalScope를 사용하였는데, 이는 앱이 죽지 않으면 계속 실행될 수 있는 scope이다. 편하기는 하지만 앱이 살아있는 동안 계속 살아있어야 하는 Coroutine 작업이 아니라면 GlobalScope를 사용하는 것은 바람직하지 않다. Coroutine에서 하는 동작이나 그로부터 나오는 결과가 더 이상 필요하지 않을 때는 보통 Coroutine 작업이 진행 중일 때 사용자가 다른 화면으로 넘어가는데 그때부터는 앞에서 진행하고 있던 작업이 더 이상 필요가 없을 때일 것이다. 이때는 GlobalScope보다는 필요한 곳의 생명 주기와 맞춰주는 것이 바람직하다. 그렇지 않으면 메모리 누수가 발생할 가능성이 높다.

 

 

 CoroutineScope를 상속받고, 거기서 launch를 수행하며 해당 Activity가 종료될 때 Coroutine에 Context로 넘겨줬던 Job을 종료시키면 Coroutine 작업도 종료시킬 수 있다. 이렇게 하면 메모리 누수 없이 효율적으로 관리할 수 있다. 개인적으로 async task를 thread로 관리하는 것보다 더 깔끔하고 편해 보인다. DataBase 사용시 Room과 함께 사용하기에도 좋으니 잘 알아두면 좋을 것 같다.

반응형

댓글