본문 바로가기
⛏️ | 개발 기록/🪐 | Cosmic Detox

[Android] clean architecture에 firebase 로직 적용하기

by immgga 2024. 8. 30.

출처: unsplash.com

 

부트캠프 최종 팀 프로젝트 기록 4

 

 

서론

팀원들과 UI 구성을 마무리 짓고 이제 본격적으로 기능 구현에 돌입했다.

내가 맡은 기능은 디톡스 타이머가 작동 중일 때, 실행이 가능한 허용 앱들을 불러와서 정해진 시간만큼만 사용할 수 있도록 구현하는 것이다.

정해진 시간만큼 사용하는 기능을 구현하기 전에, 실행이 가능한 앱들을 firestore에서 불러오는 작업을 먼저 진행하겠다.

 

 

fireStore 로직을 clean architecture로 구현하기

clean architecture의 구조는 이 포스팅을 보는 사람들을 다 어느 정도는 알고 있다는 가정하에 간단하게 설명하겠다.

clean architecture는 data, domain, presentation 계층으로 나뉘어 있고

data에는 데이터를 저장할 data class, api service, repository 구현체가 존재한다.

domain에는 repository, usecase,

presentation에는 activity or fragment, viewmodel이 존재한다.

 

clean architecture를 공부하는 데 도움이 될만한 블로그의 링크를 남겨두겠다.

https://medium.com/cj-onstyle/android-%EB%B2%84%ED%8B%B0%EC%BB%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%98-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%8F%84%EC%9E%85-a26d833e103c

 

왜 Android 신규 프로젝트는 클린 아키텍처를 도입하였는가

소프트웨어 개발은 복잡한 문제를 해결하기 위해 코드를 작성하는 것 이상을 필요로 합니다. 단순히 결과론으로 화면을 보여주는 것만 생각하고 개발한다면, 코드는 점차 커지며 나중에는 손볼

medium.com

 

 

repository 구현

우선 domain의 repository 우선 구현해 주겠다.

interface AllowedAppRepository {
    fun getAllowedApps(callback: (List<AllowedApp>) -> Unit, failCallback: (Exception?) -> Unit)
}

callback과 failCallback이 존재하는데, 이 파라미터들은 fireStore에서 데이터를 불러오는 데 성공 또는 실패했을 때 값이 들어갈 콜백들이다.

 

다음으로, repository의 구현체를 구현해 보겠다.

repository + implementation의 합성어인 repositoryImpl에서는 interface에 구현된 getAllowedApps를 구현해 준다.

class AllowedAppRepositoryImpl @Inject constructor(
    private val fireStore: FirebaseFirestore,
    private val firebaseAuth: FirebaseAuth
): AllowedAppRepository {
    override fun getAllowedApps(callback: (List<AllowedApp>) -> Unit, failCallback: (Exception?) -> Unit) {
        val fireStoreRef = fireStore.collection("users")
            .document(firebaseAuth.currentUser?.uid ?: "test1")
            .collection("apps")

        fireStoreRef.get().addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val docs = task.result.documents.map {
                    it.toObject<AllowedApp>() ?: AllowedApp()
                }
                callback(docs)
            } else {
                failCallback(task.exception)
            }
        }
    }
}

hilt를 사용 중이기 때문에 fireStore 값과 firebaseAuth 값을 받아 온다.

fireStore를 이용해 내가 불러올 데이터의 reference(경로)를 지정해 준다.

 

지정한 경로로 값을 모두 불러오려면 task.result.documents를 사용해 주어야 한다.

불러온 값을 map을 이용해 모두 AllowedApp으로 변환해서 저장한다. 원래는 string map형식으로 받아와 진다.

받아온 값을 callback으로 넘겨주고, 받아오는 데 실패하면 failCallback에 exception을 담는다.

 

 

useCase 구현

useCase에는 이전에 repository에서 구현한 함수를 받아오는 작업을 한다.

class AllowedAppUseCase @Inject constructor(private val allowedAppRepositoryImpl: AllowedAppRepositoryImpl) {
    operator fun invoke(callback: (List<AllowedApp>) -> Unit, failCallback: (Exception?) -> Unit) {
        allowedAppRepositoryImpl.getAllowedApps(callback, failCallback)
    }
}

invoke 함수를 사용해서 파라미터 그대로 받아온다.

 

 

viewModel 구현

viewModel에서는 useCase에서 받아온 데이터에 따라 state를 정의하고 ui에 쓰기 위해 stateFlow로 가공하는 작업을 진행한다.

@HiltViewModel
class AllowedAppViewModel @Inject constructor(
    private val allowedAppUseCase: AllowedAppUseCase
): ViewModel() {
    private val _allowedAppList =  MutableStateFlow<GetListUiState<List<AllowedApp>>>(GetListUiState.Init)
    val allowedAppList: StateFlow<GetListUiState<List<AllowedApp>>> = _allowedAppList

    fun getAllAllowedApps() {
        viewModelScope.launch {
            _allowedAppList.value = GetListUiState.Loading

            allowedAppUseCase(
                callback = { allowedApps ->
                    _allowedAppList.value = if (allowedApps.isEmpty()) GetListUiState.Empty
                    else GetListUiState.Success(allowedApps)
                },
                failCallback = { exception ->
                    _allowedAppList.value = GetListUiState.Failure(exception)
                }
            )
        }
    }
}

허용된 앱 목록을 불러오기 위한 allowedAppList를 생성해 주고, 함수 내부에서는

UiState를 변경해 주면서 useCase의 결과를 Success 또는 Failure에 저장한다.

 

useCase에 정의한 callback들이 모두 여기에 쓰이게 된다.

 

 

구현 중 발생한 문제 상황

처음에 repository를 구현하려 할 때, 무조건 return 값을 정의하려고 하다가 실패한 경험이 있다.

리스트를 정의해서 firestore 로직이 success, failure에 따라 return 값을 지정하려고 했는데 하지 못했다.

Flow까지 적용하려다 보니까 머리가 아파왔기 때문에 그냥 콜백으로 처리하고 Flow도 사용하지 않았다.

앞으로는 나대지 말아야겠다.

 

 

정리

clean architecture로 firebase fireStore 구현 로직을 구현했다.

욕심을 버리니까 모든 일(?)이 쉽게 풀렸다.

return과 flow 적용은 다음 기회에 시도해 보려 한다.

728x90