본문 바로가기

Language/Kotlin

[Android] Coroutine Flow를 사용하여 API를 호출해보자.

728x90

DataStore를 사용하면서 Flow를 처음 적용해 보았는데, Flow를 조금 더 많은 범위에서 사용해보고자 한다.

하나의 게시글에 Flow에 대한 정리를 하는 것이 아니라, 하나의 작업을 진행할 때 정리가 필요하다 싶은 것들을 골라 글을 작성해볼 예정이다.

 

DataStore다음으로 Flow를 적용해볼 법 한 부분을 찾아보았을 때, 아무래 통신 부분인 API 호출 부분이 아닐까 싶어서 적용해 보았다.


우선,

Flow가 무엇인지 간단하게 설명하고 진행하도록 하겠다.

Flow란,

Coroutine을 기반으로 빌드되며, 비동기식으로 계산할 수 있는 데이터 스트림의 개념.

 

이라고 한다.

간단히 말하자면, 코루틴 상에서 리액티브 프로그래밍을 지원하기 위해 만들어진 것이라고 생각할 수 있다.

 

뭐, 흔히 알고 있는 ReactiveX대신 사용한다고 생각하면 된다.

동일하게 사용되고 있기 때문에, Rx와 Flow를 비교하고 어느 것을 사용할지 고민하는 글은 찾아보면 상당히 많이 나온다.

 

Flow를 사용하기 위해서는 Gradle에 다음을 추가해준다.

 

implementation "org.jetbrains.kotlinx:kotlinx=coroutines-core:1.3.3"

 

room에 대한 Gradle을 추가한 상태라면 이처럼 추가하지 않아도 Flow를 사용할 수 있지만,

정확하게 하기위하여 해당 라이브러리를 추가해 준다.

 

 

Gradle에 라이브러리를 추가하였으면, API 호출 부분에 Flow를 추가해주면 된다.

필자는 예전에 사용했던 Clean Architecture를 사용하여 Flow를 붙여보았다.

https://github.com/HeeGyeong/CleanArchitectureSample

 

GitHub - HeeGyeong/CleanArchitectureSample

Contribute to HeeGyeong/CleanArchitectureSample development by creating an account on GitHub.

github.com


해당 예제에서 추가할 부분은 API를 호출하고, 사용하는 7개의 부분에서 수정이 이루어 진다.

수정할 각 부분은 다음과 같다.

 

  • Data Layer
    • ApiInterface
    • DataSource
    • DataSourceImpl
    • RepositosyImpl
  • Domain Layer
    • Repository
    • Usecase
  • Presentation Layer
    • ViewModel

 

해당 부분에서 수정이 이루어지는 이유는 예전에 작성해둔 Clean Architecture에서의 Data Flow게시글을 확인해보면 이해가 쉬울 것이다.

 

Data Flow의 역순으로 이동하면서 Flow를 사용하기 위한 작업을 진행해주도록 하자.

 

1. ApiInterface

 

@GET("v1/search/movie.json")
suspend fun getSearchMovieFlow(
    @Query("query") query: String,
    @Query("start") start: Int = 1,
    @Query("display") display: Int = 15
): MovieResponse

 

이곳에서 주의할 점은, suspend를 사용해야 한다는 점이다.

Flow는 기본적으로 코루틴 위에서 동작하기 때문에 suspend를 반드시 붙여주어야 정상적으로 동작한다.

suspend를 붙이지 않는다고 해서 빌드 시에 오류가 발생하거나, 런타임 시에 오류가 발생하지는 않는다.

통신에 실패하지는 않지만, 데이터가 정상적으로 들어오지 않아 문제가 발생하기 때문에 반드시 suspend선언을 해주어야 한다.

 

2. DataSource

위에 선언한 ApiInterface를 직접 호출하는 부분은 DataSourceImpl 부분이다.

DataSourceImpl는 Interface인 DataSource를 사용하므로, DataSource 부터 작업하도록 한다.

 

fun getSearchMoviesFlow(
    query: String,
    start: Int = 1
): Flow<MovieResponse>

 

api를 호출하여 받아온 데이터를 Flow로 받아서 사용할 예정이기 때문에, return type을 Flow로 감싸서 선언해 준다.

여기서 List를 받아올 경우에도 마찬가지로 Flow<List<DataDTO>> 형태로 선언하여 받아오면 된다.

 

 

3. DataSourceImpl

 

override fun getSearchMoviesFlow(query: String, start: Int): Flow<MovieResponse> {
    return flow {
        emit(apiInterface.getSearchMovieFlow(query, start))
    }
}

 

Interface를 사용하는 부분인 DataSourceImpl에서는 override 하여서 사용하면 된다.

위에서 언급하였듯이 Flow로 return 되어야 하기 때문에 flow { .. } 를 사용하여 Flow로 감싸주고, emit을 사용하여 데이터를 생성시켜주면 된다.

 

emit이란, Flow 블록에서 데이터를 발행시켜주는 부분이라고 생각하면 된다.

emit을 사용하지 않으면 데이터가 정상적으로 생성되지 않아서 오류도 발생시키지 않고, 빈 데이터를 전달하여 아무런 동작을 하지 않는 것을 볼 수 있을 것이다.

 

4. Repository

