안녕하세요 강남언니에서 안드로이드 개발하는 Dalton 입니다.

강남언니 안드로이드 프로젝트에 Kotlin Coroutine 을 1주일이라는 시간동안 앱내의 모든 백엔드 서버연동시에 사용될 수 있게 적용했던 내용을 공유하고자 합니다.
(본문에서는 간단하게 Coroutine 으로 표기하겠습니다)

왜 Coroutine 을 적용하려고 했나?

저희가 Coroutine 적용으로 얻고자 했던 것은 크게 두가지가 있습니다.

첫번째, 퍼포먼스 향상

Coroutine 을 쉽게 이해하도록 기술된 표현중에 Light Weight Thread 라는 표현이 있습니다. 말 그대로 Coroutine 은 기존 Java Thread 에 비해 CPU 자원을 더 효율적으로 사용합니다. 물론 Coroutine 도 Java Thread 기반으로 동작하지만 suspend 라는 개념의 구현으로 낭비되는 자원을 획기적으로 줄여줍니다. 이해하기 쉽게 설명된 글이 있어 가져와 보았습니다.

What is suspend function in Kotlin Coroutines?

위 장표의 그림을 보시면 suspend 의 개념을 아주 쉽게 그림으로 표현하고 있습니다. Coroutine 은 이것을 구현하기 위해 개발자가 작성한 Coroutine Block 혹은 suspend function 안의 코드들을 State Machine 을 이용한 코드로 변경해서 처리하고 있습니다.

두번째, 비동기 코드의 쉬운 작성

개발자에게 비동기 코드의 작성은 항상 불편하고 신경쓰이는 작업입니다. 비동기 코드를 작성하고 그 결과를 받아서 처리하는 코드를 따로 작성해야하고 비동기 코드가 정상적으로 완료되지 않았을 때 발생할 수 있는 모든 케이스를 고려하여 처리하는 작업을 해야만 합니다. Coroutine 은 이런 비동기 코드를 동기코드 작성할 때의 매너 그대로 작성할 수 있도록 해줍니다.

최근의 비동기코드 작성방법은 크게 세가지로 나눠볼 수 있습니다. 저희는 아래 예시중 마지막 방식으로 코드를 작성하고 싶었습니다.

Callback 방식

예시와 같이 indentation 이 깊어지다보면 callback hell 을 만날 수 있습니다.

Future, Promise, Rx.Observable 등을 이용한 방식

indentation 이 없어지기 때문에 코드가독성은 그나마 좋습니다. 하지만 call 이 누적될수록 exception 발생시 영향도 파악이 어려워지고 모든 transformation 에 대한 파악도 어렵습니다. 그리고 중간에 끼어있는 lambda block 들로 인해 코드가독성도 동기코드에 비해서는 여전히 좋지 않습니다.

Coroutine

동기코드 작성시와 동일한 가독성과 간결함을 유지합니다.

예시코드는 아래 블로그에서 발췌 했습니다.

Comparing Kotlin Coroutines with Callbacks and RxJava | Lukas Lechner

어떤 방법으로 Coroutine 을 적용했나?

저희가 사용했던 방법은 크게 두가지가 있습니다.

suspendCancellableCoroutine

suspendCancellableCoroutine 은 callback 형식의 매너로 제공되는 API 들을 Coroutine 의 매너로 작성할 수 있게 도와줍니다. 아래 예시의 코드는 Retrofit2 Library 의 KotlinExtensions.kt 의 내용중 일부입니다. 우리의 Rockstar Jake는 우리 안드로이드 개발자들을 위해서 Retrofit2 library 에서 suspend keyword 를 지원하고 있습니다. 아래 코드가 바로 그 내용중 일부입니다. suspendCancellableCoroutine 을 이용해서 okhttp 의 enqueue 를 이용한 callback 방식 코드를 suspend function 으로 바꾸어줍니다.

적용방법은 매우 간단합니다. Retrofit service 정의할 때 function 에 suspend 키워드만 추가해주면 적용이 됩니다. 그리고 호출해서 이용하는 부분은 Coroutine 블록내에서 혹은 다른 suspend function 내에서 사용하게 됩니다.

이렇게 사용하는 경우는 실제 네트워킹 코드는 OkHttp 에서 관리하는 Thread Pool 을 사용하게 되고, onResponse 가 호출되면서 부터 Coroutine Dispatcher 를 사용하게 됩니다. Retrofit 에서 suspend keyword를 왜 이런 방식으로 처리했는지 궁금해서 Retrofit 의 Github 에 문의한 적이 있습니다. Jake의 답변 중 일부를 인용하자면 아래와 같습니다.

We could just as easily do withContext(IO) { call.execute() } which uses a thread from the coroutines library rather than one from OkHttp.
The advantage of using OkHttp is that it can share a single thread for the majority of a HTTP/2 connection's bookkeeping and only move to a dedicated thread when reading the response body.

