Development/Android

[Android] Api 통신 구조를 MVVM + Hilt + Flow 형태로 리팩토링

SeungYong.Lee 2025. 4. 9. 14:32
반응형

- 기존 내 서비스에서 구성되어 있던 API 호출 구조는 매번 Api Task Class를 만들어 Base를 통해 Retrofit 객체를 생성하고 호출하는 과정이었다.

- 각종 초기화 및 고정 Header 값 추가 등의 코드가 난잡하게 구성되어 있어 가독성 및 관리에 좋지 않았다.

 

- 의존성 자동 주입을 위해 Hilt와 이참에 MVVM 패턴까지 적용해 봤다.

Network Module

- 일단 기존에는 Api Task Class 내부에 매번 API 인터페이스를 다르게 생성했는데, 전반적인 모든 API func을 담을 ApiService를 구성했다.

interface ApiService {
    @GET("api/test")
    suspend fun getTest(
        @Query("zoneId") zoneId: String
    ): TestResult?
    
    .
    .
    .
}

 

- 다음으로 Hilt를 기반으로 Retrofit 및 OkHttpClient를 제공해 줄 Network Module을 선언

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {

...

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(HeaderInterceptor())
            .connectTimeout(120, TimeUnit.SECONDS)
            .writeTimeout(120, TimeUnit.SECONDS)
            .readTimeout(120, TimeUnit.SECONDS)
            .authenticator(TokenAuthenticator())
            .addInterceptor(RetryInterceptor())
            ...
            .apply {
                if (BuildConfig.DEBUG) {
                    addInterceptor(HttpLoggingInterceptor().apply {
                        level = HttpLoggingInterceptor.Level.BODY
                    })
                }
            }.build()
    }

    @Singleton
    @Provides
    fun provideRetrofit(
        okHttpClient: OkHttpClient
    ): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java)
}

- DataBase 모듈 구성 때와 동일하게 싱글톤 컴포넌트 범위로 지정한다.

 

- 그리고 OkHttpClient에 대해서도 구성을 진행했다. OkHttpClient 생성 함수 내부에 authenticator와 RetryInterceptor 등이 존재하는데, 이 내용이 필요 없는 통신의 경우도 있어 추후 그 분기 처리에 대해서도 작성해 보겠다.

 

- 그리고 Retrofit 인스턴스를 주입해 줄 provideRetrofit과 provideApiService를 구성한다.

UI State

- Sealed 클래스를 활용하여 UI에서 활용할 상태 클래스를 선언한다. 기본적으로 Loading, Success, Error로 나누어 Flow 활용 시 emit 되는 진행 상태에 따라 UI 구성 처리가 가능하다.

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String = "Error") : UiState<Nothing>()
}

- Loading 상태의 경우 실제 아직 데이터가 없기 때문에 Nothing으로 정의

 

- Success에서는 데이터 수신에 성공한 경우로서, Response의 대상이 되는 클래스를 제네릭을 통해 매번 다르게 지정 가능하다.

 

- Error도 결국 Nothing으로 정의했으나 Toast 및 로그 처리 등을 위해 Error Message 또는 code를 남길 수도 있다.
(또는 Error Response 자체를 제네릭으로 받거나 하나의 정형화된 구조를 반환하는 형태로 구성할 수도 있겠습니다.)

Repository

- API에서 브리핑 데이터를 가져오는 역할을 하며, UI 데이터를 전달하기 위한 상태 처리(UiState) Flow 래핑한 구조

(제 작업 기준에서 역할을 해석한 것이라 다른 관점이 있다면 말씀 부탁드립니다.)

class TestRepository @Inject constructor(
    private val apiService: ApiService
) {
    fun getTest(): Flow<UiState<TestResult>> = flow {
        emit(UiState.Loading)
        try {
            val result = apiService.getTest(App.id)
            if (result != null) {
                emit(UiState.Success(result))
            } else {
                emit(UiState.Error())
            }
        } catch (e: Exception) {
            emit(UiState.Error(e.localizedMessage ?: "Unknown error"))
        }
    }.flowOn(Dispatchers.IO)
}

- 주입된 ApiService를 통해 Response를 받아오고, 그 과정에서 UiState를 로딩 -> 성공 or 실패의 형태로 Flow를 방출한다.

 

- 방출되는 Flow를 ViewModel에서 수신받아 UI에 StateFlow로 반환할 수 있도록 collect 한다.

 

- 네트워크 통신이기 때문에 IO Dispatcher에서 Flow 실행

ViewModel & Presentation

@HiltViewModel
class TestViewModel @Inject constructor(
    private val repository: TestRepository
): ViewModel() {
    private val _testState = MutableStateFlow<UiState<TestResult>>(UiState.Loading)
    val testState: StateFlow<UiState<TestResult>> = _testState

    fun fetchTest() {
        viewModelScope.launch {
            repository.getTest().collect {
                _testState.value = it
            }
        }
    }
}

- MutableStateFlow는 내부에서 값을 변경할 수 있는 상태 흐름이다. Private으로 선언하고, collect를 통해 데이터를 수신받으면 갱신하여 UI 레벨에서는 불변형의 StateFlow만 확인 가능하도록 구성 -> 캡슐화

 

- 상태 흐름을 통해 값이 변경되면 StateFlow는 UI에게 변화를 알려주고 UI에서는 예를 들어 아래와 같이 처리할 수 있다.

activity?.lifecycleScope?.launch {
    testViewModel?.testState?.collect { state ->
        fun hideSkeleton() {
            skeletonLy.visibility = View.GONE
        }
        when (state) {
            is UiState.Success -> {
                hideSkeleton()
                refreshResult(state.data)
            }
            is UiState.Error -> {
                hideSkeleton()
                setErrorMessage(state.message)
            }
            is UiState.Loading -> {
                skeletonLy.visibility = View.VISIBLE
            }
        }
    }
}

- 이런 식으로 점차 MVVM의 형태로 프로젝트 패턴 구조를 잡아가고, 추후 클린 아키텍처로까지의 개선을 바라보고 있다.

반응형