3번에서 구현한 Interface는 RepositoryImpl에서 사용되는데, 앞서서 RepositroyImpl를 사용하기 위해서는 상위의 Repository부터 작성을 해준다.

 

fun getSearchMoviesFlow(
    query: String
): Flow<List<Movie>>

 

Flow를 사용하여 지금까지 작성해왔으므로, DataType은 Repository도 마찬가지로 Flow를 사용해주어야 한다.

해당 Repository에 선언된 함수를 override 하면서 API를 통해 받아온 데이터를 mapper를 통해 실제 앱에서 사용할 데이터로 변환시켜주게 된다.

 

5. RepositoryImpl

Repository라는 interface를 선언해주었으므로, 이를 구현하는 부분을 작성해준다.

 

override fun getSearchMoviesFlow(query: String): Flow<List<Movie>> {
    return flow {
        movieRemoteDataSource.getSearchMoviesFlow(query).collect {
            emit(mapperToMovie(it.movies))
        }
    }
}

 

3번과 마찬가지로 flow 블럭을 통해 Flow로 리턴을 하도록 한다.

여기서, dataSource.get~ 부분을 통해서 가져오는 데이터 타입은 Movie가 아닌 MovieResponse이다.

따라서, Mapper를 통해 MovieResponse를 Movie로 변환시켜서 반환시켜주고 이 데이터를 emit을 사용하여 반환시켜주도록 한다.

 

6. UseCase

4번에서 선언한 interface인 Repository를 인자로 받아 사용하는 부분은 UseCase이다.

따라서 다음으로는 UseCase에 해당 repository를 호출하는 부분을 작성하도록 하자.

 

fun getFlowData(
    query: String
): Flow<List<Movie>> = repository.getSearchMoviesFlow(query)

 

이전에는 invoke를 사용하여 별도의 메서드를 만들지 않고 UseCase를 사용했지만, 지금은 그것과 더불어 추가하는 부분이기 때문에 메소드를 추가하였다.

이 부분에서 repository의 구현부를 호출하게 된다.

 

7. ViewModel

마지막으로 UseCase의 호출부. 사용자의 입력을 통해 필요한 데이터를 호출하는 부분을 추가해준다.

 

fun requestMovieFlow() {
    
    ...

    // Kotlin Flow는 Coroutine에서 동작.
    viewModelScope.launch {
        getMoviesUseCase.getFlowData(currentQuery)
            .onStart { showProgress() }
            .onCompletion { hideProgress() }
            .catch { _toastMsg.value = MessageSet.ERROR }
            .collect { movies ->
                if (movies.isEmpty()) {
                    _toastMsg.value = MessageSet.NO_RESULT
                } else {
                    _movieList.value = movies as ArrayList<Movie>
                    _toastMsg.value = MessageSet.SUCCESS
                }
            }
    }
}

 

해당 함수는 xml에서 버튼을 클릭했을 때 호출하도록 구현되어있다.

 

우선, flow는 Coroutine에서 동작하기 때문에 ViewModelScope.launch를 사용하여 코루틴 위에서 동작할 수 있도록 하였다.

UseCase를 통해서 데이터를 호출하면 되는데, 기존에 사용하던 Rx의 형태와 비슷하게 구현해 보았다.

 

  • onStart == doOnSubscribe : 시작할 때 호출된다.
  • onCompletion == doAfterTerminate : 종료 시 호출된다.
  • catch == subscribe내부의 두 번째 블록(Throwable 블록) : 예외 발생 시 호출된다.
  • collect == subscribe : 데이터 컨트롤 부분

 

이처럼 Rx를 사용할 때와 동일한 처리가 이루어지도록 구현이 되어있다.

 

해당 함수가 호출되게 되면, Flow를 사용하여 API를 호출하고 데이터를 받아와서 UI에 데이터를 뿌려줄 수 있도록 로직이 완성되게 된다.


Flow에 대해서 처음으로 공부하면서 작성해 보았는데, 해당 예제를 작성하면서도 Rx와 다른 게 무엇이며 어떤 것이 장점인지는 직접 느끼지는 못하였다.

해외 포럼에서 비교하는 글을 확인해보면 각각의 장단점이 보이기는 하는데, 아직까지 체감은 되지 않는 것 같다.

하지만, Flow가 조금 더 발전 가능성이 있고 좋은 부분이 확실히 존재하고 있다고 하니 가능하면 flow에 대해서 많은 부분을 공부하고 적용해볼 생각이다.

 

필자가 작업하는 순서대로 게시글을 작성한 이유는, 한번 더 데이터 플로우를 기억하고 습득하기 위해서도 있지만, 필자가 막상 적용해 보려고 했을 때 막막함을 느끼는 경우가 많아서이다.

사용해보고 싶은데, 어디서 부터 시작해야 할지 몰라서 넘어가는 경우가 종종 있는 것 같아 혹시라도 그런 분들에게 도움이 되지 않을까 싶어 이런 형식으로 작성해보았다.

 

해당 게시글에 사용한 예제는 다음 Github에 올려두었다.

https://github.com/HeeGyeong/CleanArchitectureSample

 

GitHub - HeeGyeong/CleanArchitectureSample

Contribute to HeeGyeong/CleanArchitectureSample development by creating an account on GitHub.

github.com

 

728x90