안녕하세요 강남언니에서 안드로이드 개발하는 Dalton 입니다.
강남언니 안드로이드 프로젝트에 Kotlin Coroutine 을 1주일이라는 시간동안 앱내의 모든 백엔드 서버연동시에 사용될 수 있게 적용했던 내용을 공유하고자 합니다.
(본문에서는 간단하게 Coroutine 으로 표기하겠습니다)
왜 Coroutine 을 적용하려고 했나?
저희가 Coroutine 적용으로 얻고자 했던 것은 크게 두가지가 있습니다.
첫번째, 퍼포먼스 향상
Coroutine 을 쉽게 이해하도록 기술된 표현중에 Light Weight Thread 라는 표현이 있습니다. 말 그대로 Coroutine 은 기존 Java Thread 에 비해 CPU 자원을 더 효율적으로 사용합니다. 물론 Coroutine 도 Java Thread 기반으로 동작하지만 suspend 라는 개념의 구현으로 낭비되는 자원을 획기적으로 줄여줍니다. 이해하기 쉽게 설명된 글이 있어 가져와 보았습니다.
![](https://static.blog.gangnamunni.com/files/b3/b36ff28a-ce6f-492c-8fd6-900d8873b64a.jpeg)
What is suspend function in Kotlin Coroutines?
위 장표의 그림을 보시면 suspend 의 개념을 아주 쉽게 그림으로 표현하고 있습니다. Coroutine 은 이것을 구현하기 위해 개발자가 작성한 Coroutine Block 혹은 suspend function 안의 코드들을 State Machine 을 이용한 코드로 변경해서 처리하고 있습니다.
두번째, 비동기 코드의 쉬운 작성
개발자에게 비동기 코드의 작성은 항상 불편하고 신경쓰이는 작업입니다. 비동기 코드를 작성하고 그 결과를 받아서 처리하는 코드를 따로 작성해야하고 비동기 코드가 정상적으로 완료되지 않았을 때 발생할 수 있는 모든 케이스를 고려하여 처리하는 작업을 해야만 합니다. Coroutine 은 이런 비동기 코드를 동기코드 작성할 때의 매너 그대로 작성할 수 있도록 해줍니다.
최근의 비동기코드 작성방법은 크게 세가지로 나눠볼 수 있습니다. 저희는 아래 예시중 마지막 방식으로 코드를 작성하고 싶었습니다.
Callback 방식
예시와 같이 indentation 이 깊어지다보면 callback hell 을 만날 수 있습니다.
![](https://static.blog.gangnamunni.com/files/60/609c8ba9-ac63-4f99-9ee4-d83025a60ac8.jpeg)
Future, Promise, Rx.Observable 등을 이용한 방식
![](https://static.blog.gangnamunni.com/files/32/3296eb3e-3263-4476-95b8-fb7552caeeca.jpeg)
indentation 이 없어지기 때문에 코드가독성은 그나마 좋습니다. 하지만 call 이 누적될수록 exception 발생시 영향도 파악이 어려워지고 모든 transformation 에 대한 파악도 어렵습니다. 그리고 중간에 끼어있는 lambda block 들로 인해 코드가독성도 동기코드에 비해서는 여전히 좋지 않습니다.
Coroutine
![](https://static.blog.gangnamunni.com/files/05/05313037-85f6-4ed6-8b72-7d62270daf50.jpeg)
동기코드 작성시와 동일한 가독성과 간결함을 유지합니다.
예시코드는 아래 블로그에서 발췌 했습니다.
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 으로 바꾸어줍니다.
![](https://static.blog.gangnamunni.com/files/fe/fef3a529-25b1-46e4-90bf-289e6ab087af.jpeg)
적용방법은 매우 간단합니다. 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
위의 API 는 Coroutine 의 async 블록의 return type 인 Deferred 의 extension function 입니다.
강남언니 안드로이드 프로젝트의 코드중에는 RxJava 를 이용해서 여러개의 비동기 작업을 merge 혹은 zip function 으로 동시처리하는 코드가 많습니다. 1주일이라는 제한된 기간동안 이 코드를 모두 Coroutine 매너로 변경하는 것은 어려웠기 때문에 그런 코드들은 asSingle 을 이용해서 기존코드가 그대로 동작할 수 있게 처리했습니다. 이를 위해서 Retrofit 에 등록해서 사용할 수 있는 CallAdapter.Factory 를 구현하여 사용했습니다. 아래는 그 코드중 일부입니다. T타입의 리턴객체를 Single
Retrofit Service 를 선언할 때 리턴타입을 Single 로 선언하게 되면 해당 Adapter 를 통해서 실제 Network Call 은 Coroutine 의 Async 블럭에서 실행되고 이후에 Single 로 변환된 결과를 반환하게 됩니다.
![](https://static.blog.gangnamunni.com/files/86/865b417b-534a-4a2b-a45f-31164134934b.jpeg)
위와 같은 Adapter 들로 구성된 CallAdapter.Factory 를 RxJava2CallAdapterFactory 를 대신해서 등록하여 사용하게 됩니다.
위의 suspendCancellableCoroutine 과 비교해서 Network Call 자체가 Coroutine Dispatcher 를 통해서 실행되는 방법입니다. 예시화면의 코드중 executeCoroutineAsync 함수에서 그 부분을 처리하고 있습니다. 이 부분은 기존 RxJava.Observable 혹은 RxJava.Single 을 이용하여 구현된 코드와의 호환성을 위해서 사용하는 방법이라고 보시면 될 것 같습니다.
그래서 적용하고 나니 진짜 좋은가?
저희는 이렇게 모든 백엔드 서버와의 연동시에 Coroutine 을 사용하게 한 이후에 퍼포먼스 향상이 있었는지 한가지 테스트를 진행했습니다. 4개의 백엔드 API 를 호출하고 그 결과를 zip 으로 묶어서 처리하는 코드를 반복 실행하면서 처리속도의 차이를 확인했습니다. 아래는 그 결과입니다.
![](https://static.blog.gangnamunni.com/files/61/61819d1b-8a4e-4b4d-9345-bfdf0f75a7a1.jpeg)
결과는 생각보다 큰 차이를 보였습니다. 사실 퍼포먼스 향상에 대한 큰 기대는 없었는데 말이죠.
물론 해당 케이스는 4개의 Network Call 을 동시에 하는 드문 케이스 였지만, 그래도 분명히 퍼포먼스의 큰 차이가 있습니다. 이렇게 큰 차이를 보이는 이유에 대해서 더 깊게 파고들어 분석하고 싶었지만 시간의 제약으로 다음으로 미루고 작업을 마무리하게 되었습니다. 개인적으로 막연하게 예상해 본 이유로는 OkHttp 에서 동일한 호스트에 대해 동일한 Connection 을 사용하면서 HTTP/2 의 통신방식으로 연동을 하다보니 요청이 완전하게 병렬로 이루어지도록 구현한 Coroutine 코드와 상대적인 차이를 가지지 않았나 생각해 보았습니다. 만약 이 추측이 맞다면, 동일한 Thread 자원을 이용하는 상황에서는 이렇게 큰 차이가 나지는 않을것을 예상 할 수 있습니다.
맺음말
Coroutine 적용 작업을 마치고 QA를 진행하면서 예상보다 이슈가 많지않았고 일주일간의 QA 이후에 성공적으로 프로젝트에 적용이 되었고 배포가 될 수 있었습니다. Code Changes 를 최소화 하면서 Coroutine의 장점만을 얻으려고 했던 계획이 나름 성공한 뿌듯한 작업이었습니다.
아직 Coroutine 을 사용하지 않으신다면 한번 사용해 보시기를 권합니다.
감사합니다.