티스토리 뷰

반응형

들어가며

Google에서 Kotlin를 Android 공식 언어로 추가함에 따라 많은 개발자들이 Java에서 Kotlin으로 변경했을 것입니다.

 

단순히 Google에서 Kotlin을 Android 공식언어로 추가했다고 Java 을 Kotlin으로 변경하였을까요?

Java의 단점 보완한 Kotlin는 Java와 호환도 잘되어 Java로 구현된 오픈소스 라이브러리를 그대로 사용할 수 있어서 많은 개발자들이 Java에서 Kotlin으로 변경하였을 것입니다.

 

Kotlin 으로 변경하였을 때 가장 큰 장점이 무엇이라고 생각되시나요? 필자는 비동기 처리 방식으로 생각합니다.
Java에서 비동기 처리는 대표적으로 RxJava을 활용해서 처리하였을 것입니다. Kotlin을 사용하는 대부분의 개발자들은 Coroutine 을 사용하여 비동기 처리 하고 있습니다. 

 

이번 포스트는 Java에서 사용했던 RxJava을 활용한 비동기 처리가 아닌 Kotlin 에서 비동기 처리 시 사용하는 Coroutine에 대해서 알아보는 시간을 갖겠습니다.

 

 

Android Kotlin Coroutines 사용해보기

서두에 언급했듯이 비동기 처리하는 방식은 다양한 방식으로 사용했을 것입니다.

Kotlin 에서 사용하는 비동기 처리의 Coroutine은 RxJava 보다는 진입장벽이 낮고 사용하기도 편리합니다.

 

Android Developer 사이트 내용 기반으로 Coroutiness 을 알아보겠습니다.

 

Android 프로젝트에서 코루틴 사용하기전 app의 build.gradle 파일에 다음 항목을 종속해야합니다.

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

 

viewModelScope는 ViewModel KTX 확장 프로그램에 포함된 사전 정의된 CoroutineScope입니다. 모든 코루틴은 범위 내에서 실행해야 합니다. CoroutineScope는 하나 이상의 관련 코루틴을 관리합니다.

 

참고로 안드로이드에서 제공하는 ViewModel 을 사용 시 viewModelScope 을 사용할 수 있습니다.
launch는 코루틴을 만들고 함수 본문의 실행을 해당하는 Dispatcher에 전달하는 함수입니다.
Dispatchers.IO는 이 코루틴을 I/O 작업용으로 예약된 스레드에서 실행해야 함을 나타냅니다.

 

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

 

코루틴을 사용하려면 Dispatchers 를 알아야하는데요. Kotlin은 코루틴을 실행할 위치를 지정하는데 세 가지 Dispathers 를 제공합니다.

  • Dispatchers.Main - 기본 Android 스레드에서 코루틴을 실행합니다. UI와 상호작용하고 빠른 작업을 실행하기 위해서만 사용해야 합니다. 예를 들어 suspend 함수를 호출하고 Android UI 프레임워크 작업을 실행하며 LiveData 객체를 업데이트하는데 사용합니다.
  • Dispatchers.IO - 기본 스레드 외부에서 디스크 또는 네트워크 I/O를 실행하도록 최적화되어 있습니다. 예를 들어 파일에서 읽거나 파일에 쓰며 네트워크 작업을 실행하는데 사용합니다.
  • Dispatchers.Default - 이 디스패처는 CPU를 많이 사용하는 작업을 기본 스레드 외부에서 실행하도록 최적화되어 있습니다. 예를 들어 목록을 정렬하고 JSON을 파싱합니다.

 

만약, 현재 정의한 Dispatcher보다 더 길게 코루틴을 사용해야할 경우 어떻게 해야할까요? 예를들어서 현재 View 가 종료되어 코루틴이 끊기는 현상을 막을 수 있는 방법은 없을까요?

 

질문을 듣자마자 Application 에서 처리하면 될거 같은데.. 라고 생각하신 분들도 계셨을 겁니다.

코루틴을 길게 유지하는 여러 방법이 있지만 가장 쉽게 접근할 수 있는 방법은 Application 에서 코루틴을 구현하는 것이라 생각됩니다. 그렇다면 어떻게 Application 에서 코루틴을 구현할 수 있을까요?

 

kotlinx.coroutines.MainScope() 을 활용하면 Application의 CoroutineScope를 구성할 수 있습니다. LowMemory 에 구성하는 것은 선택 사항이지만 Application 의 수명 동안 거기에 남아 시스템 리소스를 사용할 수 있기 때문에 구성해주는 방식은 좋은 방식인거 같습니다.

 