간단히 정리하자면, withContext(IO) 와 같은 Coroutine Builder 를 이용해서 쉽게 OkHttp 의 Thread 대신 Coroutine 을 활용하도록 구현할 수 있지만, OkHttp 의 장점을 살리기 위해서 suspendCancellableCoroutine 을 사용했다고 합니다. 그래서 OkHttp 에서 구현하고 있는 HTTP/2 통신방식의 장점을 그대로 활용할 수 있습니다. 저는 처음엔 Network Call 자체부터 Coroutine 을 활용하는것이 좋지않나 생각했지만, Jake 의 답변으로 그 의문점이 해소가 되었습니다. 이후로는 asSingle 보다는 suspend keyword 를 통해서 사용하는 것을 기본으로 하고 있습니다.

asSingle

asSingle

위의 API 는 Coroutine 의 async 블록의 return type 인 Deferred 의 extension function 입니다.

강남언니 안드로이드 프로젝트의 코드중에는 RxJava 를 이용해서 여러개의 비동기 작업을 merge 혹은 zip function 으로 동시처리하는 코드가 많습니다. 1주일이라는 제한된 기간동안 이 코드를 모두 Coroutine 매너로 변경하는 것은 어려웠기 때문에 그런 코드들은 asSingle 을 이용해서 기존코드가 그대로 동작할 수 있게 처리했습니다. 이를 위해서 Retrofit 에 등록해서 사용할 수 있는 CallAdapter.Factory 를 구현하여 사용했습니다. 아래는 그 코드중 일부입니다. T타입의 리턴객체를 Single 로 변환하는 Call Adapter 입니다.

Retrofit Service 를 선언할 때 리턴타입을 Single 로 선언하게 되면 해당 Adapter 를 통해서 실제 Network Call 은 Coroutine 의 Async 블럭에서 실행되고 이후에 Single 로 변환된 결과를 반환하게 됩니다.

위와 같은 Adapter 들로 구성된 CallAdapter.Factory 를 RxJava2CallAdapterFactory 를 대신해서 등록하여 사용하게 됩니다.

위의 suspendCancellableCoroutine 과 비교해서 Network Call 자체가 Coroutine Dispatcher 를 통해서 실행되는 방법입니다. 예시화면의 코드중 executeCoroutineAsync 함수에서 그 부분을 처리하고 있습니다. 이 부분은 기존 RxJava.Observable 혹은 RxJava.Single 을 이용하여 구현된 코드와의 호환성을 위해서 사용하는 방법이라고 보시면 될 것 같습니다.

그래서 적용하고 나니 진짜 좋은가?

저희는 이렇게 모든 백엔드 서버와의 연동시에 Coroutine 을 사용하게 한 이후에 퍼포먼스 향상이 있었는지 한가지 테스트를 진행했습니다. 4개의 백엔드 API 를 호출하고 그 결과를 zip 으로 묶어서 처리하는 코드를 반복 실행하면서 처리속도의 차이를 확인했습니다. 아래는 그 결과입니다.

결과는 생각보다 큰 차이를 보였습니다. 사실 퍼포먼스 향상에 대한 큰 기대는 없었는데 말이죠.
물론 해당 케이스는 4개의 Network Call 을 동시에 하는 드문 케이스 였지만, 그래도 분명히 퍼포먼스의 큰 차이가 있습니다. 이렇게 큰 차이를 보이는 이유에 대해서 더 깊게 파고들어 분석하고 싶었지만 시간의 제약으로 다음으로 미루고 작업을 마무리하게 되었습니다. 개인적으로 막연하게 예상해 본 이유로는 OkHttp 에서 동일한 호스트에 대해 동일한 Connection 을 사용하면서 HTTP/2 의 통신방식으로 연동을 하다보니 요청이 완전하게 병렬로 이루어지도록 구현한 Coroutine 코드와 상대적인 차이를 가지지 않았나 생각해 보았습니다. 만약 이 추측이 맞다면, 동일한 Thread 자원을 이용하는 상황에서는 이렇게 큰 차이가 나지는 않을것을 예상 할 수 있습니다.

맺음말

Coroutine 적용 작업을 마치고 QA를 진행하면서 예상보다 이슈가 많지않았고 일주일간의 QA 이후에 성공적으로 프로젝트에 적용이 되었고 배포가 될 수 있었습니다. Code Changes 를 최소화 하면서 Coroutine의 장점만을 얻으려고 했던 계획이 나름 성공한 뿌듯한 작업이었습니다.
아직 Coroutine 을 사용하지 않으신다면 한번 사용해 보시기를 권합니다.
감사합니다.

Dalton
Android Developer
강남언니에서 안드로이드 개발을 하고 있습니다. 생산성이 좋고 그것을 지속가능한 코드디자인을 찾아가는 개발자 입니다.