
앱이 사용자에게 보여주기 위해 개발자는 3가지의 단계을 거치게 된다.
이 세가지의 단계을 거치게 되면 안드로이드 사용자에게 앱을 통하여 무엇인가를 보여줄 수 있다.
이를 코드로 한번 살펴보면
fun onCreate() {
val data = getDataApi()
val processedData = data.filter { it.name } // 모든 데이터 중 'name'이라는것만 가져오기
view.show(processedData)
}
이렇게 작성될 수 있다.
하지만, 해당 코드는 실행될 수 없다.
이유는 안드로이드에서는 뷰를 담당하는 스레드는 단 한가지만 존재하기 때문이다. onCreate 함수가 메인 스레드에서 작동한다고 가정하면, getDataApi() 함수가 스레드를 블로킹할 것이다. 이로인해 앱이 크래쉬가 발생해 비정상적으로 종료될 것이다. 만일 getDataApi() 함수가 다른 스레드에서 작동을 하게끔 만든다면 뷰를 보여주는 부분인 view.show() 내부에 데이터가 존재하지 않기 때문에 이 또한 앱이 크래쉬가 발생한다. 뷰를 담당하는 스레드를 블로킹 하지 않기 위해 다양한 방법들이 탄생하였다.
블로킹이 가능한 스레드를 생성한 뒤, 메인 스레드에서 뷰를 처리하는 과정을 진행하게끔 스레드를 전환 시켜주는 방법이 문제를 해결하기 위한 가장 직관적인 방법이다.
fun onCreate() {
thread {
val data = getDataApi()
val processedData = data.filter { it.name }
runOnUiThread {
view.show(processedData)
}
}
}
하지만 이 방법 또한 몇가지 문제점이 있다.
- 스레드 실행 시 멈출 수 있는 방법이 없어 메모리 누수 문제 발생
- 2개 이상의 스레드 생성으로 비용 발생
- 스레드 전환으로 인한 복잡도 증가 및 관리의 어려움
해당 문제점을 이해하기 쉽게 풀어보자면, 먼저 뷰가 열렸다가 빠르게 닫았다고 생각해 보자.
이 문제를 해결하기 위해 또 다른 해결 방법이 존재한다.
콜백은 함수를 논블로킹으로 만들고 함수의 작업이 끝나면 호출된 콜백 함수를 넘겨준다. 콜백 패턴을 사용하는 함수는 다음과 같다.
fun onCreate() {
getDataApi { data ->
val processedData = data.filter { it.name }
view.show(processedData)
}
}
이처럼 콜백을 이용해 구현한 함수조차 중간에 스레드 작업을 취소할 수 없다. 취소 가능한 콜백 함수를 만들 수 있지만, 콜백 함수 각각에 대한 취소 처리뿐만 아니라 취소하기 위한 모든 객체를 분리하여 모아야 하므로 어려운 작업을 요구한다. 예시를 바꿔 3가지의 데이터를 얻어오는 로직을 보자.
fun showNews() {
getConfigFromApi { config ->
getNewFromApi(config) { news ->
getUserFromApi { user ->
view.showNews(user, news)
}
}
}
}
위 코드 또한 다음과 같은 문제점을 가진다.
- news 데이터를 얻어오는 작업과 사용자 데이터를 얻어오는 작업을 병렬로 처리 가능 하지만, 콜백 구조상 두 작업을 동시에 처리할 수 없다.
- 취소 가능하게 만들기 위해선 많은 리소스가 발생한다.
- 흔히 말하는 '콜백 지옥'이 만들어져 코드가 읽기 어려워진다.
- 작업의 순서를 다루기 힘들어진다.
이와 같은 문제로 인해 콜백 구조도 적절한 해결방법이 아닌것을 알 수 있다.
코틀린 코루틴은 특별한 기능을 가지고 있다. 특정 지점에서 멈추고 이후에 재개할 수 있는 기능인데, 코루틴을 사용하면 위에 있는 예시 코드들을 메인 스레드에서 실행하고 API에서 데이터를 얻어올 때 잠깐 중단시킬 수 있다. 코루틴을 중단 시켰을 때 스레드는 블로킹되지 않기 때문에 뷰를 바꾸거나 다른 코루틴을 실행하는 등 또 다른 작업이 가능하다. 때문에 다음과 같은 코드는 정상적으로 실행될 수 있다.
// 편의상 안드로이드에서 흔히 쓰이는 viewModelScope를 사용하였습니다.
// 다른 코루틴 스코프로도 범위를 지정할 수 있습니다.
fun onCreate() {
viewModelScope.launch {
val data = getDataApi()
val processedData = data.filter { it.name }
view.show(processedData)
}
}
이상적인 코드가 완성되었다. 해당 코드는 데이터를 불러오고 있으면 스레드를 블로킹하는 대신 코루틴을 잠시 멈춘다. 코루틴이 멈춰있는동안 메인 스레드에서 UI를 그리는 작업은 지속적으로 진행되고 데이터를 가져온다. 데이터를 가져오면 다시 중단된 지점부터 다시 시작하여 작업을 수행한다. 그렇다면 아까처럼 3가지의 데이터를 가져오는 코드도 코루틴으로 만들어보자.
fun showNews() {
viewModelScope.launch {
val config = getConfigApi()
val news = getNewsFromApi(config)
val user = getUserFromApi()
view.showNews(user, news)
}
}
해당 코드는 코루틴 스코프 안에 3개의 API를 순서대로 호출하게 된다. API를 호출하는데 각각 1초씩 걸린다고 가정하면 총 3초가 걸리게 된다. getNewsFromApi()는 getConfigApi()값을 필요로 하고 getNewsFromApi() 다음 작업인 getUserFromApi()는 어떠한 값도 필요로 하지 않는다. 이 경우 우린 병렬로 호출하여 3초에서 2초로 작업을 수행하게끔 변경이 가능하다. 병렬처리를 제공하고 있는 코루틴 라이브러리의 async/await을 사용하면 성능을 개선할 수 있다.
fun showNews() {
viewModelScope.launch {
val config = async { getConfigApi() }
val news = async { getNewsFromApi(config.await()) }
val user = async { getUserFromApi() }
view.showNews(user.await(), news.await())
}
}
위 내용들은 코틀린 코루틴 책의 내용을 이해하며 작성된 내용입니다. 코루틴 대신 RxJava와 리액티브 스트림으로 사용할 수 있지만 해당 내용은 높은 러닝커브를 요구하고 코루틴 사용에 대한 초점을 맞추기 위해 포스팅에서 제외하였습니다.
참고 문헌 - 코틀린 코루틴