class ApplicationClass : Application() {

    companion object {
        var applicationScope = kotlinx.coroutines.MainScope()
    }
    
    override fun onLowMemory() {
        super.onLowMemory()
        applicationScope.cancel("onLowMemory() called by system")
        applicationScope = MainScope()
    }
}

 

 

Android Coroutine + Flow

Android Coroutine 환경에서 API 통신 뿐만 아니라 Flow 을 활용해서 흐름의 끊김이 없이 데이터 업데이트를 가능하게 처리할 수있습니다.

Flow는 코루틴 환경에서 동작하도록 구성되어있습니다. 그럼 코루틴 환경에서 동작하는 suspend function과 차이점이 무엇일까요?

suspend functions 에서는 단일 값을 반환해주는 반면 Flow는여러 값을 순차적으로 내보낼 수 있습니다.

val favoriteLatestNews: Flow<List<ArticleHeadline>> =
        newsRemoteDataSource.latestNews
            // Intermediate operation to filter the list of favorite topics
            .map { news -> news.filter { userData.isFavoriteTopic(it) } }
            // Intermediate operation to save the latest news in the cache
            .onEach { news -> saveInCache(news) }
}

 

위의 소스처럼 Flow은 비동기식으로 계산할 수 있는 Data Stream 을 입니다. 다르게 표현하면 Data Stream 으로 구성되어 중간에 수정이 가능합니다. 단, Stream 수정이 이루어져도 반환 Type은 동일해야합니다.

 

 

 

Coroutine 을 사용한 Flow 예제 또한 Android Developer 사이트 내용 기반으로 Flow 을 알아보겠습니다.

먼저 Flow 을 가져오는 로직을 보겠습니다. 아래 코드는 5초만다 인터벌 돌면서 뉴스를 저장하는데요.
여기서 사용한 Flow 빌더는 코루틴 내에서 동작합니다.

 

주의할 점은 Flow 빌더 생성 시 CoroutineContext 와 값 반환 할 때에 CoroutineContext가 다를 때 값을 return 하지 못합니다.

Flow 빌더에서는 동일한 CoroutineContext 를 사용해야 합니다.

만약 새로운 코루틴을 생성 또는 witchConext 코드 블럭을 사용할 경우 CoroutineContext가 달라져 값을 return 할 수 없습니다.

class NewsRemoteDataSource(
    private val newsApi: NewsApi,
    private val refreshIntervalMs: Long = 5000
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        while(true) {
            val latestNews = newsApi.fetchLatestNews()
            emit(latestNews) // Emits the result of the request to the flow
            delay(refreshIntervalMs) // Suspends the coroutine for some time
        }
    }
}

// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
    suspend fun fetchLatestNews(): List<ArticleHeadline>
}

 

앞써 언급했듯이 FlowData Stream 으로 Stream 수정이 가능합니다.

예를들어 아래 소스를 보면 DtatSource 에서 받은 Flow 을 map 메소드를 활용해서 수정 후 반환 할 수 있음을 확인할 수 있습니다.

 

class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val userData: UserData
) {
    /**
     * Returns the favorite latest news applying transformations on the flow.
     * These operations are lazy and don't trigger the flow. They just transform
     * the current value emitted by the flow at that point in time.
     */
    val favoriteLatestNews: Flow<List<ArticleHeadline>> =
        newsRemoteDataSource.latestNews
            // Intermediate operation to filter the list of favorite topics
            .map { news -> news.filter { userData.isFavoriteTopic(it) } }
            // Intermediate operation to save the latest news in the cache
            .onEach { news -> saveInCache(news) }
}

 

 

마무리

코틀린의 코루틴 개념과 Flow을 간단히 나눴습니다. 실무에서도 많이 사용하는데 정확히 알고 사용하는 개발자는 적을 것으로 생각됩니다. 회사의 개발 환경 및 사용에 따라서 Coroutine Dispatcher을 사용하고 있는지 한번 점검 하는 것을 제안드립니다.

 

Coroutine과 Flow 을 활용해서 개발 할 시 주의 사항으로 언급했던 CoroutineContext 이 동일한지 체크하며 개발하는 것도 좋을거 같습니다.

 

이번 포스트에서 언급한 Coroutine과 Flow 을 활용하여 개발 속도와 성능 둘다 챙길 수 있으면 좋겠습니다.

 

 

참고

Android Dev Summit - Coroutine

Droid Knights 2019 Kotlin Corouines - Slide

Droid Knights 2019 Kotlin Corouines - VOD

 

반응형
댓글